diff --git a/.changeset/better-hats-cross.md b/.changeset/better-hats-cross.md deleted file mode 100644 index 99444b7fbf..0000000000 --- a/.changeset/better-hats-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Adjusted the log line wording of task worker starting diff --git a/.changeset/better-steaks-act.md b/.changeset/better-steaks-act.md deleted file mode 100644 index b3ce601245..0000000000 --- a/.changeset/better-steaks-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The templates executed with the `yarn new` command now supports templating filenames. diff --git a/.changeset/big-rabbits-think.md b/.changeset/big-rabbits-think.md new file mode 100644 index 0000000000..bd0ae32b5e --- /dev/null +++ b/.changeset/big-rabbits-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixes a bug where the `EntityListProvider` would not correctly hydrate query parameters if more than 20 were provided for the same key. diff --git a/.changeset/cruel-plums-talk.md b/.changeset/cruel-plums-talk.md deleted file mode 100644 index bc7a30b048..0000000000 --- a/.changeset/cruel-plums-talk.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Migrated Checkbox component from Base UI to React Aria Components. - -API changes required: - -- `checked` → `isSelected` -- `defaultChecked` → `defaultSelected` -- `disabled` → `isDisabled` -- `required` → `isRequired` -- `label` prop removed - use `children` instead -- CSS: `bui-CheckboxLabel` class removed -- Data attribute: `data-checked` → `data-selected` -- Use without label is no longer supported - -Migration examples: - -Before: - -```tsx - -``` - -After: - -```tsx - - Accept terms - -``` - -Before: - -```tsx - -``` - -After: - -```tsx -Option -``` - -Before: - -```tsx - -``` - -After: - -```tsx - - Accessible label - -``` diff --git a/.changeset/every-ants-count.md b/.changeset/every-ants-count.md deleted file mode 100644 index 963c3a521a..0000000000 --- a/.changeset/every-ants-count.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-test-utils': minor ---- - -Added support for Postgres 18 to the available `TestDatabases`. - -Note that the set of _default_ databases to test against for users of the `TestDatabases` class was also updated to include Postgres 14 and 18, instead of 13 and 17. If you need to override this, you can pass in an explicit `ids` argument, for example `ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3']`. diff --git a/.changeset/every-clocks-arrive.md b/.changeset/every-clocks-arrive.md deleted file mode 100644 index d2061913e3..0000000000 --- a/.changeset/every-clocks-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/dev-utils': patch ---- - -Fixed styling of the dev app by adding a lazy import of `@backstage/ui/css/styles.css`. diff --git a/.changeset/fine-hands-return.md b/.changeset/fine-hands-return.md deleted file mode 100644 index d3bf69c9b9..0000000000 --- a/.changeset/fine-hands-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix default text color in Backstage UI diff --git a/.changeset/five-seas-jam.md b/.changeset/five-seas-jam.md deleted file mode 100644 index 4aa481422f..0000000000 --- a/.changeset/five-seas-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-notifications-backend-module-email': patch ---- - -Updated `AWS SES` client to version 2 to support `nodemailer` version 7. diff --git a/.changeset/grumpy-planes-bet.md b/.changeset/grumpy-planes-bet.md deleted file mode 100644 index bf7d36be41..0000000000 --- a/.changeset/grumpy-planes-bet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch ---- - -Fixed a bug in the Scaffolder's template parsing in the `useTemplateSchema` hook by removing the title instead of setting it to `undefined` diff --git a/.changeset/loud-carpets-throw.md b/.changeset/loud-carpets-throw.md deleted file mode 100644 index 169703d815..0000000000 --- a/.changeset/loud-carpets-throw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Added a template for the `yarn new` command to create an catalog entity provider. To add this template to an explicit list in the root `package.json`, use `@backstage/cli/templates/catalog-provider-module`. diff --git a/.changeset/ninety-cobras-feel.md b/.changeset/ninety-cobras-feel.md deleted file mode 100644 index e287ef33ed..0000000000 --- a/.changeset/ninety-cobras-feel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fixed an issue where the entire TechDocs page would re-render when navigating between pages within the same entity's documentation. diff --git a/.changeset/polite-seas-divide.md b/.changeset/polite-seas-divide.md deleted file mode 100644 index e265e6df90..0000000000 --- a/.changeset/polite-seas-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix the default font size in Backstage UI. diff --git a/.changeset/pre.json b/.changeset/pre.json index 67e48bb29c..c162749562 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,230 +2,211 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.114", - "@backstage/app-defaults": "1.7.1", - "example-app-next": "0.0.28", - "app-next-example-plugin": "0.0.28", - "example-backend": "0.0.43", - "@backstage/backend-app-api": "1.2.8", - "@backstage/backend-defaults": "0.13.0", - "@backstage/backend-dev-utils": "0.1.5", - "@backstage/backend-dynamic-feature-service": "0.7.5", - "@backstage/backend-openapi-utils": "0.6.2", - "@backstage/backend-plugin-api": "1.4.4", - "@backstage/backend-test-utils": "1.9.1", - "@backstage/catalog-client": "1.12.0", - "@backstage/catalog-model": "1.7.5", - "@backstage/cli": "0.34.4", - "@backstage/cli-common": "0.1.15", - "@backstage/cli-node": "0.2.14", - "@backstage/codemods": "0.1.52", - "@backstage/config": "1.3.5", - "@backstage/config-loader": "1.10.5", - "@backstage/core-app-api": "1.19.1", - "@backstage/core-compat-api": "0.5.3", - "@backstage/core-components": "0.18.2", - "@backstage/core-plugin-api": "1.11.1", - "@backstage/create-app": "0.7.5", - "@backstage/dev-utils": "1.1.15", - "e2e-test": "0.2.33", + "example-app": "0.2.117", + "@backstage/app-defaults": "1.7.4", + "example-app-next": "0.0.31", + "app-next-example-plugin": "0.0.31", + "example-backend": "0.0.46", + "@backstage/backend-app-api": "1.4.1", + "@backstage/backend-defaults": "0.15.0", + "@backstage/backend-dev-utils": "0.1.6", + "@backstage/backend-dynamic-feature-service": "0.7.8", + "@backstage/backend-openapi-utils": "0.6.5", + "@backstage/backend-plugin-api": "1.6.1", + "@backstage/backend-test-utils": "1.10.3", + "@backstage/catalog-client": "1.12.1", + "@backstage/catalog-model": "1.7.6", + "@backstage/cli": "0.35.2", + "@backstage/cli-common": "0.1.17", + "@backstage/cli-node": "0.2.16", + "@backstage/codemods": "0.1.53", + "@backstage/config": "1.3.6", + "@backstage/config-loader": "1.10.7", + "@backstage/core-app-api": "1.19.3", + "@backstage/core-compat-api": "0.5.6", + "@backstage/core-components": "0.18.5", + "@backstage/core-plugin-api": "1.12.1", + "@backstage/create-app": "0.7.8", + "@backstage/dev-utils": "1.1.19", + "e2e-test": "0.2.36", "@backstage/e2e-test-utils": "0.1.1", "@backstage/errors": "1.2.7", - "@backstage/eslint-plugin": "0.1.12", - "@backstage/frontend-app-api": "0.13.1", - "@backstage/frontend-defaults": "0.3.2", - "@backstage/frontend-dynamic-feature-loader": "0.1.6", - "@internal/frontend": "0.0.14", - "@backstage/frontend-plugin-api": "0.12.1", - "@backstage/frontend-test-utils": "0.4.0", - "@backstage/integration": "1.18.1", - "@backstage/integration-aws-node": "0.1.18", - "@backstage/integration-react": "1.2.11", + "@backstage/eslint-plugin": "0.2.0", + "@backstage/frontend-app-api": "0.14.0", + "@backstage/frontend-defaults": "0.3.5", + "@backstage/frontend-dynamic-feature-loader": "0.1.8", + "@internal/frontend": "0.0.16", + "@backstage/frontend-plugin-api": "0.13.3", + "@backstage/frontend-test-utils": "0.4.3", + "@backstage/integration": "1.19.2", + "@backstage/integration-aws-node": "0.1.19", + "@backstage/integration-react": "1.2.14", "@internal/opaque": "0.0.1", "@backstage/release-manifests": "0.0.13", - "@backstage/repo-tools": "0.15.3", - "@internal/scaffolder": "0.0.14", - "@techdocs/cli": "1.10.0", - "techdocs-cli-embedded-app": "0.2.113", - "@backstage/test-utils": "1.7.12", - "@backstage/theme": "0.7.0", + "@backstage/repo-tools": "0.16.2", + "@internal/scaffolder": "0.0.17", + "@techdocs/cli": "1.10.4", + "techdocs-cli-embedded-app": "0.2.116", + "@backstage/test-utils": "1.7.14", + "@backstage/theme": "0.7.1", "@backstage/types": "1.2.2", - "@backstage/ui": "0.8.0", + "@backstage/ui": "0.11.0", "@backstage/version-bridge": "1.0.11", - "yarn-plugin-backstage": "0.0.7", - "@backstage/plugin-api-docs": "0.13.0", + "yarn-plugin-backstage": "0.0.8", + "@backstage/plugin-api-docs": "0.13.3", "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10", - "@backstage/plugin-app": "0.3.1", - "@backstage/plugin-app-backend": "0.5.7", - "@backstage/plugin-app-node": "0.1.38", - "@backstage/plugin-app-visualizer": "0.1.24", - "@backstage/plugin-auth": "0.1.1", - "@backstage/plugin-auth-backend": "0.25.5", - "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.8", - "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.13", - "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.8", - "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-github-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-google-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-guest-provider": "0.2.13", - "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.13", - "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.8", - "@backstage/plugin-auth-backend-module-okta-provider": "0.2.8", - "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.1", - "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.8", - "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.8", - "@backstage/plugin-auth-node": "0.6.8", - "@backstage/plugin-auth-react": "0.1.20", - "@backstage/plugin-bitbucket-cloud-common": "0.3.3", - "@backstage/plugin-catalog": "1.31.4", - "@backstage/plugin-catalog-backend": "3.1.2", - "@backstage/plugin-catalog-backend-module-aws": "0.4.16", - "@backstage/plugin-catalog-backend-module-azure": "0.3.10", - "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.7", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.4", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.4", - "@backstage/plugin-catalog-backend-module-gcp": "0.3.13", - "@backstage/plugin-catalog-backend-module-gerrit": "0.3.7", - "@backstage/plugin-catalog-backend-module-gitea": "0.1.5", - "@backstage/plugin-catalog-backend-module-github": "0.11.1", - "@backstage/plugin-catalog-backend-module-github-org": "0.3.15", - "@backstage/plugin-catalog-backend-module-gitlab": "0.7.4", - "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.14", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.5", - "@backstage/plugin-catalog-backend-module-ldap": "0.11.10", - "@backstage/plugin-catalog-backend-module-logs": "0.1.15", - "@backstage/plugin-catalog-backend-module-msgraph": "0.8.1", - "@backstage/plugin-catalog-backend-module-openapi": "0.2.15", - "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.15", - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.13", - "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.5", - "@backstage/plugin-catalog-common": "1.1.6", - "@backstage/plugin-catalog-graph": "0.5.2", - "@backstage/plugin-catalog-import": "0.13.6", - "@backstage/plugin-catalog-node": "1.19.1", - "@backstage/plugin-catalog-react": "1.21.2", - "@backstage/plugin-catalog-unprocessed-entities": "0.2.22", - "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.10", - "@backstage/plugin-config-schema": "0.1.73", - "@backstage/plugin-devtools": "0.1.32", - "@backstage/plugin-devtools-backend": "0.5.10", - "@backstage/plugin-devtools-common": "0.1.18", - "@backstage/plugin-events-backend": "0.5.7", - "@backstage/plugin-events-backend-module-aws-sqs": "0.4.16", - "@backstage/plugin-events-backend-module-azure": "0.2.25", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.25", - "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.6", - "@backstage/plugin-events-backend-module-gerrit": "0.2.25", - "@backstage/plugin-events-backend-module-github": "0.4.5", - "@backstage/plugin-events-backend-module-gitlab": "0.3.6", - "@backstage/plugin-events-backend-module-google-pubsub": "0.1.5", - "@backstage/plugin-events-backend-module-kafka": "0.1.4", - "@backstage/plugin-events-backend-test-utils": "0.1.49", - "@backstage/plugin-events-node": "0.4.16", - "@internal/plugin-todo-list": "1.0.44", - "@internal/plugin-todo-list-backend": "1.0.44", - "@internal/plugin-todo-list-common": "1.0.27", - "@backstage/plugin-gateway-backend": "1.0.6", - "@backstage/plugin-home": "0.8.13", - "@backstage/plugin-home-react": "0.1.31", - "@backstage/plugin-kubernetes": "0.12.12", - "@backstage/plugin-kubernetes-backend": "0.20.3", - "@backstage/plugin-kubernetes-cluster": "0.0.30", - "@backstage/plugin-kubernetes-common": "0.9.7", - "@backstage/plugin-kubernetes-node": "0.3.5", - "@backstage/plugin-kubernetes-react": "0.5.12", - "@backstage/plugin-mcp-actions-backend": "0.1.4", - "@backstage/plugin-mui-to-bui": "0.2.0", - "@backstage/plugin-notifications": "0.5.10", - "@backstage/plugin-notifications-backend": "0.5.11", - "@backstage/plugin-notifications-backend-module-email": "0.3.14", - "@backstage/plugin-notifications-backend-module-slack": "0.2.0", - "@backstage/plugin-notifications-common": "0.1.1", - "@backstage/plugin-notifications-node": "0.2.20", - "@backstage/plugin-org": "0.6.45", - "@backstage/plugin-org-react": "0.1.43", - "@backstage/plugin-permission-backend": "0.7.5", - "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.13", - "@backstage/plugin-permission-common": "0.9.2", - "@backstage/plugin-permission-node": "0.10.5", - "@backstage/plugin-permission-react": "0.4.37", - "@backstage/plugin-proxy-backend": "0.6.7", - "@backstage/plugin-proxy-node": "0.1.9", - "@backstage/plugin-scaffolder": "1.34.2", - "@backstage/plugin-scaffolder-backend": "3.0.0", - "@backstage/plugin-scaffolder-backend-module-azure": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.15", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.14", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.16", - "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-github": "0.9.1", - "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.6", - "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.15", - "@backstage/plugin-scaffolder-backend-module-rails": "0.5.14", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.14", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.15", - "@backstage/plugin-scaffolder-common": "1.7.2", - "@backstage/plugin-scaffolder-node": "0.12.0", - "@backstage/plugin-scaffolder-node-test-utils": "0.3.4", - "@backstage/plugin-scaffolder-react": "1.19.2", - "@backstage/plugin-search": "1.4.31", - "@backstage/plugin-search-backend": "2.0.7", - "@backstage/plugin-search-backend-module-catalog": "0.3.9", - "@backstage/plugin-search-backend-module-elasticsearch": "1.7.7", - "@backstage/plugin-search-backend-module-explore": "0.3.8", - "@backstage/plugin-search-backend-module-pg": "0.5.49", - "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.14", - "@backstage/plugin-search-backend-module-techdocs": "0.4.7", - "@backstage/plugin-search-backend-node": "1.3.16", - "@backstage/plugin-search-common": "1.2.20", - "@backstage/plugin-search-react": "1.9.5", - "@backstage/plugin-signals": "0.0.24", - "@backstage/plugin-signals-backend": "0.3.9", - "@backstage/plugin-signals-node": "0.1.25", - "@backstage/plugin-signals-react": "0.0.16", - "@backstage/plugin-techdocs": "1.15.1", - "@backstage/plugin-techdocs-addons-test-utils": "1.1.1", - "@backstage/plugin-techdocs-backend": "2.1.1", + "@backstage/plugin-app": "0.3.4", + "@backstage/plugin-app-backend": "0.5.10", + "@backstage/plugin-app-node": "0.1.41", + "@backstage/plugin-app-react": "0.1.0", + "@backstage/plugin-app-visualizer": "0.1.27", + "@backstage/plugin-auth": "0.1.4", + "@backstage/plugin-auth-backend": "0.26.0", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.10", + "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.10", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.11", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.15", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.10", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.10", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.10", + "@backstage/plugin-auth-backend-module-github-provider": "0.4.0", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-google-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-guest-provider": "0.2.15", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.10", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.15", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.11", + "@backstage/plugin-auth-backend-module-okta-provider": "0.2.10", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.3", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.10", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.10", + "@backstage/plugin-auth-node": "0.6.11", + "@backstage/plugin-auth-react": "0.1.23", + "@backstage/plugin-bitbucket-cloud-common": "0.3.6", + "@backstage/plugin-catalog": "1.32.2", + "@backstage/plugin-catalog-backend": "3.3.1", + "@backstage/plugin-catalog-backend-module-aws": "0.4.19", + "@backstage/plugin-catalog-backend-module-azure": "0.3.13", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.10", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.7", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.7", + "@backstage/plugin-catalog-backend-module-gcp": "0.3.15", + "@backstage/plugin-catalog-backend-module-gerrit": "0.3.10", + "@backstage/plugin-catalog-backend-module-gitea": "0.1.8", + "@backstage/plugin-catalog-backend-module-github": "0.12.1", + "@backstage/plugin-catalog-backend-module-github-org": "0.3.18", + "@backstage/plugin-catalog-backend-module-gitlab": "0.7.7", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.17", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.8", + "@backstage/plugin-catalog-backend-module-ldap": "0.12.1", + "@backstage/plugin-catalog-backend-module-logs": "0.1.18", + "@backstage/plugin-catalog-backend-module-msgraph": "0.8.4", + "@backstage/plugin-catalog-backend-module-openapi": "0.2.18", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.18", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.16", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.7", + "@backstage/plugin-catalog-common": "1.1.7", + "@backstage/plugin-catalog-graph": "0.5.6", + "@backstage/plugin-catalog-import": "0.13.9", + "@backstage/plugin-catalog-node": "1.20.1", + "@backstage/plugin-catalog-react": "1.21.5", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.25", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.12", + "@backstage/plugin-config-schema": "0.1.76", + "@backstage/plugin-devtools": "0.1.35", + "@backstage/plugin-devtools-backend": "0.5.13", + "@backstage/plugin-devtools-common": "0.1.21", + "@backstage/plugin-devtools-react": "0.1.0", + "@backstage/plugin-events-backend": "0.5.10", + "@backstage/plugin-events-backend-module-aws-sqs": "0.4.18", + "@backstage/plugin-events-backend-module-azure": "0.2.27", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.27", + "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.8", + "@backstage/plugin-events-backend-module-gerrit": "0.2.27", + "@backstage/plugin-events-backend-module-github": "0.4.8", + "@backstage/plugin-events-backend-module-gitlab": "0.3.8", + "@backstage/plugin-events-backend-module-google-pubsub": "0.1.7", + "@backstage/plugin-events-backend-module-kafka": "0.3.0", + "@backstage/plugin-events-backend-test-utils": "0.1.51", + "@backstage/plugin-events-node": "0.4.18", + "@internal/plugin-todo-list": "1.0.47", + "@internal/plugin-todo-list-backend": "1.0.46", + "@internal/plugin-todo-list-common": "1.0.28", + "@backstage/plugin-gateway-backend": "1.1.1", + "@backstage/plugin-home": "0.9.0", + "@backstage/plugin-home-react": "0.1.34", + "@backstage/plugin-kubernetes": "0.12.15", + "@backstage/plugin-kubernetes-backend": "0.21.0", + "@backstage/plugin-kubernetes-cluster": "0.0.33", + "@backstage/plugin-kubernetes-common": "0.9.9", + "@backstage/plugin-kubernetes-node": "0.4.0", + "@backstage/plugin-kubernetes-react": "0.5.15", + "@backstage/plugin-mcp-actions-backend": "0.1.7", + "@backstage/plugin-mui-to-bui": "0.2.3", + "@backstage/plugin-notifications": "0.5.13", + "@backstage/plugin-notifications-backend": "0.6.1", + "@backstage/plugin-notifications-backend-module-email": "0.3.17", + "@backstage/plugin-notifications-backend-module-slack": "0.3.0", + "@backstage/plugin-notifications-common": "0.2.0", + "@backstage/plugin-notifications-node": "0.2.22", + "@backstage/plugin-org": "0.6.48", + "@backstage/plugin-org-react": "0.1.46", + "@backstage/plugin-permission-backend": "0.7.7", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.15", + "@backstage/plugin-permission-common": "0.9.4", + "@backstage/plugin-permission-node": "0.10.8", + "@backstage/plugin-permission-react": "0.4.39", + "@backstage/plugin-proxy-backend": "0.6.9", + "@backstage/plugin-proxy-node": "0.1.11", + "@backstage/plugin-scaffolder": "1.35.1", + "@backstage/plugin-scaffolder-backend": "3.1.1", + "@backstage/plugin-scaffolder-backend-module-azure": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.18", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.3.1", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.17", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.19", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.17", + "@backstage/plugin-scaffolder-backend-module-github": "0.9.4", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.11.1", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.18", + "@backstage/plugin-scaffolder-backend-module-rails": "0.5.17", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.3.0", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.18", + "@backstage/plugin-scaffolder-common": "1.7.5", + "@backstage/plugin-scaffolder-node": "0.12.3", + "@backstage/plugin-scaffolder-node-test-utils": "0.3.7", + "@backstage/plugin-scaffolder-react": "1.19.5", + "@backstage/plugin-search": "1.5.3", + "@backstage/plugin-search-backend": "2.0.10", + "@backstage/plugin-search-backend-module-catalog": "0.3.11", + "@backstage/plugin-search-backend-module-elasticsearch": "1.7.9", + "@backstage/plugin-search-backend-module-explore": "0.3.10", + "@backstage/plugin-search-backend-module-pg": "0.5.51", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.16", + "@backstage/plugin-search-backend-module-techdocs": "0.4.10", + "@backstage/plugin-search-backend-node": "1.4.0", + "@backstage/plugin-search-common": "1.2.21", + "@backstage/plugin-search-react": "1.10.2", + "@backstage/plugin-signals": "0.0.27", + "@backstage/plugin-signals-backend": "0.3.11", + "@backstage/plugin-signals-node": "0.1.27", + "@backstage/plugin-signals-react": "0.0.18", + "@backstage/plugin-techdocs": "1.16.2", + "@backstage/plugin-techdocs-addons-test-utils": "2.0.1", + "@backstage/plugin-techdocs-backend": "2.1.4", "@backstage/plugin-techdocs-common": "0.1.1", - "@backstage/plugin-techdocs-module-addons-contrib": "1.1.29", - "@backstage/plugin-techdocs-node": "1.13.8", - "@backstage/plugin-techdocs-react": "1.3.4", - "@backstage/plugin-user-settings": "0.8.27", - "@backstage/plugin-user-settings-backend": "0.3.7", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.32", + "@backstage/plugin-techdocs-node": "1.14.0", + "@backstage/plugin-techdocs-react": "1.3.7", + "@backstage/plugin-user-settings": "0.8.31", + "@backstage/plugin-user-settings-backend": "0.3.10", "@backstage/plugin-user-settings-common": "0.0.1" }, - "changesets": [ - "better-hats-cross", - "better-steaks-act", - "every-ants-count", - "every-clocks-arrive", - "fine-hands-return", - "five-seas-jam", - "grumpy-planes-bet", - "loud-carpets-throw", - "ninety-cobras-feel", - "polite-seas-divide", - "rich-streets-rule", - "short-sides-feel", - "silver-garlics-thank", - "solid-bees-agree", - "solid-dancers-march", - "stupid-doodles-love", - "tender-regions-know", - "typescript-constructor-refactor", - "warm-moments-repeat", - "wild-owls-divide" - ] + "changesets": [] } diff --git a/.changeset/rich-streets-rule.md b/.changeset/rich-streets-rule.md deleted file mode 100644 index 07f431b6a7..0000000000 --- a/.changeset/rich-streets-rule.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix CSS layer ordering in Backstage UI to make sure component styles are loaded after tokens and base declarations. diff --git a/.changeset/seven-cycles-pick.md b/.changeset/seven-cycles-pick.md deleted file mode 100644 index fbd1a3895f..0000000000 --- a/.changeset/seven-cycles-pick.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Changed className prop behavior to augment default styles instead of being ignored or overriding them. - -Affected components: - -- Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator -- Switch -- Skeleton -- FieldLabel -- Header, HeaderToolbar -- HeaderPage -- Tabs, TabList, Tab, TabPanel - -If you were passing custom className values to any of these components that relied on the previous behavior, you may need to adjust your styles to account for the default classes now being applied alongside your custom classes. diff --git a/.changeset/seven-states-sleep.md b/.changeset/seven-states-sleep.md new file mode 100644 index 0000000000..1e612b3346 --- /dev/null +++ b/.changeset/seven-states-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Fixed Router deprecation warning and switched to using new `RouterBlueprint` from `@backstage/plugin-app-api`. diff --git a/.changeset/short-sides-feel.md b/.changeset/short-sides-feel.md deleted file mode 100644 index a7e3542ba1..0000000000 --- a/.changeset/short-sides-feel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Added the new `@backstage/cli/templates/catalog-provider-module` template to the explicit template configuration for the `next-app` template. diff --git a/.changeset/silver-garlics-thank.md b/.changeset/silver-garlics-thank.md deleted file mode 100644 index d95781eb04..0000000000 --- a/.changeset/silver-garlics-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix font smoothing as default in Backstage UI. diff --git a/.changeset/slimy-dots-sing.md b/.changeset/slimy-dots-sing.md new file mode 100644 index 0000000000..a7f3a8a07b --- /dev/null +++ b/.changeset/slimy-dots-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Improve sidebars (nav/TOC) layout and scrolling diff --git a/.changeset/slimy-zebras-lie.md b/.changeset/slimy-zebras-lie.md new file mode 100644 index 0000000000..4ea1ce1791 --- /dev/null +++ b/.changeset/slimy-zebras-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-github-provider': minor +--- + +Added the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by their GitHub user ID. diff --git a/.changeset/smart-donuts-teach.md b/.changeset/smart-donuts-teach.md deleted file mode 100644 index f258dee394..0000000000 --- a/.changeset/smart-donuts-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Enable tree-shaking of imports other than `*.css`. diff --git a/.changeset/solid-bees-agree.md b/.changeset/solid-bees-agree.md deleted file mode 100644 index 8feecaf6d4..0000000000 --- a/.changeset/solid-bees-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/eslint-plugin': minor ---- - -Added `@backstage/no-ui-css-imports-in-non-frontend` rule, which ensures that CSS from `@backstage/ui` is not imported outside of the frontend app. diff --git a/.changeset/solid-dancers-march.md b/.changeset/solid-dancers-march.md deleted file mode 100644 index 5633bb3e2e..0000000000 --- a/.changeset/solid-dancers-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Revert `storageApiRef` implementation diff --git a/.changeset/stupid-doodles-love.md b/.changeset/stupid-doodles-love.md deleted file mode 100644 index 878ad853cd..0000000000 --- a/.changeset/stupid-doodles-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Fix for missing styles due to move to BUI. diff --git a/.changeset/tender-regions-know.md b/.changeset/tender-regions-know.md deleted file mode 100644 index 8f59567597..0000000000 --- a/.changeset/tender-regions-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth': patch ---- - -Authentication content screen now uses application title. diff --git a/.changeset/twelve-dolls-tap.md b/.changeset/twelve-dolls-tap.md new file mode 100644 index 0000000000..33c184b39d --- /dev/null +++ b/.changeset/twelve-dolls-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed React 17 compatibility by using `useId` from `react-aria` instead of the built-in React hook which is only available in React 18+. diff --git a/.changeset/typescript-constructor-refactor.md b/.changeset/typescript-constructor-refactor.md deleted file mode 100644 index b24cb5dbd0..0000000000 --- a/.changeset/typescript-constructor-refactor.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-kubernetes-react': patch -'@backstage/plugin-notifications-backend': patch -'@backstage/plugin-notifications-node': patch -'@backstage/plugin-permission-react': patch -'@backstage/plugin-scaffolder-backend-module-gcp': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-search-backend-module-elasticsearch': patch -'@backstage/plugin-search-backend-module-pg': patch -'@backstage/plugin-search-backend': patch -'@backstage/plugin-search-backend-node': patch -'@backstage/plugin-search-react': patch -'@backstage/plugin-signals': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-events-backend': patch -'@backstage/plugin-catalog-backend-module-backstage-openapi': patch -'@backstage/plugin-events-node': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-notifications-backend-module-email': patch -'@backstage/plugin-events-backend-module-aws-sqs': patch -'@backstage/plugin-devtools-backend': patch -'@backstage/plugin-catalog-backend-module-unprocessed': patch -'@backstage/plugin-catalog-backend-module-azure': patch -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-techdocs-node': patch -'@backstage/plugin-catalog-unprocessed-entities': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-bitbucket-cloud-common': patch -'@backstage/plugin-auth-node': patch -'@backstage/backend-app-api': patch -'@backstage/backend-defaults': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch -'@backstage/frontend-app-api': patch -'@backstage/config': patch -'@backstage/backend-dynamic-feature-service': patch -'@backstage/core-components': patch -'@backstage/cli': patch -'@backstage/cli-node': patch -'@backstage/catalog-model': patch -'@backstage/integration': patch -'@backstage/frontend-plugin-api': patch -'@backstage/integration-aws-node': patch -'@backstage/repo-tools': patch -'@backstage/backend-test-utils': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-events-backend-module-kafka': patch -'@backstage/plugin-kubernetes-node': patch -'@backstage/plugin-mcp-actions-backend': patch -'@backstage/plugin-scaffolder-node-test-utils': patch -'@backstage/plugin-scaffolder-node': patch -'@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-user-settings-backend': patch ---- - -Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. diff --git a/.changeset/upset-teeth-add.md b/.changeset/upset-teeth-add.md deleted file mode 100644 index 834ce7be2c..0000000000 --- a/.changeset/upset-teeth-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-mui-to-bui': patch ---- - -Updated BUI checkbox preview example to align with new component API. diff --git a/.changeset/vast-rockets-dig.md b/.changeset/vast-rockets-dig.md new file mode 100644 index 0000000000..3340fabb2c --- /dev/null +++ b/.changeset/vast-rockets-dig.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +'@backstage/plugin-catalog-backend-module-gitlab': minor +--- + +Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. diff --git a/.changeset/warm-moments-repeat.md b/.changeset/warm-moments-repeat.md deleted file mode 100644 index a72387d720..0000000000 --- a/.changeset/warm-moments-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Fix #31348 issue where BitbucketUrlReader ignored provided token and instead always used integration credentials diff --git a/.changeset/wild-donkeys-sneeze.md b/.changeset/wild-donkeys-sneeze.md deleted file mode 100644 index 5e2c4474c5..0000000000 --- a/.changeset/wild-donkeys-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. diff --git a/.changeset/wild-owls-divide.md b/.changeset/wild-owls-divide.md deleted file mode 100644 index d903b76bd0..0000000000 --- a/.changeset/wild-owls-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix default font wight and font family in Backstage UI. diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc new file mode 100644 index 0000000000..3a81a0cb5f --- /dev/null +++ b/.cursor/rules/general.mdc @@ -0,0 +1,7 @@ +--- +description: General project guidelines for Backstage development +globs: +alwaysApply: true +--- + +Follow the instructions at /.github/copilot-instructions.md diff --git a/.cursor/rules/tests/backend-test-utils.mdc b/.cursor/rules/tests/backend-test-utils.mdc new file mode 100644 index 0000000000..857798380d --- /dev/null +++ b/.cursor/rules/tests/backend-test-utils.mdc @@ -0,0 +1,91 @@ +--- +description: Enforce usage of @backstage/backend-test-utils in backend tests +globs: ['**/*.test.*', '**/*.spec.*'] +alwaysApply: false +--- +# Backstage Backend Testing Rules + +**CRITICAL**: When writing tests for Backstage backend code, you MUST use utilities from `@backstage/backend-test-utils` wherever possible. Do NOT create custom mocks, test databases, caches, or service implementations when equivalent utilities exist in backend-test-utils. + +## Required Usage + +1. **Service Mocking**: Always use `mockServices` from `@backstage/backend-test-utils` instead of creating custom service mocks. This includes: + - `mockServices.auth()` - For authentication service mocking + - `mockServices.httpAuth()` - For HTTP authentication service mocking + - `mockServices.database()` - For database service mocking + - `mockServices.cache()` - For cache service mocking + - `mockServices.logger()` - For logger service mocking + - `mockServices.permissions()` - For permissions service mocking + - `mockServices.scheduler()` - For scheduler service mocking + - `mockServices.userInfo()` - For user info service mocking + - `mockServices.events()` - For events service mocking + - `mockServices.urlReader()` - For URL reader service mocking + - `mockServices.rootConfig()` - For root config service mocking + - `mockServices.rootLogger()` - For root logger service mocking + - `mockServices.rootHealth()` - For root health service mocking + - `mockServices.rootLifecycle()` - For root lifecycle service mocking + - `mockServices.httpRouter()` - For HTTP router service mocking + - `mockServices.lifecycle()` - For lifecycle service mocking + - `mockServices.auditor()` - For auditor service mocking + - `mockServices.permissionsRegistry()` - For permissions registry service mocking + +2. **Credentials**: Always use `mockCredentials` from `@backstage/backend-test-utils` for creating test credentials instead of manually constructing credential objects. + +3. **Test Backend**: Always use `startTestBackend` and `TestBackend` from `@backstage/backend-test-utils` when testing backend features, plugins, or modules. This provides a fully configured test backend with all necessary services. + +4. **Service Factory Testing**: Use `ServiceFactoryTester` from `@backstage/backend-test-utils` when testing individual service factories in isolation. + +5. **Database Testing**: Always use `TestDatabases` from `@backstage/backend-test-utils` for database testing. This supports multiple database engines (PostgreSQL, MySQL, SQLite) and handles ephemeral test database creation and cleanup automatically. + +6. **Cache Testing**: Always use `TestCaches` from `@backstage/backend-test-utils` for cache testing. This supports multiple cache backends (Redis, Valkey, Memcache) and handles ephemeral test cache creation and cleanup automatically. + +7. **Filesystem Testing**: Use `createMockDirectory` from `@backstage/backend-test-utils` when testing code that interacts with the filesystem. This provides a safe, isolated temporary directory for tests. + +8. **MSW Integration**: Use `registerMswTestHooks` from `@backstage/backend-test-utils` when setting up Mock Service Worker for HTTP request mocking in tests. + +9. **Error Handler**: Use `mockErrorHandler` from `@backstage/backend-test-utils` when testing Express routers that need error handling middleware. + +10. **Alpha Services**: For alpha/experimental services, use the utilities from `@backstage/backend-test-utils/alpha` (e.g., `actionsRegistryServiceMock`, `actionsServiceMock`). + +## Import Patterns + +Always import from the main package: + +```typescript +import { + mockServices, + mockCredentials, + startTestBackend, + TestDatabases, + TestCaches, + createMockDirectory, + registerMswTestHooks, + mockErrorHandler, + ServiceFactoryTester +} from '@backstage/backend-test-utils'; +``` + +For alpha services: + +```typescript +import { + actionsRegistryServiceMock, + actionsServiceMock +} from '@backstage/backend-test-utils/alpha'; +``` + +## When to Use + +- **Always** use these utilities when writing backend tests +- **Never** create custom mocks for services that have equivalents in backend-test-utils +- **Never** manually set up test databases or caches when TestDatabases/TestCaches are available +- **Never** create custom test backends when startTestBackend is available + +## Benefits + +Using backend-test-utils ensures: +- Consistent test setup across the codebase +- Proper cleanup and resource management +- Support for multiple database/cache backends +- Well-tested and maintained utilities +- Reduced boilerplate in test code diff --git a/.cursor/rules/tests/test-utils.mdc b/.cursor/rules/tests/test-utils.mdc new file mode 100644 index 0000000000..dd66e1f8fa --- /dev/null +++ b/.cursor/rules/tests/test-utils.mdc @@ -0,0 +1,100 @@ +--- +description: Enforce usage of @backstage/test-utils in frontend tests +globs: ['**/*.test.*', '**/*.spec.*'] +alwaysApply: false +--- +# Backstage Frontend/Plugin Testing Rules + +**CRITICAL**: When writing tests for Backstage frontend code, plugins, or React components, you MUST use utilities from `@backstage/test-utils` wherever possible. Do NOT create custom mocks, test wrappers, or API implementations when equivalent utilities exist in test-utils. + +## Required Usage + +1. **API Mocking**: Always use `mockApis` from `@backstage/test-utils` instead of creating custom API mocks. This includes: + - `mockApis.analytics()` - For AnalyticsApi mocking + - `mockApis.config()` - For ConfigApi mocking with optional configuration data + - `mockApis.discovery()` - For DiscoveryApi mocking + - `mockApis.identity()` - For IdentityApi mocking with user entity ref, token, profile info + - `mockApis.permission()` - For PermissionApi mocking with authorization control + - `mockApis.storage()` - For StorageApi mocking with in-memory storage + - `mockApis.translation()` - For TranslationApi mocking (from alpha) + - Each API also provides `.mock()` for jest mocks and `.factory()` for API factories + +2. **Test App Wrappers**: Always use `wrapInTestApp` or `renderInTestApp` from `@backstage/test-utils` when testing React components that need Backstage app context (routing, theme, APIs). Use `createTestAppWrapper` for custom wrapper creation. + +3. **Test API Provider**: Always use `TestApiProvider` or `TestApiRegistry` from `@backstage/test-utils` when testing components that depend on Backstage APIs. This allows you to provide only the APIs needed for your test. + +4. **Async Component Rendering**: Use `renderWithEffects` from `@backstage/test-utils` when testing components that perform asynchronous operations (e.g., useEffect with fetch). This properly handles async rendering with React's act. + +5. **Log Collection**: Use `withLogCollector` from `@backstage/test-utils` when you need to capture and assert on console logs, warnings, or errors during tests. + +6. **MSW Integration**: Use `registerMswTestHooks` from `@backstage/test-utils` when setting up Mock Service Worker for HTTP request mocking in frontend tests. + +7. **Text Content Matcher**: Use `textContentMatcher` from `@backstage/test-utils` as a custom matcher for testing-library queries when matching text content. + +8. **Mock Breakpoint**: Use `mockBreakpoint` from `@backstage/test-utils` when testing components that use Material-UI's `useMediaQuery` hook (though this is deprecated in favor of core-components/testUtils). + +9. **Individual API Mocks**: Use individual mock APIs when you need more control: + - `MockAnalyticsApi` - Analytics API mock + - `MockConfigApi` - Config API mock + - `MockErrorApi` - Error API mock + - `MockFetchApi` - Fetch API mock + - `MockPermissionApi` - Permission API mock + - `MockStorageApi` - Storage API mock + - `MockTranslationApi` - Translation API mock (from alpha) + +10. **Alpha APIs**: For alpha/experimental APIs, use the utilities from `@backstage/test-utils/alpha` (e.g., `MockTranslationApi`). + +## Import Patterns + +Always import from the main package: + +```typescript +import { + mockApis, + wrapInTestApp, + renderInTestApp, + createTestAppWrapper, + TestApiProvider, + TestApiRegistry, + renderWithEffects, + withLogCollector, + registerMswTestHooks, + textContentMatcher, + mockBreakpoint, + MockAnalyticsApi, + MockConfigApi, + MockErrorApi, + MockFetchApi, + MockPermissionApi, + MockStorageApi +} from '@backstage/test-utils'; +``` + +For alpha APIs: + +```typescript +import { + MockTranslationApi +} from '@backstage/test-utils/alpha'; +``` + +## When to Use + +- **Always** use these utilities when writing frontend/plugin tests +- **Always** use `renderInTestApp` or `wrapInTestApp` for components that need Backstage context +- **Always** use `TestApiProvider` or `TestApiRegistry` when components depend on Backstage APIs +- **Always** use `mockApis` instead of creating custom API implementations +- **Never** manually set up React Router, theme providers, or API contexts when test wrappers are available +- **Never** create custom API mocks when mockApis provides equivalents +- **Never** use raw `render` from testing-library when `renderWithEffects` is needed for async components + +## Benefits + +Using test-utils ensures: +- Consistent test setup across frontend codebase +- Proper Backstage app context (routing, theme, APIs) +- Simplified API mocking with realistic implementations +- Proper async rendering handling +- Well-tested and maintained utilities +- Reduced boilerplate in test code +- Better integration with Backstage's plugin architecture diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8491b1a260..bb11c2f9fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,7 +10,8 @@ yarn.lock @backstage/maintainers @backst /.changeset @backstage/operations-maintainers /.changeset/*.md /.github @backstage/operations-maintainers -/.github/vale @backstage/documentation-maintainers +/.github/vale @backstage/maintainers @backstage/documentation-maintainers +/.patches @backstage/operations-maintainers /beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers /docs @backstage/maintainers @backstage/documentation-maintainers /docs/assets/search @backstage/search-maintainers @@ -24,7 +25,7 @@ yarn.lock @backstage/maintainers @backst /docs/plugins/integrating-search-into-plugins.md @backstage/search-maintainers /docs/releases @backstage/operations-maintainers /docs/tooling @backstage/tooling-maintainers -/microsite @backstage/documentation-maintainers +/microsite @backstage/maintainers @backstage/documentation-maintainers /microsite/data/plugins @backstage/maintainers /microsite/static @backstage/maintainers @backstage/documentation-maintainers /packages @backstage/framework-maintainers diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 98073c5128..ee3fe6fda0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,11 +1,22 @@ -This is a TypeScript project structured as a monorepo using Yarn workspaces. +Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces. + +## Key Directories + +- `/packages`: Core framework packages (prefixed `@backstage/`) +- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`) +- `/packages/app` and `/packages/backend`: Example app for local development +- `/packages/app-next`: Example app using the new frontend system (NFS) +- `/docs`: Documentation files + +Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system. ## Code Standards The following files contain guidelines for the project: +- `/CONTRIBUTING.md`: comprehensive contribution guidelines. - `/STYLE.md`: guidelines for code style. -- `/REVIEWING.md`: guidelines for pull requests. +- `/REVIEWING.md`: guidelines for pull requests and writing changesets. - `/SECURITY.md`: guidelines for security. - `/docs/architecture-decisions/`: contains the architecture decisions for the project. @@ -19,10 +30,12 @@ Before any of these commands can be run, you need to run `yarn install` in the p - Code formatting: Use `yarn prettier --write ` to format code. - Lint: Use `yarn lint --fix` in the project root to run the linter. - API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages. +- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007). +- Create: Use `yarn new` to scaffold new plugins, packages, or modules. You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows. -All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. See the guidelines in `/REVIEWING.md` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages that below version `1.0.0`, or a `major` version bump for packages that are at version `1.0.0` or higher. +All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 593c7605f5..62277b26d6 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -59,6 +59,7 @@ Chanwit CI/CD classname cli +CLIs cloudbuild Cloudflare Cloudformation @@ -98,6 +99,7 @@ dataflow dataloader dayjs debounce +debounced debounces debuggability declaratively @@ -110,6 +112,7 @@ dependabot deps dequeue dequeueing +dereferencing deserialization destructured destructuring @@ -148,6 +151,7 @@ etag Expedia facto failover +faq Fargate faqs featureful @@ -229,6 +233,7 @@ JWTs Kaewkasi Kaswell keepalive +Keycloak Keyv Knex knip @@ -264,6 +269,7 @@ maintainer's maintainership makefile Matomo +matchers md memcache memoization @@ -323,6 +329,7 @@ onboarded onboarding Onboarding onelogin +openapi OpenSearch OpenShift openssl @@ -504,6 +511,7 @@ thumbsup todo todos togglable +tokenizer tolerations Tolerations toolchain @@ -516,6 +524,7 @@ transpile transpiled transpiler transpilers +trixie truthy tsconfig TSDoc @@ -569,3 +578,5 @@ Zolotusky zoomable zsh resizable +enums +LLMs diff --git a/.github/workflows/api-breaking-changes-comment.yml b/.github/workflows/api-breaking-changes-comment.yml index f419a12599..079c946173 100644 --- a/.github/workflows/api-breaking-changes-comment.yml +++ b/.github/workflows/api-breaking-changes-comment.yml @@ -23,7 +23,7 @@ jobs: comment-cache-key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: disable-sudo: true egress-policy: block @@ -32,7 +32,7 @@ jobs: - name: 'Download artifacts' # Fetch output (zip archive) from the workflow run that triggered this workflow. - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -74,7 +74,7 @@ jobs: - name: Cache Comment if: ${{ steps.event.outputs.ACTION != 'closed' }} - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: comment.md key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }} @@ -99,11 +99,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Fetch cached Manifests File id: cache - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: comment.md key: ${{ needs.setup.outputs.comment-cache-key }} @@ -115,13 +115,13 @@ jobs: with: issue-number: ${{ needs.setup.outputs.pr-number }} comment-author: 'github-actions[bot]' - body-includes: API changes + body-includes: api-breaking-changes-comment direction: last - name: Create or Update Comment with API Changes uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} + issue-number: ${{ needs.setup.outputs.pr-number }} body-path: comment.md edit-mode: replace diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index f72d9c29bf..7525ea3a07 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -14,11 +14,11 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history @@ -27,19 +27,20 @@ jobs: run: git fetch --depth 1 origin ${{ github.base_ref }} - name: setup-node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: linux-v20 + cache-prefix: linux-v22 - name: breaking changes check run: | yarn backstage-repo-tools repo schema openapi diff --since origin/${{ github.base_ref }} > comment.md + echo "" >> comment.md - name: clone artifacts to current directory run: | diff --git a/.github/workflows/automate_area-labels.yml b/.github/workflows/automate_area-labels.yml index f21ef62d8d..e85a9cf11f 100644 --- a/.github/workflows/automate_area-labels.yml +++ b/.github/workflows/automate_area-labels.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' sync-labels: true diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index c41f4dd8f3..257db3929a 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -23,11 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml index 59da292109..83a1e32fcf 100644 --- a/.github/workflows/automate_merge_message.yml +++ b/.github/workflows/automate_merge_message.yml @@ -24,11 +24,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: '${{ github.event.pull_request.merge_commit_sha }}' @@ -44,7 +44,7 @@ jobs: node generate.js ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} > message.txt - name: Post Message - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: ISSUE_NUMBER: ${{ github.event.pull_request.number }} with: diff --git a/.github/workflows/automate_stale.yml b/.github/workflows/automate_stale.yml index 5b92c6211b..a7eef4fae4 100644 --- a/.github/workflows/automate_stale.yml +++ b/.github/workflows/automate_stale.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Stale check - base - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: stale-issue-message: > This issue has been automatically marked as stale because it has not had @@ -42,7 +42,7 @@ jobs: operations-per-run: 100 - name: Stale check - bugs without repro - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: stale-issue-message: > This bug report has been automatically marked as stale because it has not had diff --git a/.github/workflows/ci-noop.yml b/.github/workflows/ci-noop.yml index 35901d37b2..12aafe18ee 100644 --- a/.github/workflows/ci-noop.yml +++ b/.github/workflows/ci-noop.yml @@ -19,12 +19,12 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] name: Verify ${{ matrix.node-version }} steps: # - name: Harden Runner - # uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + # uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.5.1 # with: # egress-policy: audit @@ -35,12 +35,12 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] name: Test ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24f08544aa..55e01009c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true @@ -28,14 +28,14 @@ jobs: name: Install ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -55,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true @@ -64,14 +64,14 @@ jobs: name: Verify ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -154,7 +154,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] name: Test ${{ matrix.node-version }} services: @@ -203,14 +203,14 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }} INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }} INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }} INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: fetch master branch run: git fetch origin master @@ -220,7 +220,7 @@ jobs: run: git fetch origin ${{ github.event.pull_request.base.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -236,7 +236,7 @@ jobs: # Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds - name: restore backstage-cli cache - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/backstage-cli key: ${{ runner.os }}-v${{ matrix.node-version }}-backstage-cli-${{ github.run_id }} @@ -260,7 +260,7 @@ jobs: # Always save success cache even if there were failures, that way it can be used in re-triggered builds - name: save backstage-cli cache - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: always() with: path: .cache/backstage-cli diff --git a/.github/workflows/cleanup_patch-files.yml b/.github/workflows/cleanup_patch-files.yml new file mode 100644 index 0000000000..3dc0e27837 --- /dev/null +++ b/.github/workflows/cleanup_patch-files.yml @@ -0,0 +1,160 @@ +name: Cleanup Patch Files +on: + push: + branches: + - 'patch/v*' + +concurrency: + group: cleanup-patch-files + cancel-in-progress: false + +jobs: + cleanup: + name: Cleanup Patch Files + runs-on: ubuntu-latest + if: github.repository == 'backstage/backstage' + permissions: + contents: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'Github patch cleanup workflow' + + - name: Extract PR numbers from commit messages + id: extract-pr-numbers + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const { execSync } = require('child_process'); + + // Extract PR numbers from commits pushed to the patch branch + // Commits have messages in format: "Patch from PR #123" + const beforeSha = context.payload.before; + const afterSha = context.payload.after; + + let commitRange; + if (beforeSha && beforeSha !== '0000000000000000000000000000000000000000') { + commitRange = `${beforeSha}..${afterSha}`; + } else { + // First push to branch, check recent commits + commitRange = `-n 20 ${afterSha}`; + } + + // Get commit messages from git log + let commitMessages; + try { + commitMessages = execSync( + `git log --format=%B ${commitRange}`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] } + ); + } catch (error) { + console.log('No commits found in range'); + commitMessages = ''; + } + + // Extract PR numbers from commit messages matching "Patch from PR #123" + const prNumbers = new Set(); + const prPattern = /Patch from PR #(\d+)/g; + let match; + + while ((match = prPattern.exec(commitMessages)) !== null) { + prNumbers.add(parseInt(match[1], 10)); + } + + // Convert to sorted array + const prNumbersArray = Array.from(prNumbers).sort((a, b) => a - b); + + if (prNumbersArray.length > 0) { + console.log(`Found PR numbers: ${prNumbersArray.join(', ')}`); + core.setOutput('pr_numbers', JSON.stringify(prNumbersArray)); + core.setOutput('has_pr_numbers', 'true'); + } else { + console.log('No PR numbers found in commit messages'); + core.setOutput('pr_numbers', '[]'); + core.setOutput('has_pr_numbers', 'false'); + } + + - name: Checkout master + if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true' + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: master + token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: Delete patch files + if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBERS: ${{ steps.extract-pr-numbers.outputs.pr_numbers }} + REF_NAME: ${{ github.ref_name }} + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const { execSync } = require('child_process'); + const fs = require('fs'); + const path = require('path'); + + const prNumbers = JSON.parse(process.env.PR_NUMBERS); + const patchesDir = path.join(process.cwd(), '.patches'); + + if (!fs.existsSync(patchesDir)) { + console.log('No .patches directory found, nothing to clean up'); + return; + } + + const deletedFiles = []; + + // Delete patch files for each PR number + for (const prNum of prNumbers) { + const patchFile = path.join(patchesDir, `pr-${prNum}.txt`); + if (fs.existsSync(patchFile)) { + fs.unlinkSync(patchFile); + deletedFiles.push(patchFile); + console.log(`Deleted ${patchFile}`); + } else { + console.log(`Patch file ${patchFile} not found, skipping`); + } + } + + if (deletedFiles.length === 0) { + console.log('No patch files to delete'); + return; + } + + - name: Commit and push changes + if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true' + run: | + # Check if there are any changes to commit + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + + git add .patches/ + git commit -m "chore: remove applied patch files after patch release merge + + Removed patch files for PRs included in patch release to ${{ github.ref_name }}" + + git push origin master + + - name: Summary + if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true' + run: | + echo "## Patch Files Cleanup Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Patch release to ${{ github.ref_name }} has been merged." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Patch files have been removed from the master branch." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 285c2e6c41..b695c88c90 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -10,7 +10,7 @@ jobs: timeout-minutes: 10 steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index e0f4ec29ed..f7bb55c1b9 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -16,22 +16,22 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x] + node-version: [22.x] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: path: backstage ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -52,7 +52,7 @@ jobs: working-directory: ./example-app - name: Login to GitHub Container Registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index ff0b46f0df..52684d5529 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -23,12 +23,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: find latest release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: find-release with: script: | @@ -54,20 +54,20 @@ jobs: result-encoding: string - name: checkout latest release - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: refs/tags/${{ steps.find-release.outputs.result }} - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: build API reference run: yarn build:api-docs @@ -82,7 +82,7 @@ jobs: # Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds - name: restore package-docs cache - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/package-docs key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }} @@ -102,7 +102,7 @@ jobs: # Always save success cache even if there were failures, that way it can be used in re-triggered builds - name: save package-docs cache - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: always() with: path: .cache/package-docs @@ -135,23 +135,23 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: checkout master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: build API reference run: yarn build:api-docs @@ -166,7 +166,7 @@ jobs: # Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds - name: restore package-docs cache - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/package-docs key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-${{ github.run_id }} @@ -186,7 +186,7 @@ jobs: # Always save success cache even if there were failures, that way it can be used in re-triggered builds - name: save package-docs cache - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: always() with: path: .cache/package-docs @@ -240,19 +240,19 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth # Stable docs - name: checkout latest release - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: refs/tags/${{ needs.stable.outputs.release }} @@ -286,7 +286,7 @@ jobs: # Next docs - name: checkout master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: clean: false @@ -329,7 +329,7 @@ jobs: run: ls microsite/build && ls microsite/build/storybook && ls microsite/build/api/stable && ls microsite/build/api/next - name: Deploy both microsite and storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 + uses: JamesIves/github-pages-deploy-action@9d877eea73427180ae43cf98e8914934fe157a1a # v4.7.6 with: branch: gh-pages folder: microsite/build diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index e3e109dfe7..5135e0a1e2 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] services: postgres18: @@ -68,10 +68,10 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -94,7 +94,7 @@ jobs: run: yarn backstage-cli config:check --lax - name: backstage-cli cache - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/backstage-cli key: ${{ runner.os }}-v${{ matrix.node-version }}-backstage-cli-${{ github.run_id }} @@ -147,11 +147,11 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index 17c76c19c2..80a842833a 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -16,13 +16,13 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit # We need to checkout the `.github/ISSUE_TEMPLATE` for the advanced labeler action to be able to read the templates # While at it we might as well checkout all of `.github` so that the labeling actions don't need to fetch their configs - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: sparse-checkout: .github @@ -38,7 +38,7 @@ jobs: # These two steps add labels based on user input in the issue form - name: Parse issue form - uses: stefanbuck/github-issue-parser@2ea9b35a8c584529ed00891a8f7e41dc46d0441e # v3 + uses: stefanbuck/github-issue-parser@10dcc54158ba4c137713d9d69d70a2da63b6bda3 # v3 id: issue-parser with: template-path: .github/ISSUE_TEMPLATE/.common.yaml diff --git a/.github/workflows/mui-migration-tracker.yml b/.github/workflows/mui-migration-tracker.yml index 4754ec51b0..ffde14b5f0 100644 --- a/.github/workflows/mui-migration-tracker.yml +++ b/.github/workflows/mui-migration-tracker.yml @@ -18,23 +18,23 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: Run migration analysis id: analysis @@ -48,7 +48,7 @@ jobs: echo "EOF" >> $GITHUB_ENV - name: Update GitHub Issue - uses: actions/github-script@v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -62,7 +62,7 @@ jobs: issue_number: issueNumber, body: reportBody }); - + console.log(`✅ Successfully updated issue #${issueNumber}`); } catch (error) { console.error(`❌ Error updating issue: ${error.message}`); diff --git a/.github/workflows/pr-review-comment-trigger.yaml b/.github/workflows/pr-review-comment-trigger.yaml index d5970deae8..95c9c921f5 100644 --- a/.github/workflows/pr-review-comment-trigger.yaml +++ b/.github/workflows/pr-review-comment-trigger.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 9db0792eaa..bb7bd26c53 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -17,13 +17,13 @@ jobs: steps: # Inspired by https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Read PR Number id: pr-number - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 33dd3d00ce..36988f7d07 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 232d055072..077eb2d856 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,17 +29,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: 'Checkout code' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: 'Run analysis' - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6 + uses: github/codeql-action/upload-sarif@f47c8e6a9bd05ef3ee422fc8d8663be7fe4bdc61 # v3.31.8 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_canon.yml b/.github/workflows/sync_canon.yml index 3b85c1c49d..ea6d47bbf5 100644 --- a/.github/workflows/sync_canon.yml +++ b/.github/workflows/sync_canon.yml @@ -8,26 +8,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: Checkout backstage/docs-ui - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: backstage/docs-ui path: bui-external-docs diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 33cab94280..7311d52ae4 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Fetch changes to previous commit - required for 'only_changed' in Prettier action fetch-depth: 0 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index 181d48eb38..9e894631cc 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -11,12 +11,12 @@ jobs: if: github.actor == 'dependabot[bot]' && github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 2 ref: ${{ github.head_ref }} @@ -26,7 +26,7 @@ jobs: git config --global user.email noreply@backstage.io git config --global user.name 'Github changeset workflow' - name: Generate changeset - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { promises: fs } = require('fs'); diff --git a/.github/workflows/sync_patch-release.yml b/.github/workflows/sync_patch-release.yml new file mode 100644 index 0000000000..4e3a622007 --- /dev/null +++ b/.github/workflows/sync_patch-release.yml @@ -0,0 +1,258 @@ +name: Sync Patch Release +on: + push: + branches: + - master + paths: + - '.patches/**' + pull_request: + branches: + - master + paths: + - '.patches/**' + +concurrency: + group: sync-patch-release + cancel-in-progress: true + +jobs: + sync-patch-release: + name: Sync Patch Release + runs-on: ubuntu-latest + if: github.repository == 'backstage/backstage' + permissions: + contents: write + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 20000 + fetch-tags: true + token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 22.x + registry-url: https://registry.npmjs.org/ # Needed for auth + + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'Github patch release workflow' + + - name: Check for patch files + id: check-patches + run: | + if [ -d ".patches" ] && [ "$(ls -A .patches/pr-*.txt 2>/dev/null)" ]; then + echo "has_patches=true" >> $GITHUB_OUTPUT + echo "Found patch files" + else + echo "has_patches=false" >> $GITHUB_OUTPUT + echo "No patch files found" + fi + + - name: Find existing PR + id: find-pr + if: steps.check-patches.outputs.has_patches == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const branchName = 'patch-release'; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Find PR with the branch as head + const { data: pulls } = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branchName}`, + state: 'open', + }); + + if (pulls.length > 0) { + console.log(`Found existing PR: #${pulls[0].number}`); + core.setOutput('pr_number', pulls[0].number.toString()); + core.setOutput('pr_exists', 'true'); + } else { + console.log('No existing PR found'); + core.setOutput('pr_exists', 'false'); + } + + - name: Close PR and delete branch if no patches + if: steps.check-patches.outputs.has_patches == 'false' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const branchName = 'patch-release'; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Find PR with the branch as head + const { data: pulls } = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branchName}`, + state: 'open', + }); + + if (pulls.length > 0) { + console.log(`Closing PR #${pulls[0].number}`); + await github.rest.pulls.update({ + owner, + repo, + pull_number: pulls[0].number, + state: 'closed', + }); + } + + // Delete the branch + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branchName}`, + }); + console.log(`Deleted branch ${branchName}`); + } catch (error) { + if (error.status === 422) { + console.log(`Branch ${branchName} does not exist, skipping deletion`); + } else { + throw error; + } + } + + - name: Install Dependencies + if: steps.check-patches.outputs.has_patches == 'true' + run: yarn --immutable + + - name: Read patch files for PR metadata + if: steps.check-patches.outputs.has_patches == 'true' + id: read-patches + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + // Read patch files from master branch to generate PR body + const fs = require('fs').promises; + const path = require('path'); + + const patchesDir = path.join(process.cwd(), '.patches'); + let files = []; + try { + files = await fs.readdir(patchesDir); + } catch (error) { + console.log(`Error reading patches directory: ${error.message}`); + core.setOutput('pr_body', 'This patch release fixes issues.'); + return; + } + + const patchFiles = files.filter(f => /^pr-\d+\.txt$/.test(f)); + + const patches = []; + for (const file of patchFiles) { + const match = file.match(/^pr-(\d+)\.txt$/); + if (match) { + const prNumber = parseInt(match[1], 10); + const filePath = path.join(patchesDir, file); + let content = ''; + try { + content = await fs.readFile(filePath, 'utf8'); + } catch (error) { + console.log(`Error reading ${file}: ${error.message}`); + } + const description = content.trim() || ''; + patches.push({ prNumber, description }); + } + } + + if (patches.length === 0) { + console.log('No patches found, using default values'); + core.setOutput('pr_body', 'This patch release fixes issues.'); + core.setOutput('pr_numbers', '[]'); + return; + } + + patches.sort((a, b) => a.prNumber - b.prNumber); + const prNumbers = patches.map(p => p.prNumber); + const descriptions = patches.map(p => p.description); + + const descriptionList = descriptions.map(desc => `- ${desc}`).join('\n'); + const body = `This patch release fixes the following issues:\n\n${descriptionList}`; + + core.setOutput('pr_body', body); + core.setOutput('pr_numbers', JSON.stringify(prNumbers)); + + - name: Run patch release script + if: steps.check-patches.outputs.has_patches == 'true' + id: run-script + env: + GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + HUSKY: '0' + PATCH_RELEASE_BRANCH: patch-release + run: | + # The script will handle finding the release version, creating the patch branch, + # and deleting patch files. Run the patch script with --from-dir flag to read from .patches/ directory + # The script will write the patch branch to GITHUB_OUTPUT + node scripts/patch-release-for-pr.js --from-dir + + # Ensure the branch is pushed (script pushes it, but force push in case of conflicts) + git push origin patch-release --force + + - name: Create or update PR + if: steps.check-patches.outputs.has_patches == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_EXISTS: ${{ steps.find-pr.outputs.pr_exists }} + PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }} + PR_BODY: ${{ steps.read-patches.outputs.pr_body }} + PATCH_BRANCH: ${{ steps.run-script.outputs.patch_branch }} + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const branchName = 'patch-release'; + const owner = context.repo.owner; + const repo = context.repo.repo; + + const title = 'Patch Release'; + const body = process.env.PR_BODY; + const prExists = process.env.PR_EXISTS === 'true'; + const prNumber = process.env.PR_NUMBER; + const patchBranch = process.env.PATCH_BRANCH; + + if (prExists && prNumber) { + console.log(`Updating PR #${prNumber}`); + await github.rest.pulls.update({ + owner, + repo, + pull_number: parseInt(prNumber, 10), + title, + body, + }); + } else { + // Use the patch branch from the script + if (!patchBranch) { + throw new Error('Patch branch not provided from script. Make sure the script outputs the patch branch name.'); + } + + const baseBranch = patchBranch; + console.log(`Creating new PR with base branch: ${baseBranch}`); + + const { data: pr } = await github.rest.pulls.create({ + owner, + repo, + title, + body, + head: branchName, + base: baseBranch, + }); + console.log(`Created PR #${pr.number}`); + } diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index a9c4b3722b..871b8b9712 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -8,26 +8,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # 'v' prefix is added here for the tag, we keep it out of the manifest logic ref: v${{ github.event.client_payload.version }} - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: Build yarn plugin working-directory: packages/yarn-plugin @@ -35,7 +35,7 @@ jobs: # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: backstage/versions path: versions @@ -71,7 +71,7 @@ jobs: git push - name: Dispatch update-helper update - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 2331e2c30f..07f2e24697 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -11,12 +11,12 @@ jobs: if: github.actor == 'renovate[bot]' && github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 2 ref: ${{ github.head_ref }} @@ -26,7 +26,7 @@ jobs: git config --global user.email noreply@backstage.io git config --global user.name 'Github changeset workflow' - name: Generate changeset - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const { promises: fs } = require("fs"); diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 3716c1f48b..0d1233020c 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -12,24 +12,24 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: Create Snyk report - uses: snyk/actions/node@77490d94e966421e076e95ad8fa87aa55e5ca409 # master + uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found with: args: > diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 205d146bb8..632d03c35d 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -25,13 +25,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Monitor and Synchronize Snyk Policies - uses: snyk/actions/node@77490d94e966421e076e95ad8fa87aa55e5ca409 # master + uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master with: command: monitor args: > @@ -46,7 +46,7 @@ jobs: # Above we run the `monitor` command, this runs the `test` command which is # the one that generates the SARIF report that we can upload to GitHub. - name: Create Snyk report - uses: snyk/actions/node@77490d94e966421e076e95ad8fa87aa55e5ca409 # master + uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master continue-on-error: true # To make sure that SARIF upload gets called with: args: > @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6 + uses: github/codeql-action/upload-sarif@f47c8e6a9bd05ef3ee422fc8d8663be7fe4bdc61 # v3.31.8 with: sarif_file: snyk.sarif diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 66a798bd58..3ba9833645 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -12,20 +12,28 @@ jobs: create-release-pr: name: Create Changeset PR runs-on: ubuntu-latest + steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 20000 fetch-tags: true token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 22.x + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: Install Dependencies run: yarn --immutable + - name: Create Release Pull Request uses: backstage/changesets-action@a39baf18913e669734ffb00c2fd9900472cfa240 # v2.3.2 with: diff --git a/.github/workflows/verify_accessibility-noop.yml b/.github/workflows/verify_accessibility-noop.yml index df6f004171..ee696ebc7f 100644 --- a/.github/workflows/verify_accessibility-noop.yml +++ b/.github/workflows/verify_accessibility-noop.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index 48e243b002..8f1b611f1f 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -20,19 +20,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: run Lighthouse CI run: | yarn dlx @lhci/cli@0.11.x autorun diff --git a/.github/workflows/verify_chromatic-noop.yml b/.github/workflows/verify_chromatic-noop.yml index 4de52a3258..35015def52 100644 --- a/.github/workflows/verify_chromatic-noop.yml +++ b/.github/workflows/verify_chromatic-noop.yml @@ -20,7 +20,7 @@ jobs: name: Chromatic steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml index e6649c3e55..f08441bcc1 100644 --- a/.github/workflows/verify_chromatic.yml +++ b/.github/workflows/verify_chromatic.yml @@ -19,22 +19,22 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [20.x] + node-version: [22.x] name: Chromatic steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 10000 # Required to retrieve git history - name: Use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 254938ecc8..5d976f8583 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -42,12 +42,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6 + uses: github/codeql-action/init@f47c8e6a9bd05ef3ee422fc8d8663be7fe4bdc61 # v3.31.8 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6 + uses: github/codeql-action/autobuild@f47c8e6a9bd05ef3ee422fc8d8663be7fe4bdc61 # v3.31.8 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6 + uses: github/codeql-action/analyze@f47c8e6a9bd05ef3ee422fc8d8663be7fe4bdc61 # v3.31.8 diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 6c855eeea4..8810f05a16 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -12,11 +12,11 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Vale does not support file excludes, so we use the script to generate a list of files instead # The action also does not allow args or a local config file to be passed in, so the files array diff --git a/.github/workflows/verify_e2e-linux-noop.yml b/.github/workflows/verify_e2e-linux-noop.yml index aa3358f2f7..152f4cc2c0 100644 --- a/.github/workflows/verify_e2e-linux-noop.yml +++ b/.github/workflows/verify_e2e-linux-noop.yml @@ -24,12 +24,12 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] name: E2E Linux ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index dc81d42e8a..55d92b30b3 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true @@ -43,11 +43,11 @@ jobs: name: E2E Linux ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Configure Git run: | @@ -55,7 +55,7 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index e933efe3f8..fce6691e7f 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true @@ -32,17 +32,17 @@ jobs: name: Techdocs steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.9' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} - name: yarn install diff --git a/.github/workflows/verify_e2e-windows-noop.yml b/.github/workflows/verify_e2e-windows-noop.yml index b1a4344bfc..f04748a950 100644 --- a/.github/workflows/verify_e2e-windows-noop.yml +++ b/.github/workflows/verify_e2e-windows-noop.yml @@ -20,12 +20,12 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] name: E2E Windows ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index eefcf22b35..ce037f8c01 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -24,16 +24,16 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules name: E2E Windows ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit @@ -44,7 +44,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Configure Git run: | @@ -52,13 +52,13 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: setup python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.10' diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml index 9c811fbe9f..681ac2c07a 100644 --- a/.github/workflows/verify_fossa.yml +++ b/.github/workflows/verify_fossa.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Install Fossa run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash" diff --git a/.github/workflows/verify_microsite-noop.yml b/.github/workflows/verify_microsite-noop.yml index 3466826cae..70686e3bd9 100644 --- a/.github/workflows/verify_microsite-noop.yml +++ b/.github/workflows/verify_microsite-noop.yml @@ -21,7 +21,7 @@ jobs: name: Microsite steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index f3d48ef480..b11ef3172c 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -28,12 +28,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: find latest release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: find-release with: script: | @@ -59,20 +59,20 @@ jobs: result-encoding: string - name: checkout latest release - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: refs/tags/${{ steps.find-release.outputs.result }} - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: build API reference run: yarn build:api-docs @@ -87,7 +87,7 @@ jobs: # Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds - name: restore package-docs cache - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/package-docs key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }} @@ -105,7 +105,7 @@ jobs: # Always save success cache even if there were failures, that way it can be used in re-triggered builds - name: save package-docs cache - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: always() with: path: .cache/package-docs @@ -137,23 +137,23 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - name: checkout master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 with: - cache-prefix: ${{ runner.os }}-v20.x + cache-prefix: ${{ runner.os }}-v22.x - name: build API reference run: yarn build:api-docs @@ -168,7 +168,7 @@ jobs: # Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds - name: restore package-docs cache - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/package-docs key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-${{ github.run_id }} @@ -186,7 +186,7 @@ jobs: # Always save success cache even if there were failures, that way it can be used in re-triggered builds - name: save package-docs cache - uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 if: always() with: path: .cache/package-docs @@ -234,17 +234,17 @@ jobs: name: Microsite steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + node-version: 22.x + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 with: python-version: '3.9' @@ -295,7 +295,7 @@ jobs: run: yarn build:api-docs - name: checkout master - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: clean: false diff --git a/.github/workflows/verify_microsite_accessibility-noop.yml b/.github/workflows/verify_microsite_accessibility-noop.yml index 5e0bc35cc0..bb13a85dfa 100644 --- a/.github/workflows/verify_microsite_accessibility-noop.yml +++ b/.github/workflows/verify_microsite_accessibility-noop.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index 40e1866b5f..0bfab3035b 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -15,16 +15,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Use Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - name: Use Node.js 22.x + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: - node-version: 20.x + node-version: 22.x - name: top-level install run: yarn install --immutable diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 363bc075b8..c15388d6b4 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -17,11 +17,11 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] env: CI: true - NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules + NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }} INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }} INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }} @@ -29,14 +29,14 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 0000000000..9061b70d79 --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,42 @@ +name: Add a welcome comment + +on: + pull_request_target: + types: [opened] + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + welcome: + runs-on: ubuntu-latest + if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + steps: + - name: Add a welcome comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request + const login = pr.user.login + + const message = ` + Hi @${login}, thanks for opening your first pull request in Backstage! 👋 + + A couple of useful links to help you get started: + + * [Contributing Guide](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) + * [DCO Sign-off Instructions](https://github.com/backstage/backstage/blob/master/DCO) + * [Style Guide](https://github.com/backstage/backstage/blob/master/STYLE.md) + + We really appreciate your contribution and look forward to reviewing your work. Welcome aboard! + ` + + await github.rest.issues.createComment({ + issue_number: pr.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }) diff --git a/.patches/README.md b/.patches/README.md new file mode 100644 index 0000000000..19b599d6bb --- /dev/null +++ b/.patches/README.md @@ -0,0 +1,16 @@ +# Patch Release Process + +This directory tracks patches to be applied to the next patch release. It contains a list of patch files of the format `pr-.txt` that define which pull requests should be included in the next patch release, where the content of the file is the description of the fix. + +The [sync_patch-release.yml](/.github/workflows/sync_patch-release.yml) workflow will automatically create a "Patch Release" PR with the patches in this directory. + +## Usage + +To add a PR to the set of patches, run `yarn patch-pr ` in the root of the repository. + +## GitHub Workflow + +A GitHub workflow automatically keeps a "Patch Release" PR in sync with the patches listed in this directory. When you add, modify, or remove patch files, the workflow will: + +- Update the existing PR if patch files exist +- Close and delete the PR branch if no patch files exist diff --git a/.patches/create-pr-patch.js b/.patches/create-pr-patch.js new file mode 100755 index 0000000000..528d4fc404 --- /dev/null +++ b/.patches/create-pr-patch.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const prNumber = process.argv[2]; +const description = process.argv.slice(3).join(' '); + +if (!prNumber || !description) { + console.error('Usage: yarn patch-pr '); + process.exit(1); +} + +const patchFilePath = path.join(__dirname, `pr-${prNumber}.txt`); +fs.writeFileSync(patchFilePath, description); diff --git a/.patches/pr-32409.txt b/.patches/pr-32409.txt new file mode 100644 index 0000000000..a14d9feb37 --- /dev/null +++ b/.patches/pr-32409.txt @@ -0,0 +1 @@ +Fixes a deprecation warning for Router extension in tests \ No newline at end of file diff --git a/.patches/pr-32428.txt b/.patches/pr-32428.txt new file mode 100644 index 0000000000..77136cf4c8 --- /dev/null +++ b/.patches/pr-32428.txt @@ -0,0 +1 @@ +fix(ui): restore React 17 compatibility for useId \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index 788df83ba9..6a13b1b0a7 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,14 @@ -import type { StorybookConfig } from '@storybook/react-vite'; +// This file has been automatically migrated to valid ESM format by Storybook. +import { defineMain } from '@storybook/react-vite/node'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; import { join, dirname, posix } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const require = createRequire(import.meta.url); + const isChromatic = process.env.STORYBOOK_STORY_SET === 'chromatic'; // All stories for full development @@ -35,18 +42,24 @@ function getAbsolutePath(value: string): any { return dirname(require.resolve(join(value, 'package.json'))); } -const config: StorybookConfig = { +export default defineMain({ stories, addons: [ getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@storybook/addon-themes'), getAbsolutePath('@storybook/addon-docs'), getAbsolutePath('@storybook/addon-a11y'), + getAbsolutePath('@storybook/addon-vitest'), + getAbsolutePath('@storybook/addon-mcp'), ], framework: { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, + features: { + experimentalComponentsManifest: true, + experimentalCodeExamples: true, // optional + }, viteFinal: async (config, { configType }) => { // Add Node.js polyfills for browser compatibility // @@ -122,6 +135,4 @@ const config: StorybookConfig = { return config; }, -}; - -export default config; +}); diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 8f987ebab1..ccd7adc57f 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,8 +1,12 @@ -import React, { useEffect } from 'react'; +import addonA11y from '@storybook/addon-a11y'; +import addonDocs from '@storybook/addon-docs'; +import addonThemes from '@storybook/addon-themes'; +import addonLinks from '@storybook/addon-links'; +import { definePreview } from '@storybook/react-vite'; +import { useEffect } from 'react'; import { TestApiProvider } from '@backstage/test-utils'; import { Content, AlertDisplay } from '@backstage/core-components'; import { apis } from './support/apis'; -import type { Decorator, Preview } from '@storybook/react-vite'; import { useGlobals } from 'storybook/preview-api'; import { UnifiedThemeProvider, themes } from '@backstage/theme'; import { allModes } from './modes'; @@ -16,7 +20,8 @@ import './storybook.css'; // Custom themes import './themes/spotify.css'; -const preview: Preview = { +export default definePreview({ + tags: ['manifest'], globalTypes: { themeMode: { name: 'Theme Mode', @@ -47,10 +52,12 @@ const preview: Preview = { }, }, }, + initialGlobals: { themeMode: 'light', themeName: 'backstage', }, + parameters: { layout: 'fullscreen', @@ -72,7 +79,7 @@ const preview: Preview = { }, viewport: { - viewports: { + options: { initial: { name: 'Initial', styles: { width: '320px', height: '100%' }, @@ -101,7 +108,15 @@ const preview: Preview = { // 'dark spotify': allModes['dark spotify'], }, }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo', + }, }, + decorators: [ Story => { const [globals] = useGlobals(); @@ -121,10 +136,11 @@ const preview: Preview = { }; }, [selectedTheme, selectedThemeName]); - document.body.style.backgroundColor = 'var(--bui-bg)'; + document.body.style.backgroundColor = 'var(--bui-bg-surface-0)'; const docsStoryElements = document.getElementsByClassName('docs-story'); Array.from(docsStoryElements).forEach(element => { - (element as HTMLElement).style.backgroundColor = 'var(--bui-bg)'; + (element as HTMLElement).style.backgroundColor = + 'var(--bui-bg-surface-0)'; }); return ( @@ -140,6 +156,6 @@ const preview: Preview = { ); }, ], -}; -export default preview; + addons: [addonLinks(), addonThemes(), addonDocs(), addonA11y()], +}); diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index bcb1ded9ff..352c633a15 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -216,17 +216,15 @@ } [data-theme-mode='light'][data-theme-name='spotify'] { - --bui-bg: var(--bui-gray-1); - --bui-bg-surface-1: var(--bui-white); - --bui-bg-surface-2: var(--bui-gray-2); --bui-bg-solid: #1ed760; --bui-bg-solid-hover: #3be477; --bui-bg-solid-pressed: #1abc54; - --bui-bg-solid-disabled: var(--bui-gray-2); + --bui-bg-solid-disabled: #0f6c30; --bui-fg-primary: var(--bui-black); --bui-fg-secondary: var(--bui-gray-7); --bui-fg-solid: var(--bui-black); + --bui-fg-solid-disabled: #62ab7c; --bui-border: var(--bui-gray-3); --bui-border-hover: rgba(0, 0, 0, 0.3); @@ -248,17 +246,13 @@ } [data-theme-mode='dark'][data-theme-name='spotify'] { - --bui-bg: var(--bui-black); - --bui-bg-surface-1: var(--bui-gray-1); - --bui-bg-surface-2: var(--bui-gray-2); + --bui-bg-surface-0: var(--bui-black); + --bui-bg-solid: #1ed760; --bui-bg-solid-hover: #3be477; --bui-bg-solid-pressed: #1abc54; --bui-bg-solid-disabled: #0f6c30; - --bui-bg-tint: #242424; - --bui-bg-tint-hover: #202020; - --bui-bg-tint-pressed: #292929; - --bui-bg-tint-disabled: rgba(255, 255, 255, 0.8); + --bui-bg-danger: #3b1219; --bui-bg-warning: #302008; --bui-bg-success: #132d21; @@ -269,7 +263,7 @@ --bui-fg-link-hover: var(--bui-white); --bui-fg-disabled: var(--bui-gray-5); --bui-fg-solid: var(--bui-black); - --bui-fg-solid-disabled: #083618; + --bui-fg-solid-disabled: #072f15; --bui-fg-tint: var(--bui-white); --bui-fg-tint-disabled: var(--bui-gray-5); --bui-fg-danger: #e22b2b; diff --git a/ADOPTERS.md b/ADOPTERS.md index 208cd370ba..2b3cad0b25 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -94,7 +94,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | | [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | | [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | -| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | +| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@kurtaking](https://github.com/kurtaking), [@pc-bob](https://github.com/pc-bob) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | | [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | | [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | | [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | @@ -287,3 +287,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Syngenta Digital](https://www.syngentadigital.com) | [Bitan Mallick](https://www.linkedin.com/in/bitanmallick) | Internal developer portal, designed to empower developers and streamline workflows. We use software catalog, tech-radar, software templates, tech-docs and various custom plugins to ensure an efficient and collaborative development experience. | | [Sophotech](https://sopho.tech) | [@archy-rock3t-cloud](https://github.com/archy-rock3t-cloud), [Artem Muterko](mailto:artem@sopho.tech) | Custom Developer Platform based on Backstage, providing a service catalog, infrastructure templates, and integrated tooling to give developers a self-service experience. | | [Swiss Mobiliar Insurance Company Ltd.](https://www.mobiliar.ch/) | [Patrick Wyler](mailto:patrick.wyler@mobiliar.ch) and [Beat Winistörfer](mailto:beat.winistoerfer@mobiliar.ch) | The portal provides a unified interface for accessing all relevant DevOps information previously scattered across various locations, enhancing accessibility and clarity for all IT employees. It relies on an internal graph database that enhances the Backstage software catalog with many additional elements. Significant effort has been invested in the visual representation of information through graphs and diagrams, facilitating analysis and improving the understanding of dependencies. | +| [DB Systel GmbH](https://www.dbsystel.de/dbsystel-en/) | [DB Systel GmbH](https://github.com/dbsystel) | Deutsche Bahn's Internal Developer Portal leverages Backstage and Crossplane.io to deliver a fully GitOps-driven onboarding experience for engineering teams across the company. Our Scaffolder ecosystem accelerates platform setup (Artifactory, GitLab, OpenShift), service bootstrapping, automated testing, AI-ready backends, and modern frontend development. We also make heavy use of custom catalog modules and frontend plugins that support developers in checking their provisioned resources or security and compliance of their code. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba40be86c8..80381417a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,7 @@ If you need help, just jump into our [Discord chatroom](https://discord.gg/backs - [Accessibility](#accessibility) - [Get Started!](#get-started) - [Coding Guidelines](#coding-guidelines) +- [AI Use Policy and Guidelines](#ai-use-policy-and-guidelines) - [Documentation Guidelines](#documentation-guidelines) - [Package Scripts](#package-scripts) - [Local configuration](#local-configuration) @@ -128,6 +129,37 @@ If there are any updates in `markdown` file please make sure to run `yarn run li The Backstage development environment does not require any specific editor, but it is intended to be used with one that has built-in linting and type-checking. The development server does not include any checks by default, but they can be enabled using the `--check` flag. Note that using the flag may consume more system resources and slow things down. +## AI Use Policy and Guidelines + +Our goal in the Backstage project is to develop an excellent software system. This requires careful attention to detail in every change we integrate. Maintainer time and attention is very limited, so it's important that changes you ask us to review represent your _best_ work. + +You are encouraged to use tools that help you write good code, including AI tools. However, you always need to understand and explain the changes you're proposing to make, whether or not you used an LLM as part of your process to produce them. The answer to "Why did you make change X?" should never be "I'm not sure. The AI did it." + +**Do not submit an AI-generated PR you haven't personally understood and tested**, as this wastes maintainers' time. PRs that appear to violate this guideline will be closed without review. If you do submit a largely AI-generated PR, clearly mark it as such in the description—maintainers may still close it without further review if it does not seem worthwhile. + +### Using AI as a Coding Assistant + +1. Don't skip **becoming familiar with the part of the codebase** you're working on. This will let you write better prompts and validate their output if you use an LLM. Code assistants can be a useful search engine/discovery tool in this process, but don't trust claims they make about how Backstage works. LLMs are often wrong, even about details that are clearly answered in the [Backstage documentation](https://backstage.io/docs). +2. Split up your changes into **coherent commits**, even if an LLM generates them all in one go. See our section on [Creating Changesets](#creating-changesets) for guidance on documenting your changes. +3. Don't simply ask an LLM to add **code comments**, as it will likely produce a bunch of text that unnecessarily explains what's already clear from the code. If using an LLM to generate comments, be really specific in your request, demand succinctness, and carefully edit the result. + +### Using AI for Communication + +Backstage contributors are expected to communicate with intention, to avoid wasting maintainer time with long, sloppy writing. We strongly prefer clear and concise communication about points that actually require discussion over long AI-generated comments. + +When you use an LLM to write a message for you, it remains **your responsibility** to read through the whole thing and make sure that it makes sense to you and represents your ideas concisely. A good rule of thumb is that if you can't make yourself carefully read some LLM output that you generated, nobody else wants to read it either. + +Here are some concrete guidelines for using LLMs as part of your communication workflows: + +1. When writing a pull request description, **do not include anything that's obvious** from looking at your changes directly (e.g., files changed, functions updated, etc.). Instead, focus on the _why_ behind your changes. Don't ask an LLM to generate a PR description on your behalf based on your code changes, as it will simply regurgitate the information that's already there. +2. Similarly, when responding to a pull request comment, **explain _your_ reasoning**. Don't prompt an LLM to re-describe what can already be seen from the code. +3. Verify that **everything you write is accurate**, whether or not an LLM generated any part of it. Backstage's maintainers will be unable to review your contributions if you misrepresent your work (e.g., wrongly describing your code changes, their effect, or your testing process). +4. Complete all parts of the **PR description template**, including screenshots and the checklist. Don't simply overwrite the template with LLM output. +5. **Clarity and succinctness** are much more important than perfect grammar, so you shouldn't feel obliged to pass your writing through an LLM. If you do ask an LLM to clean up your writing style, be sure it does _not_ make it longer in the process. Demand succinctness in your prompt. +6. Quoting an LLM answer is usually less helpful than linking to **relevant primary sources**, like source code, [Backstage documentation](https://backstage.io/docs), or reference materials. If you do need to quote an LLM answer in a discussion, clearly distinguish LLM output from your own thoughts. + +If you have questions about AI usage or need help, feel free to ask in [Discord](https://discord.gg/backstage-687207715902193673). + ## Documentation Guidelines Contributing to the docs is one of the best ways to start getting involved with Backstage. The documentation site is often the first stop for anyone using or exploring Backstage, so even small improvements can have a big impact! diff --git a/OWNERS.md b/OWNERS.md index d4ee3c6400..764237d9c1 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -5,11 +5,13 @@ Team: @backstage/maintainers -| Maintainer | Organization | GitHub | Discord | -| --------------- | ------------ | ----------------------------------------------- | ------------- | -| Patrik Oldsberg | Spotify | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | -| Fredrik Adelöw | Spotify | [freben](https://github.com/freben) | `freben#3926` | -| Ben Lambert | Spotify | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` | +| Maintainer | Organization | GitHub | Discord | +| --------------- | ------------ | ----------------------------------------------------- | ------------- | +| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | +| Ben Lambert | Spotify | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` | +| Fredrik Adelöw | Spotify | [freben](https://github.com/freben) | `freben#3926` | +| Patrik Oldsberg | Spotify | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | ## Project Areas @@ -45,6 +47,20 @@ Scope: The catalog plugin and catalog model | Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | | Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` | +### Community Plugins + +Team: @backstage/community-plugins-maintainers + +Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugins repository](https://github.com/backstage/community-plugins) + +| Name | Organization | GitHub | Discord | +| -------------------- | ------------- | ------------------------------------------- | ------------ | +| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` | +| Kashish Mittal | Red Hat | [04kash](https://github.com/04kash) | `kashh._.` | +| Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | +| Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` | + ### Design System Team: @backstage/design-system-maintainers @@ -64,7 +80,7 @@ Scope: The Backstage Documentation and Microsite, excluding the plugins listing | Name | Organization | GitHub | Discord | | --------------- | ------------- | ----------------------------------------------------- | ------------- | -| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | | Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` | @@ -206,20 +222,6 @@ Scope: The auditor core service, along with auditor usage in the main repository | ------------ | ------------ | ----------------------------------------------- | -------------- | | Paul Schultz | Red Hat | [schultzp2020](https://github.com/schultzp2020) | `schultzp2020` | -### Community Plugins - -Team: @backstage/community-plugins-maintainers - -Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugins repository](https://github.com/backstage/community-plugins) - -| Name | Organization | GitHub | Discord | -| -------------------- | ------------- | ------------------------------------------- | ------------ | -| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | -| Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` | -| Kashish Mittal | Red Hat | [04kash](https://github.com/04kash) | `kashh._.` | -| Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | -| Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` | - ### Events Team: @backstage/events-maintainers @@ -267,6 +269,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Niklas Gustavsson | Spotify | [protocol7](https://github.com/protocol7) | | | Dave Zolotusky | Spotify | [dzolotusky](https://github.com/dzolotusky) | | | Pia Nilsson | Spotify | [pianilsson](https://github.com/pianilsson) | | +| Stefan Särne | Spotify | [ssarne](https://github.com/ssarne) | | ## Organization Members @@ -274,9 +277,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | ------------------------------ | ------------------------- | ----------------------------------------------------- | ------------------------------ | | Adam Harvey | Okta | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey_` | | Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` | -| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` | -| Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | | Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` | | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | diff --git a/README-fr_FR.md b/README-fr_FR.md index a54e9c8c49..27a7cd1153 100644 --- a/README-fr_FR.md +++ b/README-fr_FR.md @@ -41,7 +41,7 @@ Pour commencer à utiliser Backstage, consultez [Getting Started documentation]( ## Documentation -La documentation de Backstage inclus: +La documentation de Backstage inclut: - [Main documentation](https://backstage.io/docs) - [Software Catalog](https://backstage.io/docs/features/software-catalog/) diff --git a/README.md b/README.md index ba44ca951d..e3e9bd77dd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -> [!NOTE] -> 🏖 From Monday October 27th through November 3rd, maintainers and Spotify employees will be on vacation due to Wellness Week. Expect the project to move a little slower than normal, and support to be limited. Normal service will resume after that! 🏝 - [![headline](docs/assets/headline.png)](https://backstage.io/) # [Backstage](https://backstage.io) diff --git a/app-config.yaml b/app-config.yaml index b45255d26c..ac66d223b4 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -109,8 +109,12 @@ integrations: token: ${GITLAB_TOKEN} ### Example for how to add a bitbucket cloud integration # bitbucketCloud: + # # Using API token # - username: ${BITBUCKET_USERNAME} - # appPassword: ${BITBUCKET_APP_PASSWORD} + # token: ${BITBUCKET_API_TOKEN} + # # Using OAuth + # - clientId: ${BITBUCKET_CLIENT_ID} + # clientSecret: ${BITBUCKET_CLIENT_SECRET} ### Example for how to add your bitbucket server instance using the API: # - host: server.bitbucket.com # apiBaseUrl: server.bitbucket.com @@ -185,15 +189,9 @@ catalog: target: ../catalog-model/examples/acme-corp.yaml rules: - allow: [User, Group] - # Example component for github-actions and TechDocs - - type: file - target: ../../plugins/github-actions/examples/sample.yaml # Example component for TechDocs - type: file target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml - # Example component for ADRs - - type: file - target: ../../plugins/adr/examples/component/catalog-info.yaml # Backstage example templates - type: file target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -209,6 +207,11 @@ scaffolder: email: scaffolder@backstage.io # Use to customize the default commit message when new components are created defaultCommitMessage: 'Initial commit' + defaultEnvironment: + parameters: + region: eu-west-1 + secrets: + environment: ${NODE_ENV} auth: experimentalDynamicClientRegistration: @@ -306,3 +309,8 @@ auth: permission: enabled: true + +devTools: + scheduledTasks: + plugins: + - catalog diff --git a/contrib/catalog/ImmediateEntityProvider.ts b/contrib/catalog/ImmediateEntityProvider.ts index b5580b775a..9f9f986567 100644 --- a/contrib/catalog/ImmediateEntityProvider.ts +++ b/contrib/catalog/ImmediateEntityProvider.ts @@ -1,3 +1,8 @@ +import { + coreServices, + createBackendModule, + LoggerService, +} from '@backstage/backend-plugin-api'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -10,50 +15,18 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; -import { parseEntityYaml } from '@backstage/plugin-catalog-backend'; +import { parseEntityYaml } from '@backstage/plugin-catalog-node'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import bodyParser from 'body-parser'; import express from 'express'; import Router from 'express-promise-router'; import lodash from 'lodash'; -import { Logger } from 'winston'; /** * An entity provider attached to a router, that lets users perform direct * manipulation of a set of entities using REST requests. - * - * @remarks - * - * Installation: - * - * Add it to the catalog builder in your - * `packages/backend/src/plugins/catalog.ts`. Note that it BOTH adds a provider - * and amends the catalog router: - * - * ``` - * const immediate = new ImmediateEntityProvider({ - * logger: env.logger, - * handleEntity: (deferred) => { - * // Optionally modify the incoming entity - * }, - * }); - * builder.addEntityProvider(immediate); - * - * // ... - * - * return router.use('/immediate', immediate.getRouter()); - * ``` - * - * API (assume a catalog prefix, e.g. `/api/catalog`): - * - * - `POST /immediate/entities`: Accepts a YAML document of entities, and - * inserts or updates the entities that match that document. Returns 201 OK on - * success. - * - * - `PUT /immediate/entities`: Accepts a YAML document of entities, and - * replaces the entire set of entities managed by the provider with those - * entities. Returns 201 OK on success. */ -export class ImmediateEntityProvider implements EntityProvider { +class ImmediateEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; private readonly entityValidator: (data: unknown) => Entity; @@ -76,7 +49,7 @@ export class ImmediateEntityProvider implements EntityProvider { router.use(bodyParser.raw({ type: '*/*' })); - router.post('/entities', async (req, res) => { + router.post('/immediate/entities', async (req, res) => { if (!this.connection) { throw new Error(`Service is not yet initialized`); } @@ -89,7 +62,7 @@ export class ImmediateEntityProvider implements EntityProvider { res.status(201).end(); }); - router.put('/entities', async (req, res) => { + router.put('/immediate/entities', async (req, res) => { if (!this.connection) { throw new Error(`Service is not yet initialized`); } @@ -151,19 +124,59 @@ export class ImmediateEntityProvider implements EntityProvider { /** * Options for {@link ImmediateEntityProvider}. */ -export interface ImmediateEntityProviderOptions { +interface ImmediateEntityProviderOptions { /** - * The logger to use. + * The logger. */ - logger: Logger; + logger: LoggerService; /** - * An optional function to perform adjustments to, or validate, an incoming - * entity before being stored. It is permitted to modify the deferred entity, - * but the request is static and has had its body consumed. + * An optional callback function to perform adjustments to, or validate, an + * incoming entity before being stored. It is permitted to modify the deferred + * entity, but the request is static and has had its body consumed. */ handleEntity?: ( request: express.Request, deferred: DeferredEntity, ) => void | Promise; } + +/** + * Backend module that installs an immediate entity provider. + * + * @remarks + * + * Install it by doing `backend.add(immediateEntityProviderModule)` in your `packages/backend/src/index.ts` file. + * + * API: + * + * - `POST /api/catalog/immediate/entities`: Accepts a YAML document of entities, and + * inserts or updates the entities that match that document. Returns 201 OK on + * success. + * + * - `PUT /api/catalog/immediate/entities`: Accepts a YAML document of entities, and + * replaces the entire set of entities managed by the provider with those + * entities. Returns 201 OK on success. + */ +export const immediateEntityProviderModule = createBackendModule({ + pluginId: 'catalog', + moduleId: 'immediate-entity-provider', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + router: coreServices.httpRouter, + catalogProcessing: catalogProcessingExtensionPoint, + }, + async init({ logger, router, catalogProcessing }) { + const provider = new ImmediateEntityProvider({ + logger, + // add handleEntity here if you need to modify incoming entities before they are stored + }); + + catalogProcessing.addEntityProvider(provider); + router.use(provider.getRouter()); + }, + }); + }, +}); diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild index 47fd506a34..6c87681391 100644 --- a/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild +++ b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild @@ -6,7 +6,7 @@ # This dockerfile also performs the build first inside docker. This may come # with a build time impact, but is sometimes desirable. If you want to run the -# build on the host instead, use the file simply named Dockerfile in this folder +# build on the host instead, use the file simply named Dockerfile.hostbuild in this folder # instead. # USAGE: diff --git a/contrib/docs/tutorials/heroku-deployment.md b/contrib/docs/tutorials/heroku-deployment.md index 2dea1f021c..8d0b8740b7 100644 --- a/contrib/docs/tutorials/heroku-deployment.md +++ b/contrib/docs/tutorials/heroku-deployment.md @@ -49,7 +49,7 @@ Add a build script in `package.json` to compile frontend during deployment: ## Start Command -Create a [Procfile](https://devcenter.heroku.com/articles/procfile) in the app's root: +Create a [`Procfile`](https://devcenter.heroku.com/articles/procfile) in the app's root: ```shell echo "web: yarn workspace backend start --config ../../app-config.yaml --config ../../app-config.production.yaml" > Procfile diff --git a/docker-compose.deps.yml b/docker-compose.deps.yml index c7515eca04..fa7937fdf1 100644 --- a/docker-compose.deps.yml +++ b/docker-compose.deps.yml @@ -2,7 +2,7 @@ name: backstage services: psql: - image: postgres:17.6 + image: postgres:17.7 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -24,7 +24,7 @@ services: timeout: 3s retries: 5 opensearch: - image: opensearchproject/opensearch:2.19.3 + image: opensearchproject/opensearch:2.19.4 environment: plugins.security.disabled: true discovery.type: single-node diff --git a/docs-ui/README.md b/docs-ui/README.md index 41b9921413..b943f1ce7c 100644 --- a/docs-ui/README.md +++ b/docs-ui/README.md @@ -13,3 +13,28 @@ yarn start ## Deployment Deployments are done automatically when a PR is merged into the `master` branch. We host the website using Github pages. + +## Maintaining Component Changelogs + +After a `@backstage/ui` release, sync the component changelogs to keep documentation up-to-date: + +```bash +yarn sync:changelog +``` + +This script: + +- Parses `packages/ui/CHANGELOG.md` for new versions +- Extracts entries tagged with "Affected components: ..." +- Updates `src/utils/changelog.ts` with new entries +- Handles both component-specific and general package changes + +After running, review the changes in `src/utils/changelog.ts` and commit them. + +**Preview changes before writing:** + +```bash +yarn sync:changelog:dry-run +``` + +Running this gives you a summary of what would be written, without actually adding or changing any files. diff --git a/docs-ui/next.config.mjs b/docs-ui/next.config.mjs index 8e6ed8fdc0..2ff341b609 100644 --- a/docs-ui/next.config.mjs +++ b/docs-ui/next.config.mjs @@ -1,4 +1,6 @@ import createMDX from '@next/mdx'; +import path from 'path'; +import { fileURLToPath } from 'url'; const nextConfig = { pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], @@ -13,6 +15,10 @@ const nextConfig = { // These are type-level conflicts that don't affect runtime behavior ignoreBuildErrors: true, }, + outputFileTracingRoot: path.join( + path.dirname(fileURLToPath(import.meta.url)), + '..', + ), }; const withMDX = createMDX({}); diff --git a/docs-ui/package.json b/docs-ui/package.json index 17c13f64f3..ce9b8d070f 100644 --- a/docs-ui/package.json +++ b/docs-ui/package.json @@ -7,7 +7,9 @@ "build": "next build", "lint": "next lint", "prestart": "yarn sync:css", - "start": "concurrently \"yarn sync:css:watch\" \"next dev\"", + "start": "next dev", + "sync:changelog": "node scripts/sync-changelog.mjs", + "sync:changelog:dry-run": "node scripts/sync-changelog.mjs --dry-run", "sync:css": "node scripts/sync-css.js", "sync:css:watch": "node scripts/sync-css.js --watch" }, @@ -16,37 +18,38 @@ "@types/react-dom": "19.1.7" }, "dependencies": { + "@base-ui-components/react": "^1.0.0-beta.4", "@codemirror/lang-sass": "^6.0.2", "@codemirror/view": "^6.34.4", "@lezer/highlight": "^1.2.1", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@next/mdx": "15.3.4", + "@next/mdx": "15.5.7", "@remixicon/react": "^4.6.0", - "@storybook/react": "^8.6.12", "@uiw/codemirror-themes": "^4.23.7", "@uiw/react-codemirror": "^4.23.7", + "clsx": "^2.1.1", "html-react-parser": "^5.2.5", "motion": "^12.4.1", - "next": "15.4.7", + "next": "15.5.9", "next-mdx-remote-client": "^2.1.2", "prop-types": "^15.8.1", "react": "19.1.1", "react-dom": "19.1.1", - "shiki": "^3.13.0", - "storybook": "^8.6.12" + "shiki": "^3.13.0" }, "devDependencies": { + "@octokit/rest": "^22.0.1", "@shikijs/transformers": "^3.13.0", "@types/mdx": "^2.0.13", - "@types/node": "^20", + "@types/node": "^22.13.14", "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "chokidar": "^3.6.0", - "concurrently": "^8.2.2", "eslint": "^8", - "eslint-config-next": "15.3.4", + "eslint-config-next": "15.5.7", "lightningcss": "^1.28.2", - "typescript": "^5" + "typescript": "^5", + "unified": "^11.0.4" } } diff --git a/docs-ui/public/theme-backstage.css b/docs-ui/public/theme-backstage.css index 59a5b986be..9dd84816cf 100644 --- a/docs-ui/public/theme-backstage.css +++ b/docs-ui/public/theme-backstage.css @@ -1 +1 @@ -@layer tokens{:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}}@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}body{background-color:var(--bui-bg);color:var(--bui-fg-primary);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-3);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}@layer components;@layer utilities{.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--ml-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--ml-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--ml-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--ml-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--ml-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--mr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--mr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--mr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--mr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--mr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--mt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--mt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--mt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--mt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--mt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--mb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--mb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--mb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--mb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--mb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--my-xs);margin-bottom:var(--my-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--my-sm);margin-bottom:var(--my-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--my-md);margin-bottom:var(--my-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--my-lg);margin-bottom:var(--my-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--my-xl);margin-bottom:var(--my-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--mx-xs);margin-right:var(--mx-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--mx-sm);margin-right:var(--mx-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--mx-md);margin-right:var(--mx-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--mx-lg);margin-right:var(--mx-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--mx-xl);margin-right:var(--mx-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}} \ No newline at end of file +@layer tokens{:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg-surface-0:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-1);--bui-bg-surface-3:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#163a66;--bui-bg-neutral-on-surface-0:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-0-hover:oklch(0% 0 0/.12);--bui-bg-neutral-on-surface-0-pressed:oklch(0% 0 0/.16);--bui-bg-neutral-on-surface-0-disabled:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-1:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-1-hover:oklch(0% 0 0/.12);--bui-bg-neutral-on-surface-1-pressed:oklch(0% 0 0/.16);--bui-bg-neutral-on-surface-1-disabled:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-2:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-2-hover:oklch(0% 0 0/.12);--bui-bg-neutral-on-surface-2-pressed:oklch(0% 0 0/.16);--bui-bg-neutral-on-surface-2-disabled:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-3:oklch(0% 0 0/.06);--bui-bg-neutral-on-surface-3-hover:oklch(0% 0 0/.12);--bui-bg-neutral-on-surface-3-pressed:oklch(0% 0 0/.16);--bui-bg-neutral-on-surface-3-disabled:oklch(0% 0 0/.06);--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#98a8bc;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg-surface-0:#333;--bui-bg-surface-1:var(--bui-gray-1);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-surface-3:var(--bui-gray-3);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#1b3d68;--bui-bg-neutral-on-surface-0:oklch(100% 0 0/.1);--bui-bg-neutral-on-surface-0-hover:oklch(100% 0 0/.14);--bui-bg-neutral-on-surface-0-pressed:oklch(100% 0 0/.2);--bui-bg-neutral-on-surface-0-disabled:oklch(100% 0 0/.1);--bui-bg-neutral-on-surface-1:oklch(100% 0 0/.06);--bui-bg-neutral-on-surface-1-hover:oklch(100% 0 0/.1);--bui-bg-neutral-on-surface-1-pressed:oklch(100% 0 0/.16);--bui-bg-neutral-on-surface-1-disabled:oklch(100% 0 0/.06);--bui-bg-neutral-on-surface-2:oklch(100% 0 0/.08);--bui-bg-neutral-on-surface-2-hover:oklch(100% 0 0/.12);--bui-bg-neutral-on-surface-2-pressed:oklch(100% 0 0/.2);--bui-bg-neutral-on-surface-2-disabled:oklch(100% 0 0/.08);--bui-bg-neutral-on-surface-3:oklch(100% 0 0/.08);--bui-bg-neutral-on-surface-3-hover:oklch(100% 0 0/.12);--bui-bg-neutral-on-surface-3-pressed:oklch(100% 0 0/.2);--bui-bg-neutral-on-surface-3-disabled:oklch(100% 0 0/.08);--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:#6191cc;--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757;--bui-shadow:none}}@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}body{background-color:var(--bui-bg-surface-0);color:var(--bui-fg-primary);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-3);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}[data-theme-mode=dark]{color-scheme:dark}[data-theme-mode=light]{color-scheme:light}}@layer components;@layer utilities{.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--ml-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--ml-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--ml-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--ml-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--ml-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--mr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--mr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--mr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--mr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--mr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--mt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--mt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--mt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--mt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--mt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--mb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--mb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--mb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--mb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--mb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--my-xs);margin-bottom:var(--my-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--my-sm);margin-bottom:var(--my-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--my-md);margin-bottom:var(--my-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--my-lg);margin-bottom:var(--my-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--my-xl);margin-bottom:var(--my-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--mx-xs);margin-right:var(--mx-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--mx-sm);margin-right:var(--mx-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--mx-md);margin-right:var(--mx-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--mx-lg);margin-right:var(--mx-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--mx-xl);margin-right:var(--mx-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}} \ No newline at end of file diff --git a/docs-ui/public/theme-spotify.css b/docs-ui/public/theme-spotify.css index b0bf517701..22eda0cabe 100644 --- a/docs-ui/public/theme-spotify.css +++ b/docs-ui/public/theme-spotify.css @@ -1 +1 @@ -@font-face{font-family:CircularSpTitle;font-weight:900;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Black-4588c99025b967475c31695b0743dd1a.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Black-506746f387a26f25aa3d023b3e501d34.woff)format("woff")}@font-face{font-family:CircularSpTitle;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-b2586b06a2e1522e9d879d84c2792a58.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-1624fb2df28c20d7203d7fb86ce8b481.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:400;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Book-a00e99ef9996a3a157fb6b746856d04f.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Book-3f73da7d35bd81c706bce7bbb84964de.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}[data-theme-name=spotify]{--bui-font-text:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-title:CircularSpTitle,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-regular:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;& .bui-Button{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3)}& .bui-ButtonIcon{padding:0}& .bui-MenuPopup{box-sizing:border-box;max-width:21.25rem}& .bui-MenuSubmenuTrigger,& .bui-MenuItem{height:auto;min-height:2rem}& .bui-Text{font-family:var(--bui-font-text)}& .bui-Heading{font-family:var(--bui-font-title)}& .bui-TableRow{border-radius:var(--bui-radius-4);border:none}& .bui-TableRow:hover td:first-child{border-top-left-radius:var(--bui-radius-2);border-bottom-left-radius:var(--bui-radius-2)}& .bui-TableRow:hover td:last-child{border-top-right-radius:var(--bui-radius-2);border-bottom-right-radius:var(--bui-radius-2)}& .bui-TableBody:before,& .bui-TableBody:after{line-height:var(--bui-space-1);content:"‌";display:block}& .bui-TableHeader .bui-TableRow{border-bottom:1px solid var(--bui-border)}& .bui-TableHead{font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular)}& .bui-HeaderToolbar{padding-top:var(--bui-space-2);padding-inline:var(--bui-space-2)}& .bui-HeaderToolbarWrapper{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3);border:none}& .bui-HeaderToolbarControls{right:calc(var(--bui-space-3) - 1px)}& .bui-HeaderTabsWrapper{margin-top:var(--bui-space-2);margin-inline:var(--bui-space-2);border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-1);border:none}& .bui-Input{border-radius:var(--bui-radius-3)}& .bui-Tag{border-radius:var(--bui-radius-full)}}[data-theme-mode=light][data-theme-name=spotify]{--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:var(--bui-gray-2);--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-solid:var(--bui-black);--bui-border:var(--bui-gray-3);--bui-border-hover:#0000004d;--bui-border-pressed:#00000080;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#0003;& .bui-HeaderToolbarWrapper,& .bui-HeaderTabsWrapper{border:1px solid var(--bui-border)}}[data-theme-mode=dark][data-theme-name=spotify]{--bui-bg:var(--bui-black);--bui-bg-surface-1:var(--bui-gray-1);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:#0f6c30;--bui-bg-tint:#242424;--bui-bg-tint-hover:#202020;--bui-bg-tint-pressed:#292929;--bui-bg-tint-disabled:#fffc;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:var(--bui-white);--bui-fg-link-hover:var(--bui-white);--bui-fg-disabled:var(--bui-gray-5);--bui-fg-solid:var(--bui-black);--bui-fg-solid-disabled:#083618;--bui-fg-tint:var(--bui-white);--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:var(--bui-gray-3);--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#fff3} \ No newline at end of file +@font-face{font-family:CircularSpTitle;font-weight:900;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Black-4588c99025b967475c31695b0743dd1a.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Black-506746f387a26f25aa3d023b3e501d34.woff)format("woff")}@font-face{font-family:CircularSpTitle;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-b2586b06a2e1522e9d879d84c2792a58.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-1624fb2df28c20d7203d7fb86ce8b481.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:400;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Book-a00e99ef9996a3a157fb6b746856d04f.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Book-3f73da7d35bd81c706bce7bbb84964de.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}[data-theme-name=spotify]{--bui-font-text:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-title:CircularSpTitle,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-regular:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;& .bui-Button{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3)}& .bui-ButtonIcon{padding:0}& .bui-MenuPopup{box-sizing:border-box;max-width:21.25rem}& .bui-MenuSubmenuTrigger,& .bui-MenuItem{height:auto;min-height:2rem}& .bui-Text{font-family:var(--bui-font-text)}& .bui-Heading{font-family:var(--bui-font-title)}& .bui-TableRow{border-radius:var(--bui-radius-4);border:none}& .bui-TableRow:hover td:first-child{border-top-left-radius:var(--bui-radius-2);border-bottom-left-radius:var(--bui-radius-2)}& .bui-TableRow:hover td:last-child{border-top-right-radius:var(--bui-radius-2);border-bottom-right-radius:var(--bui-radius-2)}& .bui-TableBody:before,& .bui-TableBody:after{line-height:var(--bui-space-1);content:"‌";display:block}& .bui-TableHeader .bui-TableRow{border-bottom:1px solid var(--bui-border)}& .bui-TableHead{font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular)}& .bui-HeaderToolbar{padding-top:var(--bui-space-2);padding-inline:var(--bui-space-2)}& .bui-HeaderToolbarWrapper{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3);border:none}& .bui-HeaderToolbarControls{right:calc(var(--bui-space-3) - 1px)}& .bui-HeaderTabsWrapper{margin-top:var(--bui-space-2);margin-inline:var(--bui-space-2);border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-1);border:none}& .bui-Input{border-radius:var(--bui-radius-3)}& .bui-Tag{border-radius:var(--bui-radius-full)}}[data-theme-mode=light][data-theme-name=spotify]{--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:#0f6c30;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-solid:var(--bui-black);--bui-fg-solid-disabled:#62ab7c;--bui-border:var(--bui-gray-3);--bui-border-hover:#0000004d;--bui-border-pressed:#00000080;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#0003;& .bui-HeaderToolbarWrapper,& .bui-HeaderTabsWrapper{border:1px solid var(--bui-border)}}[data-theme-mode=dark][data-theme-name=spotify]{--bui-bg-surface-0:var(--bui-black);--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:#0f6c30;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:var(--bui-white);--bui-fg-link-hover:var(--bui-white);--bui-fg-disabled:var(--bui-gray-5);--bui-fg-solid:var(--bui-black);--bui-fg-solid-disabled:#072f15;--bui-fg-tint:var(--bui-white);--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:var(--bui-gray-3);--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#fff3} \ No newline at end of file diff --git a/docs-ui/scripts/sync-changelog.mjs b/docs-ui/scripts/sync-changelog.mjs new file mode 100644 index 0000000000..5eab3616dd --- /dev/null +++ b/docs-ui/scripts/sync-changelog.mjs @@ -0,0 +1,991 @@ +#!/usr/bin/env node +/* eslint-disable no-restricted-syntax */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import { Octokit } from '@octokit/rest'; +import { execSync } from 'child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Compare two semantic versions. + * Returns: 1 if versionA > versionB, -1 if versionA < versionB, 0 if equal + * Handles pre-release versions (e.g., "0.9.0-next.2") + */ +function compareVersions(versionA, versionB) { + // Strip 'v' prefix if present + versionA = versionA.replace(/^v/, ''); + versionB = versionB.replace(/^v/, ''); + + // Split into [major.minor.patch, prerelease] + const [versionABase, versionAPre] = versionA.split('-'); + const [versionBBase, versionBPre] = versionB.split('-'); + + // Compare base versions + const versionAParts = versionABase.split('.').map(Number); + const versionBParts = versionBBase.split('.').map(Number); + + for ( + let i = 0; + i < Math.max(versionAParts.length, versionBParts.length); + i++ + ) { + const versionAPart = versionAParts[i] || 0; + const versionBPart = versionBParts[i] || 0; + + if (versionAPart > versionBPart) return 1; + if (versionAPart < versionBPart) return -1; + } + + // If base versions equal, compare pre-release + if (!versionAPre && !versionBPre) return 0; + if (!versionAPre) return 1; // versionA is release, versionB is pre-release + if (!versionBPre) return -1; // versionA is pre-release, versionB is release + + // Both have pre-release, compare them + return versionAPre.localeCompare(versionBPre); +} + +/** + * Check if versionA is newer than versionB + */ +function isNewerVersion(versionA, versionB) { + return compareVersions(versionA, versionB) > 0; +} + +/** + * Normalize version to base version (remove pre-release suffix) + * "0.9.0-next.3" → "0.9.0" + * "0.9.0" → "0.9.0" + */ +function normalizeVersion(version) { + return version.split('-')[0]; +} + +/** + * Read the existing changelog.ts and find the highest version number + * by scanning the changelogs/ directory for version files + */ +function getLastSyncedVersion(changelogPath) { + try { + // Scan changelogs/ directory for latest version file + const changelogsDir = path.join(path.dirname(changelogPath), 'changelogs'); + + if (!fs.existsSync(changelogsDir)) { + return null; + } + + // Read all version files (v0.1.0.ts, v0.2.0.ts, etc.) + const files = fs + .readdirSync(changelogsDir) + .filter(f => f.match(/^v\d+\.\d+\.\d+\.ts$/)) + .map(f => f.replace(/^v/, '').replace(/\.ts$/, '')); + + if (files.length === 0) { + return null; + } + + // Find highest version + let highest = files[0]; + for (const version of files) { + if (compareVersions(version, highest) > 0) { + highest = version; + } + } + + return highest; + } catch (error) { + console.error(`Error reading changelog file: ${error.message}`); + return null; + } +} + +/** + * Convert PascalCase component name to kebab-case slug + * Avatar -> avatar + * ButtonIcon -> button-icon + * SearchField -> searchfield (special case per existing data) + */ +function pascalToKebab(str) { + // Special cases observed in changelog.ts + const specialCases = { + SearchField: 'searchfield', + TextField: 'textfield', + DataTable: 'datatable', + ScrollArea: 'scrollarea', + }; + + if (specialCases[str]) { + return specialCases[str]; + } + + // General conversion: insert hyphen before uppercase letters + return str + .replace(/([A-Z])/g, '-$1') + .toLowerCase() + .replace(/^-/, ''); // Remove leading hyphen +} + +/** + * Map component name to valid Component type slug + * Returns null if component not recognized + */ +function mapComponentName(name, validComponents) { + const kebab = pascalToKebab(name.trim()); + const kebabLower = kebab.toLowerCase(); + + // Check if it's a valid component + const valid = validComponents.find(c => c.toLowerCase() === kebabLower); + + return valid || null; +} + +/** + * Get valid component names from types.ts Component type + */ +function getValidComponents(changelogPath) { + // Read from types.ts instead of changelog.ts + const typesPath = changelogPath.replace('changelog.ts', 'types.ts'); + const content = fs.readFileSync(typesPath, 'utf-8'); + + // Extract Component type union + const typeMatch = content.match(/export type Component =([^;]+);/s); + if (!typeMatch) { + throw new Error('Could not find Component type definition'); + } + + // Extract string literals from the union + const components = typeMatch[1] + .match(/['"]([^'"]+)['"]/g) + .map(s => s.replace(/['"]/g, '')); + + return components; +} + +/** + * Parse CHANGELOG.md and extract entries newer than sinceVersion + */ +async function parseChangelogMd(changelogPath, sinceVersion, validComponents) { + const content = fs.readFileSync(changelogPath, 'utf-8'); + const tree = unified().use(remarkParse).parse(content); + + const entries = []; + let currentVersion = null; + let currentSection = null; // 'Minor Changes', 'Patch Changes', 'Major Changes' + let processedCount = 0; + + // Walk through the markdown AST + async function walk(node, depth = 0) { + // Version headers (## 0.9.0) + if (node.type === 'heading' && node.depth === 2) { + const versionText = extractText(node).trim(); + // Validate that this is actually a version number (X.Y.Z or X.Y.Z-pre.N) + // Skip headings that are just markdown content within changelog entries + if (/^\d+\.\d+\.\d+/.test(versionText)) { + currentVersion = versionText; + currentSection = null; + } + return; + } + + // Section headers (### Minor Changes) + if (node.type === 'heading' && node.depth === 3) { + const sectionText = extractText(node); + currentSection = sectionText.trim(); + return; + } + + // List items (- 539cf26: description) + if (node.type === 'listItem' && currentVersion && currentSection) { + // Only process if version is newer than sinceVersion + if (!sinceVersion || isNewerVersion(currentVersion, sinceVersion)) { + const entry = await parseListItem( + node, + currentVersion, + currentSection, + validComponents, + content, + ); + if (entry) { + entries.push(entry); + processedCount++; + // Progress logging every 50 entries + if (processedCount % 50 === 0) { + process.stdout.write(`\r Processed ${processedCount} entries...`); + } + } + } + } + + // Recurse into children + if (node.children) { + for (const child of node.children) { + await walk(child, depth + 1); + } + } + } + + await walk(tree); + + // Clear progress line + if (processedCount > 0) { + process.stdout.write(`\r Processed ${processedCount} entries - done!\n`); + } + + return entries; +} + +/** + * Extract plain text from a markdown node + */ +function extractText(node) { + if (node.type === 'text') { + return node.value; + } + if (node.children) { + return node.children.map(extractText).join(''); + } + return ''; +} + +/** + * Extract raw markdown content from a node using position offsets + * This preserves all formatting: code blocks, lists, bold/italic, etc. + */ +function extractMarkdown(node, sourceText) { + if (!node.position) { + return extractText(node); // Fallback to text extraction + } + + const start = node.position.start.offset; + const end = node.position.end.offset; + + if (start === undefined || end === undefined) { + return extractText(node); + } + + return sourceText.slice(start, end).trim(); +} + +/** + * Parse a list item to extract changelog entry + * NOTE: PR numbers are NOT fetched here - they're fetched later only for new entries + */ +async function parseListItem( + node, + version, + section, + validComponents, + sourceText, +) { + // Extract the full text content for SHA parsing + const fullText = extractText(node); + + // Parse commit SHA and description + // Format: "- 539cf26: description..." + const match = fullText.match(/^-?\s*([a-f0-9]+):\s*(.+)/s); + if (!match) { + return null; + } + + const commitSha = match[1]; + + // Extract full markdown content from the list item + let fullMarkdown = extractMarkdown(node, sourceText); + + // Remove the commit SHA prefix from markdown + let description = fullMarkdown.replace(/^-?\s*[a-f0-9]+:\s*/, '').trim(); + + // Auto-detect components from "Affected components:" line + let components = []; + const componentMatch = description.match(/Affected components?:\s*([^\n]+)/i); + if (componentMatch) { + const componentNames = componentMatch[1] + .split(',') + .map(name => name.trim()) + .filter(Boolean); + + components = componentNames + .map(name => mapComponentName(name, validComponents)) + .filter(Boolean); + + // Optionally strip "Affected components:" line from description + description = description + .replace(/\n*Affected components?:[ \t]*[^\n]+/i, '') + .trim(); + } + + const prs = []; // Will be populated later by fetchPRNumbers() + + // Infer type from section and description + const type = inferChangeType(section, description, version); + + return { + version: normalizeVersion(version), + section, + commitSha, + description, + components, + prs, + type, + }; +} + +/** + * Infer change type from section and description + */ +function inferChangeType(section, description, version) { + const isPre1 = version.startsWith('0.'); + + // Check description keywords first + if (description.match(/^(New|Add(ed)?)\s/i)) { + return 'new'; + } + if (description.includes('BREAKING')) { + return 'breaking'; + } + + // Infer from section + if (section === 'Minor Changes') { + return isPre1 ? 'breaking' : undefined; + } else if (section === 'Major Changes') { + return 'breaking'; + } else if (section === 'Patch Changes') { + return 'fix'; + } + + return undefined; +} + +/** + * Generate new changelog entries and update changelog.ts + */ +/** + * Fetch PR numbers for entries that need them + * Only fetches for entries with empty prs array (new entries) + */ +async function fetchPRNumbers(entries, dryRun = false) { + const entriesNeedingPRs = entries.filter( + e => e.prs.length === 0 && e.commitSha, + ); + + if (entriesNeedingPRs.length === 0) { + return; + } + + console.log( + `\n🔍 Fetching PR numbers for ${entriesNeedingPRs.length} new entries...`, + ); + + let fetchedCount = 0; + const startTime = Date.now(); + + for (const entry of entriesNeedingPRs) { + const prNumber = await findPRNumber(entry.commitSha); + if (prNumber) { + entry.prs.push(prNumber); + } + + fetchedCount++; + // Progress every 10 lookups + if (fetchedCount % 10 === 0 || fetchedCount === entriesNeedingPRs.length) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + const rate = ((fetchedCount / (Date.now() - startTime)) * 1000).toFixed( + 1, + ); + process.stdout.write( + `\r Fetched ${fetchedCount}/${entriesNeedingPRs.length} PR numbers (${elapsed}s, ~${rate} req/s)`, + ); + } + } + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); + const foundCount = entriesNeedingPRs.filter(e => e.prs.length > 0).length; + console.log( + `\n ✓ Found ${foundCount}/${entriesNeedingPRs.length} PR numbers in ${totalTime}s`, + ); +} + +/** + * Generate per-version changelog files in changelogs/ directory + */ +async function generateVersionFiles(entries, changelogsDir, dryRun = false) { + // Group entries by normalized version + const byVersion = {}; + entries.forEach(entry => { + const version = entry.version; + if (!byVersion[version]) { + byVersion[version] = []; + } + byVersion[version].push(entry); + }); + + const versionFiles = []; + const skippedVersions = []; + const generatedVersions = []; + + for (const [version, versionEntries] of Object.entries(byVersion)) { + const fileName = `v${version}.ts`; + const filePath = path.join(changelogsDir, fileName); + const varName = `changelog_${version.replace(/\./g, '_')}`; + + // Check if file already exists - skip if it does + if (fs.existsSync(filePath)) { + skippedVersions.push(version); + // Still add to versionFiles array so main changelog.ts can import it + versionFiles.push({ + version, + fileName, + filePath, + varName, + content: null, // No content needed for existing files + skipped: true, + }); + continue; + } + + // Generate TypeScript code for this version + const entryObjects = versionEntries + .map(entry => { + const componentsStr = `[${entry.components + .map(c => `'${c}'`) + .join(', ')}]`; + const prsStr = `[${entry.prs.map(pr => `'${pr}'`).join(', ')}]`; + const typeStr = entry.type ? `type: '${entry.type}',` : ''; + const shaStr = entry.commitSha + ? `commitSha: '${entry.commitSha}',` + : ''; + + // Escape description for template literal + const descEscaped = entry.description + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + .replace(/\${/g, '\\${'); + + return ` { + components: ${componentsStr}, + version: '${entry.version}', + prs: ${prsStr}, + description: \`${descEscaped}\`, + ${typeStr} + ${shaStr} + }`; + }) + .join(',\n'); + + const fileContent = `import type { ChangelogProps } from '../types'; + +export const ${varName}: ChangelogProps[] = [ +${entryObjects} +]; +`; + + generatedVersions.push(version); + versionFiles.push({ + version, + fileName, + filePath, + varName, + content: fileContent, + skipped: false, + }); + + if (!dryRun) { + fs.writeFileSync(filePath, fileContent, 'utf-8'); + } + } + + // Log summary + if (skippedVersions.length > 0) { + console.log( + `\n⏭️ Skipped ${ + skippedVersions.length + } existing version files: ${skippedVersions + .sort((a, b) => compareVersions(a, b)) + .join(', ')}`, + ); + } + if (generatedVersions.length > 0) { + console.log( + `\n✨ ${dryRun ? 'Would generate' : 'Generated'} ${ + generatedVersions.length + } new version files: ${generatedVersions + .sort((a, b) => compareVersions(a, b)) + .join(', ')}`, + ); + } + + return versionFiles; +} + +/** + * Generate main changelog.ts that imports and spreads all version files + */ +async function generateMainChangelog( + versionFiles, + changelogPath, + dryRun = false, +) { + // Get the changelogs directory path + const changelogsDir = path.join(path.dirname(changelogPath), 'changelogs'); + + // Read all version files from the changelogs directory + const allVersionFiles = new Map(); + + // First, add all files from the directory (if it exists) + if (fs.existsSync(changelogsDir)) { + const files = fs.readdirSync(changelogsDir); + files.forEach(file => { + if (file.match(/^v\d+\.\d+\.\d+\.ts$/)) { + const version = file.replace(/^v/, '').replace(/\.ts$/, ''); + const varName = `changelog_${version.replace(/\./g, '_')}`; + allVersionFiles.set(version, { + version, + fileName: file, + varName, + }); + } + }); + } + + // Then, add/update with files from this run (in case new ones were generated) + versionFiles.forEach(vf => { + allVersionFiles.set(vf.version, { + version: vf.version, + fileName: vf.fileName, + varName: vf.varName, + }); + }); + + // Sort versions in descending order + const sortedVersions = Array.from(allVersionFiles.values()).sort((a, b) => + compareVersions(b.version, a.version), + ); + + const imports = sortedVersions + .map( + v => + `import { ${v.varName} } from './changelogs/${v.fileName.replace( + '.ts', + '', + )}';`, + ) + .join('\n'); + + const spreads = sortedVersions.map(v => ` ...${v.varName},`).join('\n'); + + const content = `export * from './types'; +${imports} + +export const changelog = [ +${spreads} +]; +`; + + console.log( + `\n📝 Main changelog.ts will import all ${sortedVersions.length} version files`, + ); + + if (!dryRun) { + fs.writeFileSync(changelogPath, content, 'utf-8'); + } + + return content; +} + +/** + * Check if an entry already exists in changelog.ts + */ +function isDuplicate(entry, existingContent) { + // Simple check: look for version + description substring + const descStart = entry.description.substring(0, 50); + const pattern = `version: '${entry.version}'`; + + const versionIndex = existingContent.indexOf(pattern); + if (versionIndex === -1) return false; + + // Check if description appears near this version + const contextWindow = existingContent.substring( + Math.max(0, versionIndex - 200), + versionIndex + 500, + ); + + return contextWindow.includes(descStart); +} + +/** + * Detect GitHub authentication method + * Priority: gh CLI → GITHUB_TOKEN env → unauthenticated + */ +function getGitHubAuth() { + // 1. Try gh CLI (preferred - uses keyring auth) + try { + // Check specifically for github.com authentication + const output = execSync('gh auth status -h github.com 2>&1', { + encoding: 'utf-8', + }); + if (output.includes('Logged in to github.com')) { + console.log('✓ Using gh CLI authentication'); + return { method: 'gh-cli' }; + } + } catch { + // gh not installed or not authenticated to github.com + } + + // 2. Try GITHUB_TOKEN env var + if (process.env.GITHUB_TOKEN) { + console.log('✓ Using GITHUB_TOKEN authentication'); + return { method: 'token', token: process.env.GITHUB_TOKEN }; + } + + // 3. Fallback to unauthenticated (60 req/hour limit) + console.warn('⚠️ Using unauthenticated GitHub API (60 requests/hour)'); + console.warn(' For higher limits: authenticate gh CLI or set GITHUB_TOKEN'); + return { method: 'unauthenticated' }; +} + +// Initialize auth and Octokit +const githubAuth = getGitHubAuth(); +const octokit = + githubAuth.method === 'token' + ? new Octokit({ auth: githubAuth.token }) + : new Octokit(); + +// Cache for PR lookups +const prCache = new Map(); + +/** + * Expand a truncated SHA to full SHA using local git repository + * Falls back to GitHub API if git rev-parse fails + */ +async function expandCommitSha(shortSha) { + try { + // First try local git repository (fastest, works for all commits) + // We need to run this from the backstage monorepo root + const repoRoot = path.join(__dirname, '../../'); + + try { + const fullSha = execSync(`git rev-parse ${shortSha}`, { + encoding: 'utf-8', + cwd: repoRoot, + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + + if (fullSha && fullSha.length === 40) { + return fullSha; + } + } catch (gitError) { + // Handle ambiguous SHA - parse git's error output to find commit candidates + if (gitError.stderr && gitError.stderr.includes('ambiguous')) { + const stderr = gitError.stderr.toString(); + // Extract commit SHAs from git's hint output + // Format: "hint: 83c100e6accfb commit 2025-10-22 - message" + const commitMatch = stderr.match(/hint:\s+([a-f0-9]+)\s+commit\s+/); + if (commitMatch && commitMatch[1]) { + // Found the commit SHA from the ambiguous candidates + const candidateSha = commitMatch[1]; + // Verify this is a valid commit + try { + const fullSha = execSync(`git rev-parse ${candidateSha}`, { + encoding: 'utf-8', + cwd: repoRoot, + }).trim(); + if (fullSha && fullSha.length === 40) { + return fullSha; + } + } catch { + // Candidate didn't work, continue to GitHub API + } + } + } + // Commit not in local repo, try GitHub API + } + } catch (error) { + // Error setting up git command, try GitHub API + } + + try { + // Fallback to GitHub API + if (githubAuth.method === 'gh-cli') { + const result = execSync( + `gh api repos/backstage/backstage/commits/${shortSha} --jq '.sha' 2>/dev/null`, + { encoding: 'utf-8' }, + ).trim(); + return result || null; + } else { + // Use Octokit + const { data: commit } = await octokit.rest.repos.getCommit({ + owner: 'backstage', + repo: 'backstage', + ref: shortSha, + }); + return commit.sha; + } + } catch (error) { + // Commit not found or other error - return null silently + return null; + } +} + +/** + * Find PR number for a commit SHA using GitHub API + */ +async function findPRNumber(commitSha) { + // Check cache first + if (prCache.has(commitSha)) { + return prCache.get(commitSha); + } + + try { + // If SHA is truncated (7 chars), expand it first + let fullSha = commitSha; + if (commitSha.length === 7) { + fullSha = await expandCommitSha(commitSha); + if (!fullSha) { + // Commit not found + console.warn(`⚠️ Commit not found: ${commitSha}`); + prCache.set(commitSha, null); + return null; + } + } + + let prNumber = null; + + // Use gh CLI if available (faster, uses existing auth) + if (githubAuth.method === 'gh-cli') { + try { + const result = execSync( + `gh api repos/backstage/backstage/commits/${fullSha}/pulls --jq '.[0].number' 2>/dev/null`, + { encoding: 'utf-8' }, + ).trim(); + prNumber = result || null; + } catch (ghError) { + // Handle gh CLI errors gracefully - commit exists but has no PR + prNumber = null; + } + } else { + // Otherwise use Octokit (token or unauthenticated) + const { data: prs } = + await octokit.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: 'backstage', + repo: 'backstage', + commit_sha: fullSha, + }); + prNumber = prs.length > 0 ? prs[0].number.toString() : null; + } + + // Cache the result + prCache.set(commitSha, prNumber); + return prNumber; + } catch (error) { + if (error.message?.includes('rate limit')) { + console.error('⚠️ GitHub API rate limit exceeded'); + console.error( + ' Authenticate gh CLI or set GITHUB_TOKEN for higher limits', + ); + } else { + console.warn(`⚠️ Error finding PR for ${commitSha}: ${error.message}`); + } + prCache.set(commitSha, null); + return null; + } +} + +/** + * Main sync function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes('--dry-run'); + + const changelogTsPath = path.join(__dirname, '../src/utils/changelog.ts'); + const changelogMdPath = path.join( + __dirname, + '../../packages/ui/CHANGELOG.md', + ); + + // Validate files exist + if (!fs.existsSync(changelogTsPath)) { + throw new Error(`changelog.ts not found at: ${changelogTsPath}`); + } + if (!fs.existsSync(changelogMdPath)) { + throw new Error(`CHANGELOG.md not found at: ${changelogMdPath}`); + } + + console.log('📋 Syncing UI component changelogs...\n'); + + // Get last synced version + const lastVersion = getLastSyncedVersion(changelogTsPath); + console.log(`Last synced version: ${lastVersion || '(none)'}`); + + // Get valid components + const validComponents = getValidComponents(changelogTsPath); + console.log(`Valid components: ${validComponents.length}`); + + // Parse CHANGELOG.md + console.log('\n📖 Parsing CHANGELOG.md...'); + const allEntries = await parseChangelogMd( + changelogMdPath, + lastVersion, + validComponents, + ); + console.log(`Found ${allEntries.length} total entries since ${lastVersion}`); + + // Read existing changelog content for duplicate detection + const existingContent = fs.readFileSync(changelogTsPath, 'utf-8'); + + // Filter to only new, non-duplicate entries + const relevantEntries = allEntries.filter(e => { + const hasComponents = e.components.length > 0 || e.components.length === 0; + const notDuplicate = !isDuplicate(e, existingContent); + return hasComponents && notDuplicate; + }); + + const duplicatesCount = allEntries.length - relevantEntries.length; + if (duplicatesCount > 0) { + console.log(`Skipped ${duplicatesCount} duplicate entries`); + } + + console.log( + `Relevant entries (with or without components): ${relevantEntries.length}`, + ); + + if (relevantEntries.length === 0) { + console.log('\n✅ No new entries to sync'); + return; + } + + // Fetch PR numbers for new entries (lazy fetch - only for entries that will be written) + await fetchPRNumbers(relevantEntries, dryRun); + + // Show summary + console.log('\n📝 New entries by component:'); + const byComponent = {}; + relevantEntries.forEach(entry => { + if (entry.components.length === 0) { + byComponent['(general)'] = (byComponent['(general)'] || 0) + 1; + } else { + entry.components.forEach(comp => { + byComponent[comp] = (byComponent[comp] || 0) + 1; + }); + } + }); + Object.entries(byComponent).forEach(([comp, count]) => { + console.log(` - ${comp}: ${count} ${count === 1 ? 'entry' : 'entries'}`); + }); + + // Warn about unknown components + const unknownComponents = []; + allEntries.forEach(entry => { + const fullText = entry.description; + const componentMatch = fullText.match( + /Affected components?:[ \t]*([^\n]+)/i, + ); + if (componentMatch) { + const names = componentMatch[1].split(',').map(n => n.trim()); + names.forEach(name => { + if (!mapComponentName(name, validComponents)) { + unknownComponents.push(name); + } + }); + } + }); + + if (unknownComponents.length > 0) { + console.log('\n⚠️ Unknown components (skipped):'); + [...new Set(unknownComponents)].forEach(name => { + console.log(` - ${name}`); + }); + } + + // Create changelogs directory if it doesn't exist + const changelogsDir = path.join(__dirname, '../src/utils/changelogs'); + if (!fs.existsSync(changelogsDir) && !dryRun) { + fs.mkdirSync(changelogsDir, { recursive: true }); + } + + // Generate version files + console.log( + `\n${ + dryRun ? '🔍 Dry run - would generate' : '✍️ Generating' + } version files...`, + ); + + const versionFiles = await generateVersionFiles( + relevantEntries, + changelogsDir, + dryRun, + ); + + // Generate main changelog.ts + const mainContent = await generateMainChangelog( + versionFiles, + changelogTsPath, + dryRun, + ); + + if (!dryRun) { + console.log('\n✅ Changelog sync complete!'); + const newFiles = versionFiles.filter(vf => !vf.skipped).length; + const skippedFiles = versionFiles.filter(vf => vf.skipped).length; + if (newFiles > 0) { + console.log( + ` Generated ${newFiles} new version file${newFiles === 1 ? '' : 's'}`, + ); + } + if (skippedFiles > 0) { + console.log( + ` Preserved ${skippedFiles} existing version file${ + skippedFiles === 1 ? '' : 's' + }`, + ); + } + console.log(` Updated ${changelogTsPath}`); + } else { + console.log('\n📄 Sample version file content:\n'); + // Find first non-skipped version file for sample + const sampleFile = versionFiles.find(vf => !vf.skipped && vf.content); + if (sampleFile) { + const sample = sampleFile.content.split('\n').slice(0, 30).join('\n'); + console.log(sample); + console.log('...\n'); + } else { + console.log('(No new version files to generate)\n'); + } + + console.log('\n📄 Main changelog.ts content:\n'); + console.log(mainContent); + + const newFiles = versionFiles.filter(vf => !vf.skipped).length; + const skippedFiles = versionFiles.filter(vf => vf.skipped).length; + + console.log('\n✅ Dry run complete - no files were modified'); + if (newFiles > 0) { + console.log( + ` Run without --dry-run to generate ${newFiles} new version file${ + newFiles === 1 ? '' : 's' + }`, + ); + } + if (skippedFiles > 0) { + console.log( + ` ${skippedFiles} existing version file${ + skippedFiles === 1 ? '' : 's' + } will be preserved`, + ); + } + } +} + +// Run main if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(error => { + console.error('❌ Error:', error.message); + process.exit(1); + }); +} diff --git a/docs-ui/src/app/about/page.mdx b/docs-ui/src/app/about/page.mdx deleted file mode 100644 index 2c87a3d03e..0000000000 --- a/docs-ui/src/app/about/page.mdx +++ /dev/null @@ -1,32 +0,0 @@ -# About Backstage UI - -Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS. -This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI -is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users. - -## Philosophy - -Backstage empowers product teams to build software faster and with greater quality. Its extensibility, -however, required us to rethink how to deliver a consistent and accessible user experience. Our goal is -to enable plugin creators to design plugins that seamlessly integrate with Backstage's look and feel while -still allowing customization to match individual brands. - -Instead of reinventing the wheel, we chose to focus on layout and styling while leveraging existing headless -component libraries for functionality. This approach allows us to dedicate our efforts to creating a cohesive -and flexible theming system. - -## Team - -Backstage UI is designed and maintained primarily by Spotify's Backstage team, leveraging Spotify's expertise in -crafting high-quality design and technology. Drawing from our experience in building reliable and intuitive -user experiences for the music industry, we've created a design system that looks great and works seamlessly. - -## Community - -Backstage UI is an open-source project and we welcome contributions from the community. If you are interested in -contributing to Backstage UI, please read our [contributing guide](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) -and our [code of conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md). - -## License - -Backstage UI is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/backstage/backstage/blob/master/LICENSE) file for more details. diff --git a/docs-ui/src/app/components/[slug]/page.tsx b/docs-ui/src/app/components/[slug]/page.tsx deleted file mode 100644 index d6f98c8f93..0000000000 --- a/docs-ui/src/app/components/[slug]/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { components, layoutComponents } from '@/utils/data'; - -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - - const { default: Component } = await import( - `@/content/components/${slug}.mdx` - ); - - return ; -} - -export function generateStaticParams() { - const list = [...components, ...layoutComponents]; - - return list.map(component => ({ - slug: component.slug, - })); -} - -export const dynamicParams = false; diff --git a/docs-ui/src/app/components/accordion/components.tsx b/docs-ui/src/app/components/accordion/components.tsx new file mode 100644 index 0000000000..95b312d17c --- /dev/null +++ b/docs-ui/src/app/components/accordion/components.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { + Accordion, + AccordionTrigger, + AccordionPanel, + AccordionGroup, +} from '../../../../../packages/ui/src/components/Accordion/Accordion'; +import { Box } from '../../../../../packages/ui/src/components/Box/Box'; +import { Text } from '../../../../../packages/ui/src/components/Text/Text'; + +const Content = () => ( + + + It's the edge of the world and all of Western civilization + + + The sun may rise in the East, at least it settled in a final location + + + It's understood that Hollywood sells Californication + + +); + +export const Default = () => { + return ( + + + + + + + ); +}; + +export const WithSubtitle = () => { + return ( + + + + + + + ); +}; + +export const CustomTrigger = () => { + return ( + + + + + Custom Multi-line Trigger + + + Click to expand additional details and configuration options + + + + + + + + ); +}; + +export const DefaultExpanded = () => { + return ( + + + + + + + ); +}; + +export const GroupSingleOpen = () => { + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const GroupMultipleOpen = () => { + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/docs-ui/src/app/components/accordion/page.mdx b/docs-ui/src/app/components/accordion/page.mdx new file mode 100644 index 0000000000..83d650507b --- /dev/null +++ b/docs-ui/src/app/components/accordion/page.mdx @@ -0,0 +1,139 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { + accordionPropDefs, + accordionTriggerPropDefs, + accordionPanelPropDefs, + accordionGroupPropDefs, +} from './props-definition'; +import { + accordionUsageSnippet, + defaultSnippet, + withSubtitleSnippet, + customTriggerSnippet, + defaultExpandedSnippet, + groupSingleOpenSnippet, + groupMultipleOpenSnippet, +} from './snippets'; +import { + Default, + WithSubtitle, + CustomTrigger, + DefaultExpanded, + GroupSingleOpen, + GroupMultipleOpen, +} from './components'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { AccordionDefinition } from '../../../utils/definitions'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; + + + +} + code={defaultSnippet} +/> + +## Usage + + + +## API reference + +### Accordion + +Root container for the accordion. Renders a `
` element. + + + +### AccordionTrigger + +Trigger component with built-in animated chevron icon. Renders a heading element (defaults to `

`, configurable via `level` prop) wrapping a ` + + + + ); +}; + +export const Sizes = () => { + return ( + + + + + ); +}; + +export const WithIcons = () => { + return ( + + + + + + ); +}; + +export const Disabled = () => { + return ( + + + + + + ); +}; + +export const Loading = () => { + return ( + + ); +}; + +export const AsLink = () => { + return ( + + + Button + + + ); +}; diff --git a/docs-ui/src/content/components/button.mdx b/docs-ui/src/app/components/button/page.mdx similarity index 58% rename from docs-ui/src/content/components/button.mdx rename to docs-ui/src/app/components/button/page.mdx index a25115012b..38ff2e2d57 100644 --- a/docs-ui/src/content/components/button.mdx +++ b/docs-ui/src/app/components/button/page.mdx @@ -1,32 +1,36 @@ import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; -import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets'; +import { buttonPropDefs } from './props-definition'; import { - buttonPropDefs, + variantsSnippet, + sizesSnippet, + withIconsSnippet, + disabledSnippet, + loadingSnippet, + asLinkSnippet, buttonSnippetUsage, - buttonVariantsSnippet, - buttonSizesSnippet, - buttonIconsSnippet, - buttonDisabledSnippet, buttonResponsiveSnippet, - buttonAsLinkSnippet, -} from './button.props'; +} from './snippets'; import { ChangelogComponent } from '@/components/ChangelogComponent'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; +import { ButtonDefinition } from '../../../utils/definitions'; +import { + Variants, + Sizes, + WithIcons, + Disabled, + Loading, + AsLink, +} from './components'; -} - code={buttonVariantsSnippet} -/> +} code={variantsSnippet} /> ## Usage @@ -46,21 +50,15 @@ Here's a view when buttons have different variants. align="center" py={4} open - preview={} - code={buttonVariantsSnippet} + preview={} + code={variantsSnippet} /> ### Sizes Here's a view when buttons have different sizes. -} - code={buttonSizesSnippet} -/> +} code={sizesSnippet} /> ### With Icons @@ -70,8 +68,8 @@ Here's a view when buttons have icons. align="center" py={4} open - preview={} - code={buttonIconsSnippet} + preview={} + code={withIconsSnippet} /> ### Disabled @@ -82,8 +80,20 @@ Here's a view when buttons are disabled. align="center" py={4} open - preview={} - code={buttonDisabledSnippet} + preview={} + code={disabledSnippet} +/> + +### Loading + +Here's a view when buttons are in a loading state. + +} + code={loadingSnippet} /> ### Responsive @@ -96,14 +106,8 @@ Here's a view when buttons are responsive. If you want to use a button as a link, please use the `ButtonLink` component. -} - code={buttonAsLinkSnippet} -/> +} code={asLinkSnippet} /> - + diff --git a/docs-ui/src/app/components/button/props-definition.ts b/docs-ui/src/app/components/button/props-definition.ts new file mode 100644 index 0000000000..4b225ad460 --- /dev/null +++ b/docs-ui/src/app/components/button/props-definition.ts @@ -0,0 +1,30 @@ +import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs'; +import type { PropDef } from '@/utils/propDefs'; + +export const buttonPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'medium', + responsive: true, + }, + iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, + iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, + loading: { type: 'boolean', default: 'false', responsive: false }, + children: { type: 'enum', values: ['ReactNode'], responsive: false }, + type: { + type: 'enum', + values: ['button', 'submit', 'reset'], + default: 'button', + responsive: false, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/docs-ui/src/app/components/button/snippets.ts b/docs-ui/src/app/components/button/snippets.ts new file mode 100644 index 0000000000..a31523b064 --- /dev/null +++ b/docs-ui/src/app/components/button/snippets.ts @@ -0,0 +1,54 @@ +export const buttonSnippetUsage = `import { Button } from '@backstage/ui'; + +`; + +export const variantsSnippet = ` + + + + `; + +export const sizesSnippet = ` + + + `; + +export const withIconsSnippet = ` + + + + `; + +export const disabledSnippet = ` + + + + `; + +export const loadingSnippet = ``; + +export const asLinkSnippet = ` + + Button + + `; diff --git a/docs-ui/src/app/components/card/components.tsx b/docs-ui/src/app/components/card/components.tsx new file mode 100644 index 0000000000..08fa940be8 --- /dev/null +++ b/docs-ui/src/app/components/card/components.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { + Card, + CardHeader, + CardBody, + CardFooter, +} from '../../../../../packages/ui/src/components/Card/Card'; +import { Text } from '../../../../../packages/ui/src/components/Text/Text'; + +export const Default = () => { + return ( + + Header + Body + Footer + + ); +}; + +export const CustomSize = () => { + return ( + + Header + Body + Footer + + ); +}; + +export const WithLongBody = () => { + return ( + + + Header + + + + This is the first paragraph of a long body text that demonstrates how + the Card component handles extensive content. The card should adjust + accordingly to display all the text properly while maintaining its + structure. + + + Here's a second paragraph that adds more content to our card + body. Having multiple paragraphs helps to visualize how spacing works + within the card component. + + + This third paragraph continues to add more text to ensure we have a + proper demonstration of a card with significant content. This makes it + easier to test scrolling behavior and overall layout when content + exceeds the initial view. + + + + Footer + + + ); +}; diff --git a/docs-ui/src/content/components/card.mdx b/docs-ui/src/app/components/card/page.mdx similarity index 72% rename from docs-ui/src/content/components/card.mdx rename to docs-ui/src/app/components/card/page.mdx index 06c3a5bc2e..e0b6e81fff 100644 --- a/docs-ui/src/content/components/card.mdx +++ b/docs-ui/src/app/components/card/page.mdx @@ -1,19 +1,22 @@ import { PropsTable } from '@/components/PropsTable'; -import { CardSnippet } from '@/snippets/stories-snippets'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; import { cardPropDefs, - cardUsageSnippet, - cardDefaultSnippet, cardHeaderPropDefs, cardBodyPropDefs, cardFooterPropDefs, - cardLongBodySnippet, - cardListRowSnippet, -} from './card.props'; +} from './props-definition'; +import { + cardUsageSnippet, + defaultSnippet, + customSizeSnippet, + withLongBodySnippet, +} from './snippets'; +import { Default, CustomSize, WithLongBody } from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; +import { CardDefinition } from '../../../utils/definitions'; import { ChangelogComponent } from '@/components/ChangelogComponent'; -} - code={cardDefaultSnippet} -/> +} code={defaultSnippet} /> ## Usage @@ -60,6 +58,18 @@ To display a footer in a card, use the `CardFooter` component. This will be fixe ## Examples +### Custom size + +Here's a view when card has a custom size. + +} + code={customSizeSnippet} + open +/> + ### With long body Here's a view when card has a long body. @@ -67,23 +77,11 @@ Here's a view when card has a long body. } - code={cardLongBodySnippet} + preview={} + code={withLongBodySnippet} open /> -### With list - -Here's a view when card has a list. - -} - code={cardListRowSnippet} - open -/> - - + diff --git a/docs-ui/src/app/components/card/props-definition.ts b/docs-ui/src/app/components/card/props-definition.ts new file mode 100644 index 0000000000..bb7bdad0dd --- /dev/null +++ b/docs-ui/src/app/components/card/props-definition.ts @@ -0,0 +1,30 @@ +import { + classNamePropDefs, + stylePropDefs, + childrenPropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const cardPropDefs: Record = { + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const cardHeaderPropDefs: Record = { + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const cardBodyPropDefs: Record = { + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const cardFooterPropDefs: Record = { + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/docs-ui/src/app/components/card/snippets.ts b/docs-ui/src/app/components/card/snippets.ts new file mode 100644 index 0000000000..b5de9a7008 --- /dev/null +++ b/docs-ui/src/app/components/card/snippets.ts @@ -0,0 +1,41 @@ +export const cardUsageSnippet = `import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui'; + + + Header + Body + Footer +`; + +export const defaultSnippet = ` + Header + Body + Footer +`; + +export const customSizeSnippet = ` + Header + Body + Footer +`; + +export const withLongBodySnippet = ` + + Header + + + + This is the first paragraph of a long body text that demonstrates how + the Card component handles extensive content. + + + Here's a second paragraph that adds more content to our card body. + + + This third paragraph continues to add more text to ensure we have a + proper demonstration of a card with significant content. + + + + Footer + +`; diff --git a/docs-ui/src/app/components/checkbox/components.tsx b/docs-ui/src/app/components/checkbox/components.tsx new file mode 100644 index 0000000000..a826c9e02f --- /dev/null +++ b/docs-ui/src/app/components/checkbox/components.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { Checkbox } from '../../../../../packages/ui/src/components/Checkbox/Checkbox'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; + +export const Default = () => { + return Accept terms and conditions; +}; + +export const AllVariants = () => { + return ( + + Unchecked + Checked + Disabled + + Checked & Disabled + + + ); +}; diff --git a/docs-ui/src/content/components/checkbox.mdx b/docs-ui/src/app/components/checkbox/page.mdx similarity index 64% rename from docs-ui/src/content/components/checkbox.mdx rename to docs-ui/src/app/components/checkbox/page.mdx index 5770396ebb..aaf73d7210 100644 --- a/docs-ui/src/content/components/checkbox.mdx +++ b/docs-ui/src/app/components/checkbox/page.mdx @@ -1,15 +1,16 @@ import { PropsTable } from '@/components/PropsTable'; -import { CheckboxSnippet } from '@/snippets/stories-snippets'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; +import { checkboxPropDefs } from './props-definition'; import { - checkboxPropDefs, checkboxUsageSnippet, - checkboxDefaultSnippet, - checkboxVariantsSnippet, -} from './checkbox.props'; + defaultSnippet, + allVariantsSnippet, +} from './snippets'; +import { Default, AllVariants } from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; +import { CheckboxDefinition } from '../../../utils/definitions'; import { ChangelogComponent } from '@/components/ChangelogComponent'; -} - code={checkboxDefaultSnippet} -/> +} code={defaultSnippet} /> ## Usage @@ -42,10 +38,10 @@ Here's a view when checkboxes have different variants. align="center" py={4} open - preview={} - code={checkboxVariantsSnippet} + preview={} + code={allVariantsSnippet} /> - + diff --git a/docs-ui/src/content/components/checkbox.props.ts b/docs-ui/src/app/components/checkbox/props-definition.ts similarity index 64% rename from docs-ui/src/content/components/checkbox.props.ts rename to docs-ui/src/app/components/checkbox/props-definition.ts index ce0f1033ff..9e5d8a1e16 100644 --- a/docs-ui/src/content/components/checkbox.props.ts +++ b/docs-ui/src/app/components/checkbox/props-definition.ts @@ -46,16 +46,3 @@ export const checkboxPropDefs: Record = { ...classNamePropDefs, ...stylePropDefs, }; - -export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui'; - -Accept terms`; - -export const checkboxDefaultSnippet = `Accept terms and conditions`; - -export const checkboxVariantsSnippet = ` - Unchecked - Checked - Disabled - Checked & Disabled -`; diff --git a/docs-ui/src/app/components/checkbox/snippets.ts b/docs-ui/src/app/components/checkbox/snippets.ts new file mode 100644 index 0000000000..bef496195f --- /dev/null +++ b/docs-ui/src/app/components/checkbox/snippets.ts @@ -0,0 +1,12 @@ +export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui'; + +Accept terms`; + +export const defaultSnippet = `Accept terms and conditions`; + +export const allVariantsSnippet = ` + Unchecked + Checked + Disabled + Checked & Disabled +`; diff --git a/docs-ui/src/app/components/container/components.tsx b/docs-ui/src/app/components/container/components.tsx new file mode 100644 index 0000000000..7032b3f620 --- /dev/null +++ b/docs-ui/src/app/components/container/components.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Box } from '../../../../../packages/ui/src/components/Box/Box'; + +const DecorativeBox = () => ( + +); + +export const Preview = () => { + return ( +
+ +
+ ); +}; diff --git a/docs-ui/src/content/components/container.mdx b/docs-ui/src/app/components/container/page.mdx similarity index 77% rename from docs-ui/src/content/components/container.mdx rename to docs-ui/src/app/components/container/page.mdx index 05615cb1c2..87bc55829e 100644 --- a/docs-ui/src/content/components/container.mdx +++ b/docs-ui/src/app/components/container/page.mdx @@ -1,16 +1,17 @@ import { CodeBlock } from '@/components/CodeBlock'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; -import { ContainerSnippet } from '@/snippets/stories-snippets'; +import { containerPropDefs } from './props-definition'; import { - containerPropDefs, containerUsageSnippet, - containerDefaultSnippet, + previewSnippet, containerSimpleSnippet, containerResponsiveSnippet, -} from './container.props'; +} from './snippets'; +import { Preview } from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; +import { ContainerDefinition } from '../../../utils/definitions'; import { ChangelogComponent } from '@/components/ChangelogComponent'; -} - code={containerDefaultSnippet} -/> +} code={previewSnippet} /> ## Usage @@ -48,6 +45,6 @@ create responsive designs. - + diff --git a/docs-ui/src/app/components/container/props-definition.ts b/docs-ui/src/app/components/container/props-definition.ts new file mode 100644 index 0000000000..0980520365 --- /dev/null +++ b/docs-ui/src/app/components/container/props-definition.ts @@ -0,0 +1,12 @@ +import { + classNamePropDefs, + stylePropDefs, + gapPropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const containerPropDefs: Record = { + ...gapPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; diff --git a/docs-ui/src/content/components/container.props.ts b/docs-ui/src/app/components/container/snippets.ts similarity index 60% rename from docs-ui/src/content/components/container.props.ts rename to docs-ui/src/app/components/container/snippets.ts index a4d23a8495..83f4dabbbc 100644 --- a/docs-ui/src/content/components/container.props.ts +++ b/docs-ui/src/app/components/container/snippets.ts @@ -1,23 +1,10 @@ -import { - classNamePropDefs, - stylePropDefs, - gapPropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const containerPropDefs: Record = { - ...gapPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - export const containerUsageSnippet = `import { Container } from "@backstage/ui"; Hello World!`; -export const containerDefaultSnippet = ` +export const previewSnippet = `
-`; +
`; export const containerSimpleSnippet = ` Hello World diff --git a/docs-ui/src/app/components/dialog/components.tsx b/docs-ui/src/app/components/dialog/components.tsx new file mode 100644 index 0000000000..3a1fd8c2c9 --- /dev/null +++ b/docs-ui/src/app/components/dialog/components.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { + Dialog, + DialogTrigger, + DialogHeader, + DialogBody, + DialogFooter, +} from '../../../../../packages/ui/src/components/Dialog/Dialog'; +import { Button } from '../../../../../packages/ui/src/components/Button/Button'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; +import { Text } from '../../../../../packages/ui/src/components/Text/Text'; +import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField'; +import { Select } from '../../../../../packages/ui/src/components/Select/Select'; + +export const Default = () => ( + + + + Example Dialog + + This is a basic dialog example. + + + + + + + +); + +export const PreviewFixedWidthAndHeight = () => ( + + + + Long Content Dialog + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. + + + Sed ut perspiciatis unde omnis iste natus error sit voluptatem + accusantium doloremque laudantium, totam rem aperiam, eaque ipsa + quae ab illo inventore veritatis et quasi architecto beatae vitae + dicta sunt explicabo. + + + + + + + + + +); + +export const PreviewWithForm = () => ( + + + + Create New User + + + + + +); + +export const WithLabelAndDescription = () => ( + + } + style={{ width: 300 }} + /> +); + +export const Disabled = () => ( + +); + +export const Searchable = () => ( + +); + +export const SearchableMultiple = () => ( + `; + +export const selectDefaultSnippet = ``; + +export const selectIconSnippet = ` + `; + +export const selectResponsiveSnippet = ``; + +export const selectMultipleSnippet = ``; + +export const selectDisabledOptionsSnippet = ` + + + + {themes.map(({ name, value }) => ( + + {name} + + ))} + + + + + Version {version} + + + + + + + + + + +

); }; diff --git a/docs-ui/src/components/Toolbar/theme-name.module.css b/docs-ui/src/components/Toolbar/theme-name.module.css deleted file mode 100644 index 1e1943b886..0000000000 --- a/docs-ui/src/components/Toolbar/theme-name.module.css +++ /dev/null @@ -1,110 +0,0 @@ -.Select { - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - height: 3rem; - padding-left: 1.25rem; - padding-right: 1rem; - margin: 0; - outline: 0; - border: 1px solid var(--color-gray-200); - border-radius: 3rem; - font-family: inherit; - font-size: 1rem; - line-height: 1.5rem; - color: var(--color-gray-900); - cursor: pointer; - user-select: none; - background-color: var(--action); - - &:focus-visible { - outline: 2px solid var(--color-blue); - outline-offset: -1px; - } -} - -.Popup { - box-sizing: border-box; - padding-block: 0.25rem; - border-radius: 0.375rem; - background-color: var(--panel); - color: var(--color-gray-900); - border: 1px solid var(--border); - padding-inline: 0.25rem; - transform-origin: var(--transform-origin); - transition: transform 150ms, opacity 150ms; - - &[data-starting-style], - &[data-ending-style] { - opacity: 0; - transform: scale(0.9); - } - - &[data-side='none'] { - transition: none; - transform: none; - opacity: 1; - } - - @media (prefers-color-scheme: light) { - outline: 1px solid var(--color-gray-200); - box-shadow: 0px 10px 15px -3px var(--color-gray-200), - 0px 4px 6px -4px var(--color-gray-200); - } - - @media (prefers-color-scheme: dark) { - outline: 1px solid var(--color-gray-300); - outline-offset: -1px; - } -} - -.Popup[data-trigger='Select'] { - min-width: var(--trigger-width); - - .ListBox { - display: block; - width: unset; - max-height: inherit; - min-height: unset; - border: none; - - .react-aria-Header { - padding-left: 1.571rem; - } - } - - .Item { - position: relative; - padding: 0 0.571rem 0 1.571rem; - height: 2rem; - display: flex; - align-items: center; - - &[data-focus-visible] { - outline: none; - } - - &[data-selected] { - font-weight: 600; - background: unset; - color: var(--text-color); - - &::before { - content: '✓'; - content: '✓' / ''; - alt: ' '; - position: absolute; - left: 4px; - } - } - - &[data-focused], - &[data-pressed] { - background: var(--bg); - color: var(--primary); - cursor: pointer; - } - } -} diff --git a/docs-ui/src/components/Toolbar/theme-name.tsx b/docs-ui/src/components/Toolbar/theme-name.tsx deleted file mode 100644 index ae9e7f9f35..0000000000 --- a/docs-ui/src/components/Toolbar/theme-name.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { - Button, - ListBox, - ListBoxItem, - Popover, - Select, - SelectValue, -} from 'react-aria-components'; -import styles from './theme-name.module.css'; -import { usePlayground } from '@/utils/playground-context'; -import { RiArrowDownSLine } from '@remixicon/react'; - -const themes = [ - { name: 'Backstage', value: 'backstage' }, - { name: 'Spotify', value: 'spotify' }, - { name: 'Custom theme', value: 'custom' }, -]; - -export const ThemeNameSelector = () => { - const { selectedThemeName, setSelectedThemeName } = usePlayground(); - - return ( - - ); -}; diff --git a/docs-ui/src/components/Toolbar/theme.module.css b/docs-ui/src/components/Toolbar/theme.module.css deleted file mode 100644 index c5490a72d7..0000000000 --- a/docs-ui/src/components/Toolbar/theme.module.css +++ /dev/null @@ -1,79 +0,0 @@ -.tabs { - border-radius: 0.375rem; - width: 100%; -} - -.tabsTheme { - width: 100px; - border-radius: 0.375rem; -} - -.list { - display: flex; - position: relative; - z-index: 0; - gap: 0.25rem; -} - -.tab { - display: flex; - align-items: center; - justify-content: center; - border: 0; - margin: 0; - outline: 0; - background: none; - appearance: none; - color: var(--secondary); - user-select: none; - flex: 1; - cursor: pointer; - width: 3rem; - height: 3rem; - - &[data-selected] { - color: var(--primary); - - & p { - color: var(--primary); - } - } - - @media (hover: hover) { - &:hover { - color: var(--primary); - } - } - - &:focus-visible { - position: relative; - - &::before { - content: ''; - position: absolute; - inset: 0.25rem 0; - border-radius: 0.25rem; - outline: 2px solid var(--action); - outline-offset: -1px; - } - } -} - -.tab p { - color: var(--secondary) !important; -} - -.indicator { - position: absolute; - z-index: -1; - left: 0; - top: 50%; - translate: var(--active-tab-left) -50%; - width: var(--active-tab-width); - height: 3rem; - border-radius: 3rem; - background-color: var(--action); - transition-property: translate, width; - transition-duration: 200ms; - transition-timing-function: ease-in-out; -} diff --git a/docs-ui/src/components/Toolbar/theme.tsx b/docs-ui/src/components/Toolbar/theme.tsx deleted file mode 100644 index 7ece7bfe7b..0000000000 --- a/docs-ui/src/components/Toolbar/theme.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { Tabs } from '@base-ui-components/react/tabs'; -import { usePlayground } from '@/utils/playground-context'; -import styles from './theme.module.css'; -import { RiMoonLine, RiSunLine } from '@remixicon/react'; - -export const ThemeSelector = () => { - const { selectedTheme, setSelectedTheme } = usePlayground(); - - return ( - - - - - - - - - - ); -}; diff --git a/docs-ui/src/content/components/avatar.mdx b/docs-ui/src/content/components/avatar.mdx deleted file mode 100644 index be1fb070d1..0000000000 --- a/docs-ui/src/content/components/avatar.mdx +++ /dev/null @@ -1,63 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { AvatarSnippet } from '@/snippets/stories-snippets'; -import { - avatarPropDefs, - snippetUsage, - snippetSizes, - snippetFallback, -} from './avatar.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={``} -/> - -## Usage - - - -## API reference - - - -## Examples - -### Sizes - -Avatar sizes can be set using the `size` prop. - -} - code={snippetSizes} -/> - -### Fallback - -If the image is not available, the avatar will show the initials of the name. - -} - code={snippetFallback} -/> - - - - diff --git a/docs-ui/src/content/components/avatar.props.ts b/docs-ui/src/content/components/avatar.props.ts deleted file mode 100644 index 8cc53eeff3..0000000000 --- a/docs-ui/src/content/components/avatar.props.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs'; -import type { PropDef } from '@/utils/propDefs'; - -export const avatarPropDefs: Record = { - src: { - type: 'string', - }, - name: { - type: 'string', - }, - size: { - type: 'enum', - values: ['small', 'medium', 'large'], - default: 'medium', - responsive: true, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const snippetUsage = `import { Avatar } from '@backstage/ui'; - -`; - -export const snippetSizes = ` - - - -`; - -export const snippetFallback = ``; diff --git a/docs-ui/src/content/components/button-icon.props.ts b/docs-ui/src/content/components/button-icon.props.ts deleted file mode 100644 index 2ba7372a9c..0000000000 --- a/docs-ui/src/content/components/button-icon.props.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const buttonIconPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - icon: { type: 'enum', values: ['ReactNode'], responsive: false }, - isDisabled: { type: 'boolean', default: 'false', responsive: false }, - type: { - type: 'enum', - values: ['button', 'submit', 'reset'], - default: 'button', - responsive: false, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui'; - -`; - -export const buttonIconDefaultSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const buttonIconVariantsSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const buttonIconSizesSnippet = ` - } size="small" /> - } size="medium" /> -`; - -export const buttonIconDisabledSnippet = `} isDisabled />`; - -export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; - -export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/ui'; - - - Button -`; diff --git a/docs-ui/src/content/components/button-link.mdx b/docs-ui/src/content/components/button-link.mdx deleted file mode 100644 index 5ce538838d..0000000000 --- a/docs-ui/src/content/components/button-link.mdx +++ /dev/null @@ -1,96 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { ButtonLinkSnippet } from '@/snippets/stories-snippets'; -import { - buttonLinkPropDefs, - buttonLinkSnippetUsage, - buttonLinkVariantsSnippet, - buttonLinkSizesSnippet, - buttonLinkIconsSnippet, - buttonLinkDisabledSnippet, - buttonLinkResponsiveSnippet, -} from './button-link.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={buttonLinkVariantsSnippet} -/> - -## Usage - - - -## API reference - - - -## Examples - -### Variants - -Here's a view when buttons have different variants. - -} - code={buttonLinkVariantsSnippet} -/> - -### Sizes - -Here's a view when buttons have different sizes. - -} - code={buttonLinkSizesSnippet} -/> - -### With Icons - -Here's a view when buttons have icons. - -} - code={buttonLinkIconsSnippet} -/> - -### Disabled - -Here's a view when buttons are disabled. - -} - code={buttonLinkDisabledSnippet} -/> - -### Responsive - -Here's a view when buttons are responsive. - - - - - - diff --git a/docs-ui/src/content/components/button-link.props.ts b/docs-ui/src/content/components/button-link.props.ts deleted file mode 100644 index 758d6fd811..0000000000 --- a/docs-ui/src/content/components/button-link.props.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs'; -import type { PropDef } from '../../utils/propDefs'; - -export const buttonLinkPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, - iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, - isDisabled: { type: 'boolean', default: 'false', responsive: false }, - href: { type: 'string', responsive: false }, - hrefLang: { type: 'string', responsive: false }, - target: { - type: 'enum', - values: ['HTMLAttributeAnchorTarget'], - default: '_self', - responsive: false, - }, - rel: { type: 'string', responsive: false }, - children: { type: 'enum', values: ['ReactNode'], responsive: false }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ui'; - -`; - -export const buttonLinkVariantsSnippet = ` - } variant="primary"> - Button - - } variant="secondary"> - Button - -`; - -export const buttonLinkSizesSnippet = ` - Small - Medium -`; - -export const buttonLinkIconsSnippet = ` - }>Button - }>Button - } - iconEnd={}> - Button - -`; - -export const buttonLinkDisabledSnippet = ` - - Primary - - - Secondary - -`; - -export const buttonLinkResponsiveSnippet = ` - Responsive Button -`; diff --git a/docs-ui/src/content/components/button.props.ts b/docs-ui/src/content/components/button.props.ts deleted file mode 100644 index 4647c20956..0000000000 --- a/docs-ui/src/content/components/button.props.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs'; -import type { PropDef } from '../../utils/propDefs'; - -export const buttonPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, - iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, - isDisabled: { type: 'boolean', default: 'false', responsive: false }, - children: { type: 'enum', values: ['ReactNode'], responsive: false }, - type: { - type: 'enum', - values: ['button', 'submit', 'reset'], - default: 'button', - responsive: false, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const buttonSnippetUsage = `import { Button } from '@backstage/ui'; - - - -`; - -export const buttonSizesSnippet = ` - - -`; - -export const buttonIconsSnippet = ` - - - -`; - -export const buttonDisabledSnippet = ` - - -`; - -export const buttonResponsiveSnippet = ``; - -export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/ui'; - - - Button -`; diff --git a/docs-ui/src/content/components/card.props.ts b/docs-ui/src/content/components/card.props.ts deleted file mode 100644 index 70c71bdf56..0000000000 --- a/docs-ui/src/content/components/card.props.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const cardPropDefs: Record = { - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const cardHeaderPropDefs: Record = { - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const cardBodyPropDefs: Record = { - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const cardFooterPropDefs: Record = { - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const cardUsageSnippet = `import { card } from '@backstage/ui'; - - - Header - Body - Footer -`; - -export const cardDefaultSnippet = ` - Header - Body - Footer -`; - -export const cardLongBodySnippet = ` - - Header - - - - This is the first paragraph of a long body text that demonstrates how - the Card component handles extensive content. The card should adjust - accordingly to display all the text properly while maintaining its - structure. - - - Here's a second paragraph that adds more content to our card body. - Having multiple paragraphs helps to visualize how spacing works within - the card component. - - - This third paragraph continues to add more text to ensure we have a - proper demonstration of a card with significant content. This makes it - easier to test scrolling behavior and overall layout when content - exceeds the initial view. - - - - Footer - -`; - -export const cardListRowSnippet = ` - - Header - - - Hello world - Hello world - Hello world - Hello world - ... - - - Footer - -`; diff --git a/docs-ui/src/content/components/collapsible.mdx b/docs-ui/src/content/components/collapsible.mdx deleted file mode 100644 index 948da323fe..0000000000 --- a/docs-ui/src/content/components/collapsible.mdx +++ /dev/null @@ -1,72 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { CollapsibleSnippet } from '@/snippets/stories-snippets'; -import { - collapsibleRootPropDefs, - collapsibleTriggerPropDefs, - collapsiblePanelPropDefs, - collapsibleUsageSnippet, - collapsibleDefaultSnippet, - collapsibleTriggerSnippet, - collapsibleOpenSnippet, -} from './collapsible.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={collapsibleDefaultSnippet} -/> - -## Usage - - - -## API reference - -### Collapsible.Root - -Groups all parts of the collapsible. Renders a `
` element. - - - -### Collapsible.Trigger - -The trigger by default render a simple unstyled button. Because menus can be rendered in different ways, we recommend -using the `render` prop to render a custom trigger. - - - - - -### Collapsible.Panel - -A panel with the collapsible contents. Renders a `
` element. - - - -## Examples - -Open the panel by default by setting the `defaultOpen` prop to `true`. - -} - code={collapsibleOpenSnippet} -/> - - - - diff --git a/docs-ui/src/content/components/collapsible.props.ts b/docs-ui/src/content/components/collapsible.props.ts deleted file mode 100644 index 55e80f6689..0000000000 --- a/docs-ui/src/content/components/collapsible.props.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - renderPropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const collapsibleRootPropDefs: Record = { - defaultOpen: { - type: 'boolean', - default: 'false', - }, - open: { - type: 'boolean', - }, - onOpenChange: { - type: 'enum', - values: ['(open) => void'], - }, - ...renderPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const collapsibleTriggerPropDefs: Record = { - ...renderPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const collapsiblePanelPropDefs: Record = { - hiddenUntilFound: { - type: 'boolean', - default: 'false', - }, - keepMounted: { - type: 'boolean', - default: 'false', - }, - ...renderPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const collapsibleUsageSnippet = `import { Collapsible } from '@backstage/ui'; - - - ( - - )} /> - Your content -`; - -export const collapsibleDefaultSnippet = ` - ( - - )} /> - - - It's the edge of the world and all of Western civilization - The sun may rise in the East, at least it settled in a final location - It's understood that Hollywood sells Californication - - -`; - -export const collapsibleTriggerSnippet = ` - )} /> - - - It's the edge of the world and all of Western civilization - The sun may rise in the East, at least it settled in a final location - It's understood that Hollywood sells Californication - - -`; diff --git a/docs-ui/src/content/components/flex.props.ts b/docs-ui/src/content/components/flex.props.ts deleted file mode 100644 index cc4c2d5a75..0000000000 --- a/docs-ui/src/content/components/flex.props.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - childrenPropDefs, - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const flexPropDefs: Record = { - align: { - type: 'enum', - values: ['start', 'center', 'end', 'baseline', 'stretch'], - responsive: true, - }, - direction: { - type: 'enum', - values: ['row', 'column', 'row-reverse', 'column-reverse'], - responsive: true, - }, - justify: { - type: 'enum', - values: ['start', 'center', 'end', 'between'], - responsive: true, - }, - ...childrenPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const flexUsageSnippet = `import { Flex } from '@backstage/ui'; - -`; - -export const flexDefaultSnippet = ` - - - -`; - -export const flexSimpleSnippet = ` - Hello World - Hello World - Hello World -`; - -export const flexResponsiveSnippet = ` - Hello World - Hello World - Hello World -`; - -export const flexAlignSnippet = ` - Hello World - Hello World - Hello World -`; diff --git a/docs-ui/src/content/components/grid.mdx b/docs-ui/src/content/components/grid.mdx deleted file mode 100644 index 0ffdba6a66..0000000000 --- a/docs-ui/src/content/components/grid.mdx +++ /dev/null @@ -1,98 +0,0 @@ -import { CodeBlock } from '@/components/CodeBlock'; -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { GridSnippet } from '@/snippets/stories-snippets'; -import { spacingPropDefs } from '@/utils/propDefs'; -import { - gridPropDefs, - gridItemPropDefs, - gridUsageSnippet, - gridDefaultSnippet, - gridSimpleSnippet, - gridComplexSnippet, - gridMixingRowsSnippet, - gridResponsiveSnippet, - gridStartEndSnippet, -} from './grid.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={gridDefaultSnippet} -/> - -## Usage - - - -## API reference - -### Grid.Root - -This is the grid container component. It will help to define the number of -columns that will be used in the grid. You can also define the gap between the -columns. All values are responsive. - - - -The grid component also accepts all the spacing props from the Box component. - - - -### Grid.Item - -If you need more control over the columns, you can use the grid item -component. This will give you access to `rowSpan`, `colSpan`, `start` and -`end`. All values are responsive. This component is optional, you can use any -elements directly if you prefer. - - - -## Examples - -### Simple grid - -A simple grid with 3 columns and a gap of md. - - - -### Complex grid - -You can also use the grid item to create more complex layouts. In this example -the first column will span 1 column and the second column will span 2 columns. - - - -### Mixing rows and columns - -The grid item component also supports the `rowSpan` prop, which allows you to -span multiple rows within the grid layout. In this example, the first item -will span 2 rows to achieve a dynamic and flexible grid structure. - - - -### Responsive grid - -The grid component also supports responsive values, making it easy to create -responsive designs. - - - -### Start and End - -The start and end props can be used to position the item in the grid. - - - - - - diff --git a/docs-ui/src/content/components/grid.props.ts b/docs-ui/src/content/components/grid.props.ts deleted file mode 100644 index 6f037cd64f..0000000000 --- a/docs-ui/src/content/components/grid.props.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - childrenPropDefs, - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -const columnsValues = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', -]; - -export const gridPropDefs: Record = { - columns: { - type: 'enum | string', - values: [...columnsValues, 'auto'], - responsive: true, - default: 'auto', - }, - ...childrenPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const gridItemPropDefs: Record = { - colSpan: { - type: 'enum | string', - values: [...columnsValues, 'full'], - responsive: true, - }, - rowSpan: { - type: 'enum | string', - values: [...columnsValues, 'full'], - responsive: true, - }, - colStart: { - type: 'enum | string', - values: [...columnsValues, 'auto'], - responsive: true, - }, - colEnd: { - type: 'enum | string', - values: [...columnsValues, 'auto'], - responsive: true, - }, - ...childrenPropDefs, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const gridUsageSnippet = `import { Grid } from '@backstage/ui'; - -`; - -export const gridDefaultSnippet = ` - - - -`; - -export const gridSimpleSnippet = ` - Hello World - Hello World - Hello World -`; - -export const gridComplexSnippet = ` - - Hello World - - - Hello World - -`; - -export const gridMixingRowsSnippet = ` - - Hello World - - - Hello World - - - Hello World - -`; - -export const gridResponsiveSnippet = ` - - - - Hello World - -`; - -export const gridStartEndSnippet = ` - - Hello World - -`; diff --git a/docs-ui/src/content/components/link.mdx b/docs-ui/src/content/components/link.mdx deleted file mode 100644 index e0c1be353f..0000000000 --- a/docs-ui/src/content/components/link.mdx +++ /dev/null @@ -1,102 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { - MenuSnippet, - ButtonSnippet, - LinkSnippet, -} from '@/snippets/stories-snippets'; -import { - linkPropDefs, - linkUsageSnippet, - linkDefaultSnippet, - linkVariantsSnippet, - linkWeightsSnippet, - linkColorsSnippet, - linkRouterSnippet, - linkTruncateSnippet, -} from './link.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={linkDefaultSnippet} -/> - -## Usage - - - -## API reference - - - -## Router Integration - -The `Link` component handles both internal and external navigation. It automatically detects whether the provided URL is internal (relative path) or external (absolute URL with protocol) and renders the appropriate element: - -- **Internal routes**: Uses `react-router-dom`'s `Link` component for client-side navigation -- **External URLs**: Renders a standard `` element for traditional navigation - - - -## Examples - -### Variants - -Here's a view when links have different variants. - -} - code={linkVariantsSnippet} -/> - -### Weights - -Here's a view when links have different weights. - -} - code={linkWeightsSnippet} -/> - -### Colors - -Here's a view when links have different colors. - -} - code={linkColorsSnippet} -/> - -### Truncate - -The `Link` component has a `truncate` prop that can be used to truncate the text. - -} - code={linkTruncateSnippet} -/> - - - - diff --git a/docs-ui/src/content/components/link.props.ts b/docs-ui/src/content/components/link.props.ts deleted file mode 100644 index 748e64cc15..0000000000 --- a/docs-ui/src/content/components/link.props.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const linkPropDefs: Record = { - href: { - type: 'string', - }, - variant: { - type: 'enum', - values: [ - 'title-large', - 'title-medium', - 'title-small', - 'title-x-small', - 'body-large', - 'body-medium', - 'body-small', - 'body-x-small', - ], - default: 'body-medium', - responsive: true, - }, - weight: { - type: 'enum', - values: ['regular', 'bold'], - default: 'regular', - responsive: true, - }, - color: { - type: 'enum', - values: ['primary', 'secondary', 'danger', 'warning', 'success'], - default: 'primary', - responsive: true, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const linkUsageSnippet = `import { Link } from '@backstage/ui'; - -Sign up for Backstage`; - -export const linkDefaultSnippet = `Sign up for Backstage`; - -export const linkVariantsSnippet = ` - ... - ... - ... - ... - ... - ... - ... - ... -`; - -export const linkWeightsSnippet = ` - - -`; - -export const linkColorsSnippet = ` - I am primary - I am secondary - I am danger - I am warning - I am success -`; - -export const linkRouterSnippet = `import { Link } from '@backstage/ui'; - -// Internal route -Home - -// External URL -Backstage -`; - -export const linkTruncateSnippet = `...`; diff --git a/docs-ui/src/content/components/menu.props.ts b/docs-ui/src/content/components/menu.props.ts deleted file mode 100644 index 2c7d76dd5d..0000000000 --- a/docs-ui/src/content/components/menu.props.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -const placementValues = [ - 'bottom', - 'bottom left', - 'bottom right', - 'bottom start', - 'bottom end', - 'top', - 'top left', - 'top right', - 'top start', - 'top end', - 'left', - 'left top', - 'left bottom', - 'start', - 'start top', - 'start bottom', - 'right', - 'right top', - 'right bottom', - 'end', - 'end top', - 'end bottom', -]; - -export const menuTriggerPropDefs: Record = { - isOpen: { - type: 'boolean', - }, - defaultOpen: { - type: 'boolean', - }, - onOpenChange: { - type: 'enum', - values: ['(isOpen: boolean) => void'], - }, -}; - -export const submenuTriggerPropDefs: Record = { - delay: { - type: 'number', - default: '200', - }, -}; - -export const menuPropDefs: Record = { - disabledKeys: { - type: 'enum', - values: ['Iterable'], - }, - selectionMode: { - type: 'enum', - values: ['none', 'single', 'multiple'], - }, - selectedKeys: { - type: 'enum', - values: ['all', 'Iterable'], - }, - defaultSelectedKeys: { - type: 'enum', - values: ['all', 'Iterable'], - }, - placement: { - type: 'enum', - values: placementValues, - }, - virtualized: { - type: 'boolean', - default: 'false', - }, - maxWidth: { - type: 'number', - }, - maxHeight: { - type: 'number', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuListBoxPropDefs: Record = { - disabledKeys: { - type: 'enum', - values: ['Iterable'], - }, - selectionMode: { - type: 'enum', - values: ['none', 'single', 'multiple'], - }, - selectedKeys: { - type: 'enum', - values: ['all', 'Iterable'], - }, - defaultSelectedKeys: { - type: 'enum', - values: ['all', 'Iterable'], - }, - placement: { - type: 'enum', - values: placementValues, - }, - virtualized: { - type: 'boolean', - default: 'false', - }, - maxWidth: { - type: 'number', - }, - maxHeight: { - type: 'number', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuAutocompletePropDefs: Record = { - placement: { - type: 'enum', - values: placementValues, - }, - virtualized: { - type: 'boolean', - default: 'false', - }, - maxWidth: { - type: 'number', - }, - maxHeight: { - type: 'number', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuAutocompleteListboxPropDefs: Record = { - placement: { - type: 'enum', - values: placementValues, - }, - virtualized: { - type: 'boolean', - default: 'false', - }, - maxWidth: { - type: 'number', - }, - maxHeight: { - type: 'number', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuItemPropDefs: Record = { - id: { - type: 'enum', - values: ['Key'], - }, - value: { - type: 'string', - }, - textValue: { - type: 'string', - }, - isDisabled: { - type: 'boolean', - }, - href: { - type: 'string', - }, - onAction: { - type: 'enum', - values: ['(event) => void'], - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuListBoxItemPropDefs: Record = { - id: { - type: 'enum', - values: ['Key'], - }, - value: { - type: 'string', - }, - textValue: { - type: 'string', - }, - isDisabled: { - type: 'boolean', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuSectionPropDefs: Record = { - title: { - type: 'string', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const menuSeparatorPropDefs: Record = { - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui'; - - - - - Apple - Banana - Blueberry - - - Vegetables - - Carrot - Tomato - Potato - - - -`; - -export const preview = ` - - - {options.map(option => ( - {option.label} - ))} - -`; - -export const submenu = ` - - - Edit - Duplicate - - Submenu - - Edit - Duplicate - Rename - - Share - Move - - }>Feedback - - - -`; - -export const icons = ` - - - }>Copy - }>Rename - }>Send feedback - -`; - -export const sections = ` - - - - }>Profile - }>Settings - - - }>Help Center - }> - Contact Support - - }>Feedback - - -`; - -export const separators = ` - - - Edit - Duplicate - Rename - - Share - Move - - }>Feedback - -`; - -export const links = ` - - - Internal link - - External link - - Email link - -`; - -export const autocomplete = ` - - - Create new file... - Create new folder... - Assign to... - Assign to me - Change status... - Change priority... - Add label... - Remove label... - -`; - -export const autocompleteListbox = `const [selected, setSelected] = useState( - new Set(['blueberry']), -); - - - Selected: {Array.from(selected).join(', ')} - - - - {options.map(option => ( - - {option.label} - - ))} - - -`; - -export const autocompleteListboxMultiple = `const [selected, setSelected] = useState( - new Set(['blueberry', 'cherry']), -); - - - Selected: {Array.from(selected).join(', ')} - - - - {options.map(option => ( - - {option.label} - - ))} - - -`; diff --git a/docs-ui/src/content/components/password-field.props.ts b/docs-ui/src/content/components/password-field.props.ts deleted file mode 100644 index 493405b5fd..0000000000 --- a/docs-ui/src/content/components/password-field.props.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const inputPropDefs: Record = { - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'small', - responsive: true, - }, - label: { - type: 'string', - }, - icon: { - type: 'enum', - values: ['ReactNode'], - }, - description: { - type: 'string', - }, - name: { - type: 'string', - required: true, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui'; - -`; - -export const passwordFieldDefaultSnippet = ``; - -export const passwordFieldSizesSnippet = ` - } /> - } /> -`; - -export const passwordFieldDescriptionSnippet = ``; diff --git a/docs-ui/src/content/components/radio-group.props.ts b/docs-ui/src/content/components/radio-group.props.ts deleted file mode 100644 index 3ed08f1053..0000000000 --- a/docs-ui/src/content/components/radio-group.props.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const radioGroupPropDefs: Record = { - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'small', - responsive: true, - }, - label: { - type: 'string', - }, - icon: { - type: 'enum', - values: ['ReactNode'], - }, - description: { - type: 'string', - }, - name: { - type: 'string', - required: true, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/ui'; - -`; - -export const radioGroupDefaultSnippet = ``; - -export const radioGroupDescriptionSnippet = ``; - -export const radioGroupHorizontalSnippet = ``; - -export const radioGroupDisabledSnippet = ` - Bulbasaur - Charmander - Squirtle -`; - -export const radioGroupDisabledSingleSnippet = ` - Bulbasaur - Charmander - Squirtle -`; - -export const radioGroupValidationSnippet = ` (value === \'charmander\' ? \'Nice try!\' : null)> - Bulbasaur - Charmander - Squirtle -`; - -export const radioGroupReadOnlySnippet = ` - Bulbasaur - Charmander - Squirtle -`; diff --git a/docs-ui/src/content/components/search-field.props.ts b/docs-ui/src/content/components/search-field.props.ts deleted file mode 100644 index b11ae3931c..0000000000 --- a/docs-ui/src/content/components/search-field.props.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const searchFieldPropDefs: Record = { - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'small', - responsive: true, - }, - label: { - type: 'string', - }, - icon: { - type: 'enum', - values: ['ReactNode'], - }, - description: { - type: 'string', - }, - name: { - type: 'string', - required: true, - }, - startCollapsed: { - type: 'boolean', - default: 'false', - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui'; - -`; - -export const searchFieldDefaultSnippet = ``; - -export const searchFieldSizesSnippet = ` - } /> - } /> -`; - -export const searchFieldDescriptionSnippet = ``; - -export const searchFieldCollapsibleSnippet = ``; diff --git a/docs-ui/src/content/components/select.props.ts b/docs-ui/src/content/components/select.props.ts deleted file mode 100644 index a93f448bd8..0000000000 --- a/docs-ui/src/content/components/select.props.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const selectPropDefs: Record = { - label: { - type: 'string', - responsive: false, - }, - description: { - type: 'string', - responsive: false, - }, - name: { - type: 'string', - responsive: false, - required: true, - }, - options: { - type: 'enum', - values: ['Array<{ value: string, label: string }>'], - required: true, - }, - placeholder: { - type: 'string', - default: 'Select an item', - responsive: false, - }, - icon: { - type: 'enum', - values: ['ReactNode'], - responsive: false, - }, - value: { - type: 'string', - responsive: false, - }, - defaultValue: { - type: 'string', - responsive: false, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - isOpen: { - type: 'boolean', - responsive: false, - }, - defaultOpen: { - type: 'boolean', - responsive: false, - }, - disabledKeys: { - type: 'enum', - values: ['string[]'], - responsive: false, - }, - isDisabled: { - type: 'boolean', - responsive: false, - }, - isRequired: { - type: 'boolean', - responsive: false, - }, - isInvalid: { - type: 'boolean', - responsive: false, - }, - selectedKey: { - type: 'string', - responsive: false, - }, - defaultSelectedKey: { - type: 'string', - responsive: false, - }, - onOpenChange: { - type: 'enum', - values: ['(isOpen: boolean) => void'], - responsive: false, - }, - onSelectionChange: { - type: 'enum', - values: ['(key: Key | null) => void'], - responsive: false, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const selectUsageSnippet = `import { Select } from '@backstage/ui'; - -`; - -export const selectDescriptionSnippet = `} - options={[ ... ]} -/>`; - -export const selectSizesSnippet = ` - -`; - -export const selectDisabledSnippet = ``; diff --git a/docs-ui/src/content/components/skeleton.props.ts b/docs-ui/src/content/components/skeleton.props.ts deleted file mode 100644 index ab79268d15..0000000000 --- a/docs-ui/src/content/components/skeleton.props.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const skeletonPropDefs: Record = { - width: { - type: 'number', - default: '80', - responsive: false, - }, - height: { - type: 'number', - default: '24', - responsive: false, - }, - rounded: { - type: 'boolean', - default: 'false', - responsive: false, - }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const skeletonUsageSnippet = `import { Flex, Skeleton } from '@backstage/ui'; - - - - - -`; - -export const skeletonDefaultSnippet = ` - - - -`; - -export const skeletonDemo1Snippet = ` - - - - - - - - - - -`; - -export const skeletonDemo2Snippet = ` - - - -`; diff --git a/docs-ui/src/content/components/tabs.mdx b/docs-ui/src/content/components/tabs.mdx deleted file mode 100644 index 26905d4733..0000000000 --- a/docs-ui/src/content/components/tabs.mdx +++ /dev/null @@ -1,84 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { TabsSnippet } from '@/snippets/stories-snippets'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { - tabsPropDefs, - tabPropDefs, - tabsUsageSnippet, - tabsDefaultSnippet, - tabsWithTabPanelsSnippet, - tabsWithLinksSnippet, - tabsWithDeeplyNestedRoutesSnippet, -} from './tabs.props'; -import { PageTitle } from '@/components/PageTitle'; -import { Theming } from '@/components/Theming'; -import { ChangelogComponent } from '@/components/ChangelogComponent'; - - - -} - code={tabsDefaultSnippet} -/> - -## Usage - - - -## API reference - -### Tabs - -Groups the tabs and the corresponding panels. Renders a `
` element. - - - -### Tab - -An individual interactive tab button that toggles the corresponding panel. Renders a `} /> + - Content + - + + + + + + + Content + + + \`\`\` + + CSS classes: \`.bui-CollapsibleRoot\` → \`.bui-Accordion\`, \`.bui-CollapsibleTrigger\` → \`.bui-AccordionTrigger\` (now on heading element), \`.bui-CollapsiblePanel\` → \`.bui-AccordionPanel\` + + ## Migration Path 2: React Aria Disclosure (Full Customization) + + For custom styling without preset styles: + + \`\`\`tsx + import { Disclosure, Button, DisclosurePanel } from 'react-aria-components'; + + + + Content + ; + \`\`\``, + type: 'breaking', + commitSha: '83c100e', + }, + { + components: ['select'], + version: '0.9.0', + prs: ['31649'], + description: `**BREAKING**: The \`SelectProps\` interface now accepts a generic type parameter for selection mode. + + Added searchable and multiple selection support to Select component. The component now accepts \`searchable\`, \`selectionMode\`, and \`searchPlaceholder\` props to enable filtering and multi-selection modes. + + Migration: If you're using \`SelectProps\` type directly, update from \`SelectProps\` to \`SelectProps<'single' | 'multiple'>\`. Component usage remains backward compatible.`, + type: 'breaking', + commitSha: '816af0f', + }, + { + components: ['header'], + version: '0.9.0', + prs: ['31525'], + description: `Fix broken external links in Backstage UI Header component.`, + type: 'fix', + commitSha: 'd01de00', + }, + { + components: ['select'], + version: '0.9.0', + prs: ['31618'], + description: `Fixed CSS issues in Select component including popover width constraints, focus outline behavior, and overflow handling.`, + type: 'fix', + commitSha: '35a3614', + }, + { + components: ['password-field', 'searchfield', 'menu'], + version: '0.9.0', + prs: ['31679'], + description: `Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components.`, + type: 'fix', + commitSha: '01476f0', + }, + { + components: [], + version: '0.9.0', + prs: ['31429'], + description: `Fix default text color in Backstage UI`, + type: 'fix', + commitSha: '26c6a78', + }, + { + components: ['text'], + version: '0.9.0', + prs: ['31615'], + description: `Fixed Text component to prevent \`truncate\` prop from being spread to the underlying DOM element.`, + type: 'fix', + commitSha: 'deaa427', + }, + { + components: ['link'], + version: '0.9.0', + prs: ['31524'], + description: `Improved the Link component structure in Backstage UI.`, + type: 'fix', + commitSha: '1059f95', + }, + { + components: ['dialog'], + version: '0.9.0', + prs: ['31673'], + description: `Fixed dialog backdrop appearance in dark mode.`, + type: 'fix', + commitSha: '836b0c7', + }, + { + components: ['table', 'avatar'], + version: '0.9.0', + prs: ['31608'], + description: `Migrated CellProfile component from Base UI Avatar to Backstage UI Avatar component.`, + type: 'fix', + commitSha: '6874094', + }, + { + components: ['avatar'], + version: '0.9.0', + prs: ['31623'], + description: `Avatar components in x-small and small sizes now display only one initial instead of two, improving readability at smaller dimensions.`, + type: 'fix', + commitSha: '719d772', + }, + { + components: [], + version: '0.9.0', + prs: ['31672'], + description: `Removed \`@base-ui-components/react\` dependency as all components now use React Aria Components.`, + type: 'fix', + commitSha: '6d35a6b', + }, + { + components: [], + version: '0.9.0', + prs: ['31435'], + description: `Fix the default font size in Backstage UI.`, + type: 'fix', + commitSha: 'dac851f', + }, + { + components: [], + version: '0.9.0', + prs: ['31448'], + description: `Fix CSS layer ordering in Backstage UI to make sure component styles are loaded after tokens and base declarations.`, + type: 'fix', + commitSha: '3c0ea67', + }, + { + components: ['radiogroup'], + version: '0.9.0', + prs: ['31576'], + description: `Fixed RadioGroup radio button ellipse distortion by preventing flex shrink and grow.`, + type: 'fix', + commitSha: '3b18d80', + }, + { + components: [], + version: '0.9.0', + prs: ['31444'], + description: `Fix font smoothing as default in Backstage UI.`, + type: 'fix', + commitSha: '4eb455c', + }, + { + components: [], + version: '0.9.0', + prs: ['31516'], + description: `Enable tree-shaking of imports other than \`*.css\`.`, + type: 'fix', + commitSha: 'ff9f0c3', + }, + { + components: ['button', 'button-icon'], + version: '0.9.0', + prs: ['31681'], + description: `Added \`loading\` prop to Button and ButtonIcon components for displaying spinner during async operations.`, + type: 'new', + commitSha: '7839e7b', + }, + { + components: ['table'], + version: '0.9.0', + prs: ['31680'], + description: `Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the \`href\` prop.`, + type: 'fix', + commitSha: 'a00fb88', + }, + { + components: [], + version: '0.9.0', + prs: ['31469'], + description: `Set the color-scheme property depending on theme`, + type: 'fix', + commitSha: 'e16ece5', + }, + { + components: ['visually-hidden'], + version: '0.9.0', + prs: ['31484'], + description: `Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers.`, + type: 'new', + commitSha: '1ef3ca4', + }, + { + components: [], + version: '0.9.0', + prs: ['31432'], + description: `Fix default font wight and font family in Backstage UI.`, + type: 'fix', + commitSha: '00bfb83', + }, +]; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index 18d75d31c8..1b046fd3e5 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -1,203 +1,140 @@ interface Page { title: string; slug: string; - status?: 'alpha' | 'beta' | 'stable' | 'deprecated' | 'inProgress'; + status?: 'alpha' | 'beta' | 'stable' | 'deprecated' | 'inProgress' | 'new'; } -export const overview: Page[] = [ - { - title: 'Getting Started', - slug: '', - }, - { - title: 'Installation', - slug: 'install', - }, - { - title: 'About', - slug: 'about', - }, - { - title: 'Changelog', - slug: 'changelog', - }, -]; - -export const coreConcepts: Page[] = [ - { - title: 'Layout', - slug: 'layout', - }, - { - title: 'Responsive', - slug: 'responsive', - }, - { - title: 'Theming', - slug: 'theming', - }, -]; - export const layoutComponents: Page[] = [ { title: 'Box', slug: 'box', - status: 'alpha', }, { title: 'Container', slug: 'container', - status: 'alpha', }, { title: 'Grid', slug: 'grid', - status: 'alpha', }, { title: 'Flex', slug: 'flex', - status: 'alpha', }, ]; export const components: Page[] = [ + { + title: 'Accordion', + slug: 'accordion', + }, { title: 'Avatar', slug: 'avatar', - status: 'alpha', }, { title: 'Button', slug: 'button', - status: 'alpha', }, { title: 'ButtonIcon', slug: 'button-icon', - status: 'alpha', }, { title: 'ButtonLink', slug: 'button-link', - status: 'alpha', }, { title: 'Card', slug: 'card', - status: 'alpha', }, { title: 'Checkbox', slug: 'checkbox', - status: 'alpha', - }, - { - title: 'Collapsible', - slug: 'collapsible', - status: 'alpha', }, { title: 'Dialog', slug: 'dialog', - status: 'alpha', }, { title: 'Header', slug: 'header', - status: 'alpha', }, { title: 'HeaderPage', slug: 'header-page', - status: 'alpha', }, { title: 'Link', slug: 'link', - status: 'alpha', }, { title: 'Menu', slug: 'menu', - status: 'alpha', }, { title: 'PasswordField', slug: 'password-field', - status: 'alpha', + }, + { + title: 'Popover', + slug: 'popover', + status: 'new', }, { title: 'RadioGroup', slug: 'radio-group', - status: 'alpha', }, { title: 'SearchField', slug: 'search-field', - status: 'alpha', }, { title: 'Select', slug: 'select', - status: 'alpha', }, { title: 'Skeleton', slug: 'skeleton', - status: 'alpha', }, { title: 'Switch', slug: 'switch', - status: 'alpha', }, { title: 'Table', slug: 'table', - status: 'alpha', }, { title: 'Tabs', slug: 'tabs', - status: 'alpha', }, { title: 'TagGroup', slug: 'tag-group', - status: 'alpha', }, { title: 'Text', slug: 'text', - status: 'alpha', }, { title: 'TextField', slug: 'text-field', - status: 'alpha', + }, + { + title: 'ToggleButton', + slug: 'toggle-button', + }, + { + title: 'ToggleButtonGroup', + slug: 'toggle-button-group', }, { title: 'Tooltip', slug: 'tooltip', - status: 'alpha', }, { title: 'VisuallyHidden', slug: 'visually-hidden', - status: 'alpha', }, ]; - -export type ScreenSize = { - title: string; - slug: string; - width: number; -}; - -export const screenSizes: ScreenSize[] = [ - { title: 'Mobile', slug: 'mobile', width: 390 }, - { title: 'Tablet', slug: 'tablet', width: 768 }, - { title: 'Desktop', slug: 'desktop', width: 1280 }, - { title: 'Wide', slug: 'wide', width: 1600 }, -]; diff --git a/docs-ui/src/utils/definitions.ts b/docs-ui/src/utils/definitions.ts new file mode 100644 index 0000000000..d279d73775 --- /dev/null +++ b/docs-ui/src/utils/definitions.ts @@ -0,0 +1 @@ +export * from '../../../packages/ui/src/definitions'; diff --git a/docs-ui/src/utils/getPageName.ts b/docs-ui/src/utils/getPageName.ts index a3298bc9b7..7a1ccbf2a1 100644 --- a/docs-ui/src/utils/getPageName.ts +++ b/docs-ui/src/utils/getPageName.ts @@ -1,18 +1,6 @@ -import { overview, coreConcepts, components, layoutComponents } from './data'; +import { components, layoutComponents } from './data'; export function getPageName(slug: string): string | null { - // Search in overview pages - const overviewPage = overview.find(p => p.slug === slug); - if (overviewPage) { - return overviewPage.title; - } - - // Search in core concepts array - const coreConcept = coreConcepts.find(c => c.slug === slug); - if (coreConcept) { - return coreConcept.title; - } - // Search in components array const component = components.find(c => c.slug === slug); if (component) { diff --git a/docs-ui/src/utils/playground-context.tsx b/docs-ui/src/utils/playground-context.tsx index 2f8ffbaee4..a1cee6ecba 100644 --- a/docs-ui/src/utils/playground-context.tsx +++ b/docs-ui/src/utils/playground-context.tsx @@ -16,8 +16,8 @@ const PlaygroundContext = createContext<{ setSelectedScreenSizes: (screenSizes: string[]) => void; selectedComponents: string[]; setSelectedComponents: (components: string[]) => void; - selectedTheme: Theme; - setSelectedTheme: (theme: Theme) => void; + selectedTheme: Set; + setSelectedTheme: (keys: Set) => void; selectedThemeName: ThemeName; setSelectedThemeName: (themeName: ThemeName) => void; }>({ @@ -25,7 +25,7 @@ const PlaygroundContext = createContext<{ setSelectedScreenSizes: () => {}, selectedComponents: [], setSelectedComponents: () => {}, - selectedTheme: 'light', + selectedTheme: new Set(['light']), setSelectedTheme: () => {}, selectedThemeName: 'backstage', setSelectedThemeName: () => {}, @@ -40,16 +40,24 @@ export const PlaygroundProvider = ({ children }: { children: ReactNode }) => { const [selectedComponents, setSelectedComponents] = useState( components.map(component => component.slug), ); - const [selectedTheme, setSelectedTheme] = useState('light'); + const [selectedTheme, setSelectedTheme] = useState>( + new Set(['light']), + ); const [selectedThemeName, setSelectedThemeName] = useState('backstage'); // Load saved theme from localStorage after hydration useEffect(() => { if (isBrowser) { - const savedTheme = localStorage.getItem('theme-mode') as Theme; - if (savedTheme) { - setSelectedTheme(savedTheme); + const savedThemeString = localStorage.getItem('theme-mode'); + if (savedThemeString) { + // Parse the comma-separated string back into a Set + const themeArray = savedThemeString + .split(',') + .filter(Boolean) as Theme[]; + setSelectedTheme(new Set(themeArray)); + } else { + setSelectedTheme(new Set(['light'])); } } }, [isBrowser]); @@ -68,9 +76,9 @@ export const PlaygroundProvider = ({ children }: { children: ReactNode }) => { if (isBrowser) { document.documentElement.setAttribute( 'data-theme-mode', - selectedTheme || 'light', + Array.from(selectedTheme).join(','), ); - localStorage.setItem('theme-mode', selectedTheme || 'light'); + localStorage.setItem('theme-mode', Array.from(selectedTheme).join(',')); } }, [selectedTheme, isBrowser]); diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts new file mode 100644 index 0000000000..f2f773ebc2 --- /dev/null +++ b/docs-ui/src/utils/types.ts @@ -0,0 +1,46 @@ +export type Component = + | 'avatar' + | 'box' + | 'button' + | 'button-link' + | 'heading' + | 'text' + | 'button-icon' + | 'icon' + | 'tabs' + | 'menu' + | 'textfield' + | 'datatable' + | 'select' + | 'collapsible' + | 'accordion' + | 'checkbox' + | 'container' + | 'link' + | 'tooltip' + | 'scrollarea' + | 'flex' + | 'switch' + | 'grid' + | 'searchfield' + | 'radio-group' + | 'card' + | 'skeleton' + | 'header' + | 'header-page' + | 'password-field' + | 'table' + | 'visually-hidden' + | 'dialog' + | 'tag-group'; + +export type Version = `${number}.${number}.${number}`; + +export interface ChangelogProps { + components: Component[]; + description: string; + version: Version; + prs: string[]; + type?: 'breaking' | 'new' | 'fix'; + commitSha?: string; +} diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock index 538b4947b2..8b5a1d865e 100644 --- a/docs-ui/yarn.lock +++ b/docs-ui/yarn.lock @@ -23,10 +23,51 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.21.0": - version: 7.28.3 - resolution: "@babel/runtime@npm:7.28.3" - checksum: 10/f2415e4dbface7496f6fc561d640b44be203071fb0dfb63fbe338c7d2d2047419cb054ef13d1ebb8fc11e35d2b55aa3045def4b985e8b82aea5d7e58e1133e52 +"@babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 + languageName: node + linkType: hard + +"@base-ui-components/react@npm:^1.0.0-beta.4": + version: 1.0.0-rc.0 + resolution: "@base-ui-components/react@npm:1.0.0-rc.0" + dependencies: + "@babel/runtime": "npm:^7.28.4" + "@base-ui-components/utils": "npm:0.2.2" + "@floating-ui/react-dom": "npm:^2.1.6" + "@floating-ui/utils": "npm:^0.2.10" + reselect: "npm:^5.1.1" + tabbable: "npm:^6.3.0" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + "@types/react": ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/ea22dd8206962a1b74fea30f4b20f8b5995bd20155466a5ee7fc7a59cc1632e2ba9615094eac9983dbf8387e50daa8b078024da85b3a8d60cdbc43fb1cfc3b50 + languageName: node + linkType: hard + +"@base-ui-components/utils@npm:0.2.2": + version: 0.2.2 + resolution: "@base-ui-components/utils@npm:0.2.2" + dependencies: + "@babel/runtime": "npm:^7.28.4" + "@floating-ui/utils": "npm:^0.2.10" + reselect: "npm:^5.1.1" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + "@types/react": ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/a67803b5637cfce1071963f958306f90784f7934ae0b072b450dbf7f2e3423ea9b40e420672849cfcf2965c94ac5960d2c6a5e04e359f1f76166b929dda932a2 languageName: node linkType: hard @@ -138,14 +179,14 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.34.4, @codemirror/view@npm:^6.35.0": - version: 6.38.1 - resolution: "@codemirror/view@npm:6.38.1" + version: 6.38.8 + resolution: "@codemirror/view@npm:6.38.8" dependencies: "@codemirror/state": "npm:^6.5.0" crelt: "npm:^1.0.6" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/e0c5a365608749dd096ba7a930c8393f316bf4c2cacd1465a47a057d0a9f9868ff372a0bb6eb696c926f88411139f79a97a05f8c884bcc380145445cc61e68c8 + checksum: 10/81b1508015a378e4719d0239254173f0c5cd340c2abf96eb488fe5fb474bdb37ec1f010b9890ced774accd7aeb9443e7337cb6a89544b954273e5ddabece7cea languageName: node linkType: hard @@ -177,188 +218,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/aix-ppc64@npm:0.25.9" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/android-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/android-arm64@npm:0.25.9" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/android-arm@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/android-arm@npm:0.25.9" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@esbuild/android-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/android-x64@npm:0.25.9" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/darwin-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/darwin-arm64@npm:0.25.9" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/darwin-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/darwin-x64@npm:0.25.9" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/freebsd-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/freebsd-arm64@npm:0.25.9" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/freebsd-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/freebsd-x64@npm:0.25.9" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/linux-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-arm64@npm:0.25.9" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/linux-arm@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-arm@npm:0.25.9" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@esbuild/linux-ia32@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-ia32@npm:0.25.9" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/linux-loong64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-loong64@npm:0.25.9" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - -"@esbuild/linux-mips64el@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-mips64el@npm:0.25.9" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"@esbuild/linux-ppc64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-ppc64@npm:0.25.9" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/linux-riscv64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-riscv64@npm:0.25.9" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - -"@esbuild/linux-s390x@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-s390x@npm:0.25.9" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - -"@esbuild/linux-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/linux-x64@npm:0.25.9" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/netbsd-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/netbsd-arm64@npm:0.25.9" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/netbsd-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/netbsd-x64@npm:0.25.9" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openbsd-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/openbsd-arm64@npm:0.25.9" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/openbsd-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/openbsd-x64@npm:0.25.9" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openharmony-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/openharmony-arm64@npm:0.25.9" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/sunos-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/sunos-x64@npm:0.25.9" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/win32-arm64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/win32-arm64@npm:0.25.9" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/win32-ia32@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/win32-ia32@npm:0.25.9" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/win32-x64@npm:0.25.9": - version: 0.25.9 - resolution: "@esbuild/win32-x64@npm:0.25.9" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.7.0": version: 4.7.0 resolution: "@eslint-community/eslint-utils@npm:4.7.0" @@ -401,6 +260,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" + dependencies: + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/a8952ff2673ddf28f12feeb86d90c54949e45bcb1af5758b7672850ac0dadb36d4bd61aa45dad1b6a35ba40d4756d3573afac6610b90502639d7266b91e0864e + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.7.4": + version: 1.7.4 + resolution: "@floating-ui/dom@npm:1.7.4" + dependencies: + "@floating-ui/core": "npm:^1.7.3" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/d3d6a23e7b9804ba56338c7c666590258683af14b6026270d32afc1202f72b5b82cca359004bdc7830bf2463a045da6c7bd4e7d5351218cf270ff94206197971 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.1.6": + version: 2.1.6 + resolution: "@floating-ui/react-dom@npm:2.1.6" + dependencies: + "@floating-ui/dom": "npm:^1.7.4" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/fbfd3319b42edb9c156e4e872f500d2edb112bc9cfd1b45892bff16ccf21c2484ddc9c416f7631c2aaaadec1b2f98b205db8a3f89eb78ca870905fcfe3917c35 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.13.0": version: 0.13.0 resolution: "@humanwhocodes/config-array@npm:0.13.0" @@ -650,10 +547,10 @@ __metadata: languageName: node linkType: hard -"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0": - version: 1.2.3 - resolution: "@lezer/common@npm:1.2.3" - checksum: 10/dad24e353e4e67d88b203191361ca1dff26c01c2b7b4ae829b668a1d115929334d077217367683e39180c0556510ed2066ea8ddba2b079be7c08a7152208cc87 +"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.3.0": + version: 1.3.0 + resolution: "@lezer/common@npm:1.3.0" + checksum: 10/8e195a8e426bc18d4339b3f2a1a7ad39c3b2cfa740c7108657a241985f63bdee5255a5f5cf8d863b878881744288bcb679d16170f0e5bcebb141188b53cfd8c0 languageName: node linkType: hard @@ -669,11 +566,11 @@ __metadata: linkType: hard "@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.2.1": - version: 1.2.1 - resolution: "@lezer/highlight@npm:1.2.1" + version: 1.2.3 + resolution: "@lezer/highlight@npm:1.2.3" dependencies: - "@lezer/common": "npm:^1.0.0" - checksum: 10/fec3082419ee87fb265039b680fbac6796f862d8e3042dcb860e8c5a34291503a74927302b568ff1a626f0d2b5cf8dae02a51cfd200084eb329e5fd1236c3163 + "@lezer/common": "npm:^1.3.0" + checksum: 10/8f787d464f8a036f117a0b23e73ac034d224a57d72501c6559089098a28f127c9e495b90ac7d132acc86199e0b64d4c038f75f9293a37c7c61add52fa1acdb4e languageName: node linkType: hard @@ -705,8 +602,8 @@ __metadata: linkType: hard "@mdx-js/loader@npm:^3.1.0": - version: 3.1.0 - resolution: "@mdx-js/loader@npm:3.1.0" + version: 3.1.1 + resolution: "@mdx-js/loader@npm:3.1.1" dependencies: "@mdx-js/mdx": "npm:^3.0.0" source-map: "npm:^0.7.0" @@ -715,18 +612,19 @@ __metadata: peerDependenciesMeta: webpack: optional: true - checksum: 10/983b8d4949f4d214a17f4ad8597a50de36ace75e93292d2493b5fb6077d69d6abc8230b62f88b818c04f12a01d281d63edb6a0738937f7e259406a73ce998134 + checksum: 10/b78afe06e49b5c0bec9e5b232fbc874ffa721ff6b022bdaa7a29e2b5a44fa9f8282aa2b34f74701ca0b8750b33a775cd3182aaba5202e209f3d66961600c930f languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.0.0, @mdx-js/mdx@npm:^3.1.0": - version: 3.1.0 - resolution: "@mdx-js/mdx@npm:3.1.0" +"@mdx-js/mdx@npm:^3.0.0, @mdx-js/mdx@npm:^3.1.1": + version: 3.1.1 + resolution: "@mdx-js/mdx@npm:3.1.1" dependencies: "@types/estree": "npm:^1.0.0" "@types/estree-jsx": "npm:^1.0.0" "@types/hast": "npm:^3.0.0" "@types/mdx": "npm:^2.0.0" + acorn: "npm:^8.0.0" collapse-white-space: "npm:^2.0.0" devlop: "npm:^1.0.0" estree-util-is-identifier-name: "npm:^3.0.0" @@ -747,19 +645,19 @@ __metadata: unist-util-stringify-position: "npm:^4.0.0" unist-util-visit: "npm:^5.0.0" vfile: "npm:^6.0.0" - checksum: 10/4bd4e1160e2b2bc9ea2b5b93246ce0e34a11ac5fd420ec025e82fb1120a72b80025d9cb205cce6394bb5f0013f209b9ea453cbda4c0ca4f97a2169df60084742 + checksum: 10/b871da2497f6e0f11da1574fe772a50b09b7c177de8df0f821f708bf162febe76c01a572a5c68e860960189209fd66f768c2e747fdb3a1f497c5c32e11890c11 languageName: node linkType: hard -"@mdx-js/react@npm:^3.1.0": - version: 3.1.0 - resolution: "@mdx-js/react@npm:3.1.0" +"@mdx-js/react@npm:^3.1.0, @mdx-js/react@npm:^3.1.1": + version: 3.1.1 + resolution: "@mdx-js/react@npm:3.1.1" dependencies: "@types/mdx": "npm:^2.0.0" peerDependencies: "@types/react": ">=16" react: ">=16" - checksum: 10/cf89d6392c76091622fb647f205e1ab5cbdf5edd4401dde7092138cefc9fbb6d61428aa63557de0bccca3695d5a8854dd4a93b34a27cb8e27369da7eaeaa3e73 + checksum: 10/52a740e2f37761694fa94d4704b7825084b4055616a95c8b8f4c1676190d399ddc5cdbb399ffc45b550beecd30497a7224c2e5b05bf43ecb668c7473641037d1 languageName: node linkType: hard @@ -774,25 +672,25 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.4.7": - version: 15.4.7 - resolution: "@next/env@npm:15.4.7" - checksum: 10/8341fa6f1b5aebb6f99d8abad2f3ee2685281aaa51785f8ceff3c9a7f03186645f372003ee833715c0ce05e4e7d960defa4252018977f19564001b872b24714e +"@next/env@npm:15.5.9": + version: 15.5.9 + resolution: "@next/env@npm:15.5.9" + checksum: 10/962329701343c2617c85ae99ef35adfd9e8f7b58d9c0316f2f0857f82875a91ef3151ef0743c50964b0066aa3d1214c5193cf229f7757d7c9329f7fb95605a4a languageName: node linkType: hard -"@next/eslint-plugin-next@npm:15.3.4": - version: 15.3.4 - resolution: "@next/eslint-plugin-next@npm:15.3.4" +"@next/eslint-plugin-next@npm:15.5.7": + version: 15.5.7 + resolution: "@next/eslint-plugin-next@npm:15.5.7" dependencies: fast-glob: "npm:3.3.1" - checksum: 10/8a473bd32a06f62c16f60f9c40b7b8149cf91170ce4c1546cda57e9c85ac8481c7ad3aa4a77e8c3c7000e80d3de809d2db7df6cb79e13fd258fff382729abf60 + checksum: 10/5b59860aeccc0d07a979c40e4405b410a9d7aa86a235d5dc0edbac8d2da35586f6f2fa3407cd3d80fd7e7d4f197c70291e9c6e71a64942d09b8fe82470e90ee0 languageName: node linkType: hard -"@next/mdx@npm:15.3.4": - version: 15.3.4 - resolution: "@next/mdx@npm:15.3.4" +"@next/mdx@npm:15.5.7": + version: 15.5.7 + resolution: "@next/mdx@npm:15.5.7" dependencies: source-map: "npm:^0.7.0" peerDependencies: @@ -803,62 +701,62 @@ __metadata: optional: true "@mdx-js/react": optional: true - checksum: 10/07904beda049317e43857f9dfe659adb6e57c4417475de1d21a7605f8ace5d9aa992a06465d1a94fe4b62331901765b15659555e8aadedbd410f86df1bebd1bd + checksum: 10/caf2ad1e3a8b02381ad33fe32f59e2ef4c15c96a04ba2a7d713991c142214d3b69d25b879ffc12da1c333ffd34d30aec44061d740f708c8e12529b183a159146 languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-darwin-arm64@npm:15.4.7" +"@next/swc-darwin-arm64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-arm64@npm:15.5.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-darwin-x64@npm:15.4.7" +"@next/swc-darwin-x64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-x64@npm:15.5.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-linux-arm64-gnu@npm:15.4.7" +"@next/swc-linux-arm64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-linux-arm64-musl@npm:15.4.7" +"@next/swc-linux-arm64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-linux-x64-gnu@npm:15.4.7" +"@next/swc-linux-x64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-linux-x64-musl@npm:15.4.7" +"@next/swc-linux-x64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-musl@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-win32-arm64-msvc@npm:15.4.7" +"@next/swc-win32-arm64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.5.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.4.7": - version: 15.4.7 - resolution: "@next/swc-win32-x64-msvc@npm:15.4.7" +"@next/swc-win32-x64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.5.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -919,6 +817,130 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-token@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/auth-token@npm:6.0.0" + checksum: 10/a30f5c4c984964b57193de5b6f67169f74e4779fedbe716157dd3558dd9de3ca5c105cae521b7bd8ce1ae180773a2ef01afe2306ad5a329f4fd291eba2b7c7d1 + languageName: node + linkType: hard + +"@octokit/core@npm:^7.0.6": + version: 7.0.6 + resolution: "@octokit/core@npm:7.0.6" + dependencies: + "@octokit/auth-token": "npm:^6.0.0" + "@octokit/graphql": "npm:^9.0.3" + "@octokit/request": "npm:^10.0.6" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + before-after-hook: "npm:^4.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^11.0.2": + version: 11.0.2 + resolution: "@octokit/endpoint@npm:11.0.2" + dependencies: + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/0d088747baf94eafbba69da23ba840b40cd3f5d0bfbc51c692ff9d9d78de6d81f06366e6e30df8c1783355be826c27d38ab9ab0708396af8f430b06cfa29db35 + languageName: node + linkType: hard + +"@octokit/graphql@npm:^9.0.3": + version: 9.0.3 + resolution: "@octokit/graphql@npm:9.0.3" + dependencies: + "@octokit/request": "npm:^10.0.6" + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^27.0.0": + version: 27.0.0 + resolution: "@octokit/openapi-types@npm:27.0.0" + checksum: 10/5cd2cdf4e41fdf522e15e3d53f3ece8380d98dda9173a6fc905828fb2c33e8733d5f5d2a757ae3a572525f4749748e66cb40e7939372132988d8eb4ba978d54f + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^14.0.0": + version: 14.0.0 + resolution: "@octokit/plugin-paginate-rest@npm:14.0.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/57ddd857528dad9c02431bc6254c2374c06057872cf9656a4a88b162ebe1c2bc9f34fbec360f2ccff72c940f29b120758ce14e8135bd027223d381eb1b8b6579 + languageName: node + linkType: hard + +"@octokit/plugin-request-log@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/plugin-request-log@npm:6.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/8a79973b1429bfead9113c4117f418aaef5ff368795daded3415ba14623d97d5fc08d1e822dbd566ecc9f041119e1a48a11853a9c48d9eb1caa62baa79c17f83 + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0": + version: 17.0.0 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/e9d9ad4d9755cc7fb82fdcbfa870ddea8a432180f0f76c8469095557fd1e26f8caea8cae58401209be17c4f3d8cc48c0e16a3643e37e48f4d23c39e058bf2c55 + languageName: node + linkType: hard + +"@octokit/request-error@npm:^7.0.2": + version: 7.0.2 + resolution: "@octokit/request-error@npm:7.0.2" + dependencies: + "@octokit/types": "npm:^16.0.0" + checksum: 10/8edfaca9f5271115b090f470ebfe1006841ee152dd52cb39786e026236e2c13545aa3cf3187b7b1de717167696a42f91d8fa2f870bf9be0832fb2b11d11a741d + languageName: node + linkType: hard + +"@octokit/request@npm:^10.0.6": + version: 10.0.6 + resolution: "@octokit/request@npm:10.0.6" + dependencies: + "@octokit/endpoint": "npm:^11.0.2" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + fast-content-type-parse: "npm:^3.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/b8c5cd43d3225c8c3b803e889ff251808ae180c953ca92642522843123dafcd84d919b20ca2969b6e4080ee3c2197a99afda15b5649ee51d0715f8249a5c9ca4 + languageName: node + linkType: hard + +"@octokit/rest@npm:^22.0.1": + version: 22.0.1 + resolution: "@octokit/rest@npm:22.0.1" + dependencies: + "@octokit/core": "npm:^7.0.6" + "@octokit/plugin-paginate-rest": "npm:^14.0.0" + "@octokit/plugin-request-log": "npm:^6.0.0" + "@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0" + checksum: 10/ec2e94cfa8766716faeb3ca18527d9af746482d35aaa6e4265a30cb669ae3f31f4ebb6235edebe5ae62bc2cec2b8e88902584f698df2e7cabac3a15fd27da665 + languageName: node + linkType: hard + +"@octokit/types@npm:^16.0.0": + version: 16.0.0 + resolution: "@octokit/types@npm:16.0.0" + dependencies: + "@octokit/openapi-types": "npm:^27.0.0" + checksum: 10/03d5cfc29556a9b53eae8beb1bf15c0b704dc722db2c51b53f093f3c3ee6c1d8e20b682be8117a3a17034b458be7746d1b22aaefb959ceb5152ad7589b39e2c9 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -927,11 +949,11 @@ __metadata: linkType: hard "@remixicon/react@npm:^4.6.0": - version: 4.6.0 - resolution: "@remixicon/react@npm:4.6.0" + version: 4.7.0 + resolution: "@remixicon/react@npm:4.7.0" peerDependencies: react: ">=18.2.0" - checksum: 10/d92c8131734062fca8d82d55dd8f3f4e28cb72c5eefda528c8aba5f3298f0c847342eecdbd7787d6da5af21b699d735f6cf27f151f0c982c1c9f06a78b235c94 + checksum: 10/9ecee093dd4aec3744bcc7562eddb987bf9c7c059787dfecfbcc25a64c2f4833dc815dff715aa2a7f75011d65f877b3751a401d19cf2d725b5bcc685587d37f4 languageName: node linkType: hard @@ -949,74 +971,74 @@ __metadata: languageName: node linkType: hard -"@shikijs/core@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/core@npm:3.13.0" +"@shikijs/core@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/core@npm:3.19.0" dependencies: - "@shikijs/types": "npm:3.13.0" + "@shikijs/types": "npm:3.19.0" "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" hast-util-to-html: "npm:^9.0.5" - checksum: 10/c9b301a170cd76fa2ce446adee26b539b19204107a2e60207f3f5e7d96773df322367f6035e2d689d43f97a3e9f741793e976c366015d8e3a9709dcac1399af2 + checksum: 10/78fc5f849636459e267cd77f3e60b492c8260a8be9449d3b1b3a463eab22153354ca6c961e021f936cb8251695298d85913b2cb3b31aafa343fdddadebbfed1c languageName: node linkType: hard -"@shikijs/engine-javascript@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/engine-javascript@npm:3.13.0" +"@shikijs/engine-javascript@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/engine-javascript@npm:3.19.0" dependencies: - "@shikijs/types": "npm:3.13.0" + "@shikijs/types": "npm:3.19.0" "@shikijs/vscode-textmate": "npm:^10.0.2" - oniguruma-to-es: "npm:^4.3.3" - checksum: 10/acd4e78fdc732738c6a3a9f254011b7d8995a2149f2f72f7396d5e4514f3b0c3fdc767d52bf8852d65066e613dd8d1157bc4900442ab42684db7ef7396a44871 + oniguruma-to-es: "npm:^4.3.4" + checksum: 10/7b493cfce620976efd1d7cb3df946370897cac0c16fb71b089be9e197fde43ee60a3d1317db45820c61bcae47ae8e4ac4df91555454e733cd239dc064ef6cc03 languageName: node linkType: hard -"@shikijs/engine-oniguruma@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/engine-oniguruma@npm:3.13.0" +"@shikijs/engine-oniguruma@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/engine-oniguruma@npm:3.19.0" dependencies: - "@shikijs/types": "npm:3.13.0" + "@shikijs/types": "npm:3.19.0" "@shikijs/vscode-textmate": "npm:^10.0.2" - checksum: 10/bc7f1c69640ccece9a3ac3f3adc9cebde9c24bcd722747cc464c338a32725af2b9087a42646cd0f56fbea98d99414918c558f2ca0beadc13db0a740183e7707f + checksum: 10/6bf6e35aa61f62b1a532071d11e1937a67fb68469c1886ab3c06c6d384b8feee589973ee101cbebefad9ff9405040ee640ced429e83b9dddd906f07d994a1939 languageName: node linkType: hard -"@shikijs/langs@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/langs@npm:3.13.0" +"@shikijs/langs@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/langs@npm:3.19.0" dependencies: - "@shikijs/types": "npm:3.13.0" - checksum: 10/2a0478246ce61745d9012cf5051c9790ef5afad6c89fe5716be4a81d04d7dfd9ffc90af0bad0be0061111bfc001a3bc0c55ffd4fc0b82bc1c09a4c33d7b593ca + "@shikijs/types": "npm:3.19.0" + checksum: 10/679d61b89132c858bd1e96fc7a46fbae56dc8e9be176586cabb57cd9504bb7ea45bbd22879f63d822957060fe322302ad9824b9a6f18415a86b54e22347c0c2d languageName: node linkType: hard -"@shikijs/themes@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/themes@npm:3.13.0" +"@shikijs/themes@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/themes@npm:3.19.0" dependencies: - "@shikijs/types": "npm:3.13.0" - checksum: 10/4ab0ecf1ddb35e387cef22e267cdf3ed7548a84f24ad58885d19401f75cc01a5a10152ee0196222e01cead8202f20012615aa0076f798599f7dfc2bb7beaa617 + "@shikijs/types": "npm:3.19.0" + checksum: 10/7eb53912fe8b877e6fc498c937188ae55b960c681f65cd4201f2c3a919b0f5951f9ce090819ded5dc4eb0aabe7a811ba705a19b5c003e0dbfebad08d1ba535d5 languageName: node linkType: hard "@shikijs/transformers@npm:^3.13.0": - version: 3.13.0 - resolution: "@shikijs/transformers@npm:3.13.0" + version: 3.19.0 + resolution: "@shikijs/transformers@npm:3.19.0" dependencies: - "@shikijs/core": "npm:3.13.0" - "@shikijs/types": "npm:3.13.0" - checksum: 10/30f3e66334b60320dc976b9f645f823ba13b312aead4c891ae6db23d076f1570dad488443f603fb5eecd5937973ba7c2907da6678ccae2363a66e0c5b173bb02 + "@shikijs/core": "npm:3.19.0" + "@shikijs/types": "npm:3.19.0" + checksum: 10/813c2e4053007a8516ebae8c06f4fa2e7b42c6570ea4cae88ecd8bb749695be023ca3b271f8152559dbb74ed36404a77f76c14f63632dc44f225d963a446b1d1 languageName: node linkType: hard -"@shikijs/types@npm:3.13.0": - version: 3.13.0 - resolution: "@shikijs/types@npm:3.13.0" +"@shikijs/types@npm:3.19.0": + version: 3.19.0 + resolution: "@shikijs/types@npm:3.19.0" dependencies: "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" - checksum: 10/a57cc95847edd84134166de1f11a044e356581834b2dfd75dcd67f2221aef6f20f505839c090a76b7c8e0a79d7d5cee829de50d261f363a633ae9afca29546c4 + checksum: 10/3f0e79f2ecbb0754ad08626e14313701a15dd1dbda1a1295d1e35a172b4555339a135118fabcc3ebdbf5017ceb64dbe3e5096268c3034c0c91ba3b0848e4b162 languageName: node linkType: hard @@ -1027,109 +1049,6 @@ __metadata: languageName: node linkType: hard -"@storybook/components@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/components@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/d3647505510313aa3c32fd1f8f202eda723ad99bd353b20aa929c5ecab4edc3c86ad06d0eac02f3c6d948e88f13f65ca5fa35ef27c079ef1b92abe8692f23699 - languageName: node - linkType: hard - -"@storybook/core@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/core@npm:8.6.14" - dependencies: - "@storybook/theming": "npm:8.6.14" - better-opn: "npm:^3.0.2" - browser-assert: "npm:^1.2.1" - esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" - esbuild-register: "npm:^3.5.0" - jsdoc-type-pratt-parser: "npm:^4.0.0" - process: "npm:^0.11.10" - recast: "npm:^0.23.5" - semver: "npm:^7.6.2" - util: "npm:^0.12.5" - ws: "npm:^8.2.3" - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - checksum: 10/8f8c811edd4ea8dcedcc63a79b3168dc83aa0401e3760990cb5995ea6beaac4026c6ccc1046182f41eed7c09333005f3348d8b42f89fc5dd5b2ce011a9d2a48f - languageName: node - linkType: hard - -"@storybook/global@npm:^5.0.0": - version: 5.0.0 - resolution: "@storybook/global@npm:5.0.0" - checksum: 10/0e7b495f4fe7f36447e793926f1c0460ec07fd66f0da68e3150da5878f6043c9eeb9b41614a45c5ec0d48d5d383c59ca8f88b6dc7882a2a784ac9b20375d8edb - languageName: node - linkType: hard - -"@storybook/manager-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/manager-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/4544b317050b81574f1cd6f911dcb99ee2adbd7190555209171a7dd2233cd331165fa11c16a5977ebe58eeaf26c86bbfcb23f701cfd79a10f0d034dae65197bd - languageName: node - linkType: hard - -"@storybook/preview-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/preview-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/9b77288f2f627a7c70cfd3e88bdc4348c1425fab6b333ed77efb913e45881bcdf2ca67f1174e47fe978a7993c2a653fb8accf518ba51441cfb9450145101e9d8 - languageName: node - linkType: hard - -"@storybook/react-dom-shim@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/react-dom-shim@npm:8.6.14" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - checksum: 10/d14d970b98dc8266d4df80225538fb537449c15877f0f54465dfe52242f0c3f041c975824cc2894f72a3a8023fdffa0a281d90325b95297858e3684913b3cc15 - languageName: node - linkType: hard - -"@storybook/react@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/react@npm:8.6.14" - dependencies: - "@storybook/components": "npm:8.6.14" - "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.6.14" - "@storybook/preview-api": "npm:8.6.14" - "@storybook/react-dom-shim": "npm:8.6.14" - "@storybook/theming": "npm:8.6.14" - peerDependencies: - "@storybook/test": 8.6.14 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - typescript: ">= 4.2.x" - peerDependenciesMeta: - "@storybook/test": - optional: true - typescript: - optional: true - checksum: 10/a8710dcb80da9df4a78cfb3560a8c0c31c6f6c04902aa95446d4e40f6a9179770ead6f9a0a06fa44d00677cd1cd92eb60fdc57f433035ec722275847139671a4 - languageName: node - linkType: hard - -"@storybook/theming@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/theming@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/6936ea3348968fe598ad47421c11a78c6ee2ce62336ea1ce9cb8257e9faa2553d3ac3e443f8a36d35a41b0d60eb169231516649c710582ec68fdead4f23ffc0e - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -1212,12 +1131,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20": - version: 20.19.11 - resolution: "@types/node@npm:20.19.11" +"@types/node@npm:^22.13.14": + version: 22.19.2 + resolution: "@types/node@npm:22.19.2" dependencies: undici-types: "npm:~6.21.0" - checksum: 10/7179ed464a781cad20117362df58c12839b40922b53356e1a74f86c99d9d1ef9a27edce360901608f896c247c9c4131ba9c7222331e92cde17b723c75d5c8f2e + checksum: 10/c7e72fde4aef120d984416f4aa01d23b0704eeed7a0a63cdf6178783361f3985abb02a65133b80f47226ad61f38a27fe82c3cd6dd6a62d5b82bc643fe54b9c97 languageName: node linkType: hard @@ -1390,9 +1309,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.25.1": - version: 4.25.1 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.1" +"@uiw/codemirror-extensions-basic-setup@npm:4.25.4": + version: 4.25.4 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.4" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -1409,13 +1328,13 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/61d128a7896e086c3cc0558e0b5f85715f5387ee983cc9f1e341db4b383ade1973f389a63cdf9312a5421a0f5a6044755a573d6dd7bb805dc256d9f9ce4f0d15 + checksum: 10/8fa523af7cb4ea36db5263221e5a34ecbf18e57ee5c57fbb46f7bfd08ff08523b9fe2c5ae376117d98edaa1a904d1a2966123710e0d866a78ec2d1359955773c languageName: node linkType: hard "@uiw/codemirror-themes@npm:^4.23.7": - version: 4.25.1 - resolution: "@uiw/codemirror-themes@npm:4.25.1" + version: 4.25.4 + resolution: "@uiw/codemirror-themes@npm:4.25.4" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" @@ -1424,19 +1343,19 @@ __metadata: "@codemirror/language": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/337316f7c15a57bca4824498769448e38861197ab3d73b3bc58b2193028751c525a175af06f1f3984fd25b001f990e1d30ce442ddc756642698d9f8adff7f95a + checksum: 10/aab62e2c16a10dbde4592cef704d386c1a9e3caece4ff0eed85e9d9a5acdeaf3fd8717e2cb6a6795e12e890b8600956effb52d453aacbc618934e7ae8c3e59e3 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.23.7": - version: 4.25.1 - resolution: "@uiw/react-codemirror@npm:4.25.1" + version: 4.25.4 + resolution: "@uiw/react-codemirror@npm:4.25.4" dependencies: "@babel/runtime": "npm:^7.18.6" "@codemirror/commands": "npm:^6.1.0" "@codemirror/state": "npm:^6.1.1" "@codemirror/theme-one-dark": "npm:^6.0.0" - "@uiw/codemirror-extensions-basic-setup": "npm:4.25.1" + "@uiw/codemirror-extensions-basic-setup": "npm:4.25.4" codemirror: "npm:^6.0.0" peerDependencies: "@babel/runtime": ">=7.11.0" @@ -1446,7 +1365,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/32311a3b2037bf57533bd4d7c50117385c6e3cfce8dcd45f2c2ff8d9170f8ae53bc868b4ac91a58f5502ed065fce11bfffb8af5e829e8a5609290053dcdb0b2e + checksum: 10/679966cfa5cc2e7d5b7c8b40060018bebfc3a2d66492672c64e659e1c719cedd7a10ed7fb3aa014d77c42927581c4ebdf6760ab56568225781798b35dc13b8c6 languageName: node linkType: hard @@ -1804,15 +1723,6 @@ __metadata: languageName: node linkType: hard -"ast-types@npm:^0.16.1": - version: 0.16.1 - resolution: "ast-types@npm:0.16.1" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/f569b475eb1c8cb93888cb6e7b7e36dc43fa19a77e4eb132cbff6e3eb1598ca60f850db6e60b070e5a0ee8c1559fca921dac0916e576f2f104e198793b0bdd8d - languageName: node - linkType: hard - "astring@npm:^1.8.0": version: 1.9.0 resolution: "astring@npm:1.9.0" @@ -1866,12 +1776,10 @@ __metadata: languageName: node linkType: hard -"better-opn@npm:^3.0.2": - version: 3.0.2 - resolution: "better-opn@npm:3.0.2" - dependencies: - open: "npm:^8.0.4" - checksum: 10/24668e5a837d0d2c0edf17ad5ebcfeb00a8a5578a5eb09f7a409e1a60617cdfea40b8ebfc95e5f12d9568157930d033e6805788fcf0780413ac982c95d3745d1 +"before-after-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "before-after-hook@npm:4.0.0" + checksum: 10/9fd52bc0c3cca0fb115e04dacbeeaacff38fa23e1af725d62392254c31ef433b15da60efcba61552e44d64e26f25ea259f72dba005115924389e88d2fd56e19f languageName: node linkType: hard @@ -1910,13 +1818,6 @@ __metadata: languageName: node linkType: hard -"browser-assert@npm:^1.2.1": - version: 1.2.1 - resolution: "browser-assert@npm:1.2.1" - checksum: 10/8b2407cd04c1ed592cf892dec35942b7d72635829221e0788c9a16c4d2afa8b7156bc9705b1c4b32c30d88136c576fda3cbcb8f494d6f865264c706ea8798d92 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -1977,9 +1878,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001579": - version: 1.0.30001735 - resolution: "caniuse-lite@npm:1.0.30001735" - checksum: 10/4b404ed363a5ccf3ec07144c2360106aa47dcec1f896ea6705a2773c6977e9ffb19a2b58be9492462733170e29b4823211533b0986d5a25f0f65f7ff702470ab + version: 1.0.30001760 + resolution: "caniuse-lite@npm:1.0.30001760" + checksum: 10/ac5c13d00b946c3ace331d25379ed9d85743b1028aba79ae80402cb128b4b491122585144898fefb836d4d9879c13c717a081ae241a00420cbbc7e1b934ec685 languageName: node linkType: hard @@ -1990,7 +1891,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -2061,14 +1962,10 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^8.0.1": - version: 8.0.1 - resolution: "cliui@npm:8.0.1" - dependencies: - string-width: "npm:^4.2.0" - strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^7.0.0" - checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 languageName: node linkType: hard @@ -2144,26 +2041,6 @@ __metadata: languageName: node linkType: hard -"concurrently@npm:^8.2.2": - version: 8.2.2 - resolution: "concurrently@npm:8.2.2" - dependencies: - chalk: "npm:^4.1.2" - date-fns: "npm:^2.30.0" - lodash: "npm:^4.17.21" - rxjs: "npm:^7.8.1" - shell-quote: "npm:^1.8.1" - spawn-command: "npm:0.0.2" - supports-color: "npm:^8.1.1" - tree-kill: "npm:^1.2.2" - yargs: "npm:^17.7.2" - bin: - conc: dist/bin/concurrently.js - concurrently: dist/bin/concurrently.js - checksum: 10/dcb1aa69d9c611a7bda9d4fc0fe1e388f971d1744acec7e0d52dffa2ef55743f1266ec9292f414c5789b9f61734b3fce772bd005d4de9564a949fb121b97bae1 - languageName: node - linkType: hard - "crelt@npm:^1.0.5, crelt@npm:^1.0.6": version: 1.0.6 resolution: "crelt@npm:1.0.6" @@ -2229,15 +2106,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.30.0": - version: 2.30.0 - resolution: "date-fns@npm:2.30.0" - dependencies: - "@babel/runtime": "npm:^7.21.0" - checksum: 10/70b3e8ea7aaaaeaa2cd80bd889622a4bcb5d8028b4de9162cbcda359db06e16ff6e9309e54eead5341e71031818497f19aaf9839c87d1aba1e27bb4796e758a9 - languageName: node - linkType: hard - "debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0": version: 4.4.1 resolution: "debug@npm:4.4.1" @@ -2286,13 +2154,6 @@ __metadata: languageName: node linkType: hard -"define-lazy-prop@npm:^2.0.0": - version: 2.0.0 - resolution: "define-lazy-prop@npm:2.0.0" - checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 - languageName: node - linkType: hard - "define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" @@ -2331,36 +2192,37 @@ __metadata: version: 0.0.0-use.local resolution: "docs-ui@workspace:." dependencies: + "@base-ui-components/react": "npm:^1.0.0-beta.4" "@codemirror/lang-sass": "npm:^6.0.2" "@codemirror/view": "npm:^6.34.4" "@lezer/highlight": "npm:^1.2.1" "@mdx-js/loader": "npm:^3.1.0" "@mdx-js/react": "npm:^3.1.0" - "@next/mdx": "npm:15.3.4" + "@next/mdx": "npm:15.5.7" + "@octokit/rest": "npm:^22.0.1" "@remixicon/react": "npm:^4.6.0" "@shikijs/transformers": "npm:^3.13.0" - "@storybook/react": "npm:^8.6.12" "@types/mdx": "npm:^2.0.13" - "@types/node": "npm:^20" + "@types/node": "npm:^22.13.14" "@types/react": "npm:19.1.9" "@types/react-dom": "npm:19.1.7" "@uiw/codemirror-themes": "npm:^4.23.7" "@uiw/react-codemirror": "npm:^4.23.7" chokidar: "npm:^3.6.0" - concurrently: "npm:^8.2.2" + clsx: "npm:^2.1.1" eslint: "npm:^8" - eslint-config-next: "npm:15.3.4" + eslint-config-next: "npm:15.5.7" html-react-parser: "npm:^5.2.5" lightningcss: "npm:^1.28.2" motion: "npm:^12.4.1" - next: "npm:15.4.7" + next: "npm:15.5.9" next-mdx-remote-client: "npm:^2.1.2" prop-types: "npm:^15.8.1" react: "npm:19.1.1" react-dom: "npm:19.1.1" shiki: "npm:^3.13.0" - storybook: "npm:^8.6.12" typescript: "npm:^5" + unified: "npm:^11.0.4" languageName: unknown linkType: soft @@ -2654,113 +2516,6 @@ __metadata: languageName: node linkType: hard -"esbuild-register@npm:^3.5.0": - version: 3.6.0 - resolution: "esbuild-register@npm:3.6.0" - dependencies: - debug: "npm:^4.3.4" - peerDependencies: - esbuild: ">=0.12 <1" - checksum: 10/4ae1a016e3dad5b53c3d68cf07e31d8c1cec1a0b584038ece726097ac80bd33ab48fb224c766c9b341c04793837e652461eaca9327a116e7564f553b61ccca71 - languageName: node - linkType: hard - -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0": - version: 0.25.9 - resolution: "esbuild@npm:0.25.9" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.9" - "@esbuild/android-arm": "npm:0.25.9" - "@esbuild/android-arm64": "npm:0.25.9" - "@esbuild/android-x64": "npm:0.25.9" - "@esbuild/darwin-arm64": "npm:0.25.9" - "@esbuild/darwin-x64": "npm:0.25.9" - "@esbuild/freebsd-arm64": "npm:0.25.9" - "@esbuild/freebsd-x64": "npm:0.25.9" - "@esbuild/linux-arm": "npm:0.25.9" - "@esbuild/linux-arm64": "npm:0.25.9" - "@esbuild/linux-ia32": "npm:0.25.9" - "@esbuild/linux-loong64": "npm:0.25.9" - "@esbuild/linux-mips64el": "npm:0.25.9" - "@esbuild/linux-ppc64": "npm:0.25.9" - "@esbuild/linux-riscv64": "npm:0.25.9" - "@esbuild/linux-s390x": "npm:0.25.9" - "@esbuild/linux-x64": "npm:0.25.9" - "@esbuild/netbsd-arm64": "npm:0.25.9" - "@esbuild/netbsd-x64": "npm:0.25.9" - "@esbuild/openbsd-arm64": "npm:0.25.9" - "@esbuild/openbsd-x64": "npm:0.25.9" - "@esbuild/openharmony-arm64": "npm:0.25.9" - "@esbuild/sunos-x64": "npm:0.25.9" - "@esbuild/win32-arm64": "npm:0.25.9" - "@esbuild/win32-ia32": "npm:0.25.9" - "@esbuild/win32-x64": "npm:0.25.9" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@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-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/openharmony-arm64": - 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: 10/fc174ae7f646ad413adb641c7e46f16be575e462ed209866b55d5954d382e5da839e3f3f89a8e42e2b71d48895cc636ba43523011249fe5ff9c63d8d39d3a364 - languageName: node - linkType: hard - -"escalade@npm:^3.1.1": - version: 3.2.0 - resolution: "escalade@npm:3.2.0" - checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -2768,11 +2523,11 @@ __metadata: languageName: node linkType: hard -"eslint-config-next@npm:15.3.4": - version: 15.3.4 - resolution: "eslint-config-next@npm:15.3.4" +"eslint-config-next@npm:15.5.7": + version: 15.5.7 + resolution: "eslint-config-next@npm:15.5.7" dependencies: - "@next/eslint-plugin-next": "npm:15.3.4" + "@next/eslint-plugin-next": "npm:15.5.7" "@rushstack/eslint-patch": "npm:^1.10.3" "@typescript-eslint/eslint-plugin": "npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" "@typescript-eslint/parser": "npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" @@ -2788,7 +2543,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/6c21254d3383b9158ff5f3b2881cc702bee3d2635b4326757965945691f6e65e25fdfef4f2964382fb4b2f52d9f03b929cb71d267709727df7365e7da80c8c3a + checksum: 10/fd1813c7ef4e33ec297b86dd0de75879e10514b12cb04a103e7a5b8c5f27d2ea0a91dd15e6d530006c9fe3e7e18f815e900bd6ba117c3b8fbe311c38c340d899 languageName: node linkType: hard @@ -3013,16 +2768,6 @@ __metadata: languageName: node linkType: hard -"esprima@npm:~4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10/f1d3c622ad992421362294f7acf866aa9409fbad4eb2e8fa230bd33944ce371d32279667b242d8b8907ec2b6ad7353a717f3c0e60e748873a34a7905174bc0eb - languageName: node - linkType: hard - "esquery@npm:^1.4.2": version: 1.6.0 resolution: "esquery@npm:1.6.0" @@ -3137,6 +2882,13 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-content-type-parse@npm:3.0.0" + checksum: 10/8616a8aa6c9b4f8f4f3c90eaa4e7bfc2240cfa6f41f0eef5b5aa2b2c8b38bd9ad435f1488b6d817ffd725c54651e2777b882ae9dd59366e71e7896f1ec11d473 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -3270,11 +3022,11 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^12.23.12": - version: 12.23.12 - resolution: "framer-motion@npm:12.23.12" +"framer-motion@npm:^12.23.26": + version: 12.23.26 + resolution: "framer-motion@npm:12.23.26" dependencies: - motion-dom: "npm:^12.23.12" + motion-dom: "npm:^12.23.23" motion-utils: "npm:^12.23.6" tslib: "npm:^2.4.0" peerDependencies: @@ -3288,7 +3040,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/c15af3dc22476bf10d7509b9a336d113ed0fbeba57fca81ce5a6bf07f0d659ebc3588c3a839475cbdfc9601b64e2fc7551911e1c806dd2b896603c8a2c0c3870 + checksum: 10/29d51ac57ce8c2599664bf706d7a83c2aba4fe1dee1c0c7d75d5c45c120b1c2ca5d257c3ff608992b26bf8e9cf258cacf231b0873d7b16f01c5eeb42b9831f78 languageName: node linkType: hard @@ -3355,13 +3107,6 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": - version: 2.0.5 - resolution: "get-caller-file@npm:2.0.5" - checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 - languageName: node - linkType: hard - "get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" @@ -3630,31 +3375,31 @@ __metadata: languageName: node linkType: hard -"html-dom-parser@npm:5.1.1": - version: 5.1.1 - resolution: "html-dom-parser@npm:5.1.1" +"html-dom-parser@npm:5.1.2": + version: 5.1.2 + resolution: "html-dom-parser@npm:5.1.2" dependencies: domhandler: "npm:5.0.3" htmlparser2: "npm:10.0.0" - checksum: 10/85bbaa8699d40d54fd64f5472088199178a6c3dd6a907358ac476d9c511cb8ab10bff1469a785411e8b831a73ac08348392e56076eab6d100a3ec19dccc722ad + checksum: 10/d54fcdafbc0b2881c441a396965ad285072d2e4df62bebe7074d7aaa2a1fffc95e9286bcb81d6f241c4cdb10a84435fa623738985b635574acb3c84e994e8bb6 languageName: node linkType: hard "html-react-parser@npm:^5.2.5": - version: 5.2.6 - resolution: "html-react-parser@npm:5.2.6" + version: 5.2.11 + resolution: "html-react-parser@npm:5.2.11" dependencies: domhandler: "npm:5.0.3" - html-dom-parser: "npm:5.1.1" + html-dom-parser: "npm:5.1.2" react-property: "npm:2.0.2" - style-to-js: "npm:1.1.17" + style-to-js: "npm:1.1.21" peerDependencies: "@types/react": 0.14 || 15 || 16 || 17 || 18 || 19 react: 0.14 || 15 || 16 || 17 || 18 || 19 peerDependenciesMeta: "@types/react": optional: true - checksum: 10/be2903fd932d44ff6cae66bd18b025d318879e16eb1e67c0ae6dc640e5c77509da41b61948c0d404a43b5362b57b7cfa3963df242b9773d4c7a16f410db7f2ac + checksum: 10/5b52699e63e6ebd7c1c2d76f6b5214fdbf017571785c5c01f0630704a84fcd3b75a9539e0f4e8bb65e91d8871ee3e80980baa21660cfd9749587dafd96a959bf languageName: node linkType: hard @@ -3754,17 +3499,17 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3": +"inherits@npm:2": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 languageName: node linkType: hard -"inline-style-parser@npm:0.2.4": - version: 0.2.4 - resolution: "inline-style-parser@npm:0.2.4" - checksum: 10/80814479d1f3c9cbd102f9de4cd6558cf43cc2e48640e81c4371c3634f1e8b6dfeb2f21063cfa31d46cc83e834c20cd59ed9eeed9bfd45ef5bc02187ad941faf +"inline-style-parser@npm:0.2.7": + version: 0.2.7 + resolution: "inline-style-parser@npm:0.2.7" + checksum: 10/cdfe719bd694b53bfbad20a645a9a4dda89c3ff56ee2b95945ad4eea86c541501f49f852d23bc2f73cd8127b6b62ea9027f697838e323a7f7d0d9b760e27a632 languageName: node linkType: hard @@ -3803,16 +3548,6 @@ __metadata: languageName: node linkType: hard -"is-arguments@npm:^1.0.4": - version: 1.2.0 - resolution: "is-arguments@npm:1.2.0" - dependencies: - call-bound: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.2" - checksum: 10/471a8ef631b8ee8829c43a8ab05c081700c0e25180c73d19f3bf819c1a8448c426a9e8e601f278973eca68966384b16ceb78b8c63af795b099cd199ea5afc457 - languageName: node - linkType: hard - "is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" @@ -3925,15 +3660,6 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" - bin: - is-docker: cli.js - checksum: 10/3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -3957,7 +3683,7 @@ __metadata: languageName: node linkType: hard -"is-generator-function@npm:^1.0.10, is-generator-function@npm:^1.0.7": +"is-generator-function@npm:^1.0.10": version: 1.1.0 resolution: "is-generator-function@npm:1.1.0" dependencies: @@ -4079,7 +3805,7 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15, is-typed-array@npm:^1.1.3": +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": version: 1.1.15 resolution: "is-typed-array@npm:1.1.15" dependencies: @@ -4114,15 +3840,6 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" - dependencies: - is-docker: "npm:^2.0.0" - checksum: 10/20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 - languageName: node - linkType: hard - "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -4179,20 +3896,13 @@ __metadata: linkType: hard "js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" dependencies: argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 - languageName: node - linkType: hard - -"jsdoc-type-pratt-parser@npm:^4.0.0": - version: 4.8.0 - resolution: "jsdoc-type-pratt-parser@npm:4.8.0" - checksum: 10/1844ef7848f5d0c3c60af95629197f97ff8603c12b99d750aaf7511724ee0d41989c5d24ea5f8d22078a5a8034337a272c9c22f49a8f07a857946417f3c84cfb + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 languageName: node linkType: hard @@ -4275,92 +3985,102 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-arm64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-darwin-arm64@npm:1.30.1" +"lightningcss-android-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-android-arm64@npm:1.30.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-arm64@npm:1.30.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"lightningcss-darwin-x64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-darwin-x64@npm:1.30.1" +"lightningcss-darwin-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-x64@npm:1.30.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"lightningcss-freebsd-x64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-freebsd-x64@npm:1.30.1" +"lightningcss-freebsd-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-freebsd-x64@npm:1.30.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"lightningcss-linux-arm-gnueabihf@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.1" +"lightningcss-linux-arm-gnueabihf@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"lightningcss-linux-arm64-gnu@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm64-gnu@npm:1.30.1" +"lightningcss-linux-arm64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-gnu@npm:1.30.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-arm64-musl@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm64-musl@npm:1.30.1" +"lightningcss-linux-arm64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-musl@npm:1.30.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"lightningcss-linux-x64-gnu@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-x64-gnu@npm:1.30.1" +"lightningcss-linux-x64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-gnu@npm:1.30.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-x64-musl@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-x64-musl@npm:1.30.1" +"lightningcss-linux-x64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-musl@npm:1.30.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"lightningcss-win32-arm64-msvc@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-win32-arm64-msvc@npm:1.30.1" +"lightningcss-win32-arm64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-arm64-msvc@npm:1.30.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"lightningcss-win32-x64-msvc@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-win32-x64-msvc@npm:1.30.1" +"lightningcss-win32-x64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-x64-msvc@npm:1.30.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "lightningcss@npm:^1.28.2": - version: 1.30.1 - resolution: "lightningcss@npm:1.30.1" + version: 1.30.2 + resolution: "lightningcss@npm:1.30.2" dependencies: detect-libc: "npm:^2.0.3" - lightningcss-darwin-arm64: "npm:1.30.1" - lightningcss-darwin-x64: "npm:1.30.1" - lightningcss-freebsd-x64: "npm:1.30.1" - lightningcss-linux-arm-gnueabihf: "npm:1.30.1" - lightningcss-linux-arm64-gnu: "npm:1.30.1" - lightningcss-linux-arm64-musl: "npm:1.30.1" - lightningcss-linux-x64-gnu: "npm:1.30.1" - lightningcss-linux-x64-musl: "npm:1.30.1" - lightningcss-win32-arm64-msvc: "npm:1.30.1" - lightningcss-win32-x64-msvc: "npm:1.30.1" + lightningcss-android-arm64: "npm:1.30.2" + lightningcss-darwin-arm64: "npm:1.30.2" + lightningcss-darwin-x64: "npm:1.30.2" + lightningcss-freebsd-x64: "npm:1.30.2" + lightningcss-linux-arm-gnueabihf: "npm:1.30.2" + lightningcss-linux-arm64-gnu: "npm:1.30.2" + lightningcss-linux-arm64-musl: "npm:1.30.2" + lightningcss-linux-x64-gnu: "npm:1.30.2" + lightningcss-linux-x64-musl: "npm:1.30.2" + lightningcss-win32-arm64-msvc: "npm:1.30.2" + lightningcss-win32-x64-msvc: "npm:1.30.2" dependenciesMeta: + lightningcss-android-arm64: + optional: true lightningcss-darwin-arm64: optional: true lightningcss-darwin-x64: @@ -4381,7 +4101,7 @@ __metadata: optional: true lightningcss-win32-x64-msvc: optional: true - checksum: 10/6c921135216cc498dfcb87e35dffe8432e99306cbd58009c598b1daf20c81cc14535abbd4c1066e5d1faf4080ed44a2995e8ecc343633db4897a2d041b76fb05 + checksum: 10/d6cc06d9bac295589a49446e9c45a241dfa16f4f81a7318c26cbc0be3e189003ec0da5d9a0fd9bdffc63a3ce05878cc7329277eaac77a826e8b68c73dc96cfda languageName: node linkType: hard @@ -4401,13 +4121,6 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 - languageName: node - linkType: hard - "longest-streak@npm:^3.0.0": version: 3.1.0 resolution: "longest-streak@npm:3.1.0" @@ -4558,8 +4271,8 @@ __metadata: linkType: hard "mdast-util-to-hast@npm:^13.0.0": - version: 13.2.0 - resolution: "mdast-util-to-hast@npm:13.2.0" + version: 13.2.1 + resolution: "mdast-util-to-hast@npm:13.2.1" dependencies: "@types/hast": "npm:^3.0.0" "@types/mdast": "npm:^4.0.0" @@ -4570,7 +4283,7 @@ __metadata: unist-util-position: "npm:^5.0.0" unist-util-visit: "npm:^5.0.0" vfile: "npm:^6.0.0" - checksum: 10/b17ee338f843af31a1c7a2ebf0df6f0b41c9380b7119a63ab521d271df665456578e1234bb7617883e8d860fe878038dcf2b76ab2f21e0f7451215a096d26cce + checksum: 10/8fddf5e66ea24dc85c8fe1cc2acd8fbe36e9d4f21b06322e156431fd71385eab9d2d767646f50276ca4ce3684cb967c4e226c60c3fff3428feb687ccb598fa39 languageName: node linkType: hard @@ -5062,21 +4775,21 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.1": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" - bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba +"minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 languageName: node linkType: hard -"motion-dom@npm:^12.23.12": - version: 12.23.12 - resolution: "motion-dom@npm:12.23.12" +"motion-dom@npm:^12.23.23": + version: 12.23.23 + resolution: "motion-dom@npm:12.23.23" dependencies: motion-utils: "npm:^12.23.6" - checksum: 10/e13cbd1f32ad30dc642ebc83f7f35d0fb76c4d93902daebdbbea2cdb85bb07481fb4fe315b3e2c899076dab5490fb611ff803c334b336bc4fcd59b9a1aa6d7ff + checksum: 10/6bec34c28b478622bd23ce919c0d64e7f372cbc3f88e665f1e34fbf590e9bb53eebea086fb28f4e0c31941bd5274e086dc9b5360b68c4976891b94c4e92ca01a languageName: node linkType: hard @@ -5088,10 +4801,10 @@ __metadata: linkType: hard "motion@npm:^12.4.1": - version: 12.23.12 - resolution: "motion@npm:12.23.12" + version: 12.23.26 + resolution: "motion@npm:12.23.26" dependencies: - framer-motion: "npm:^12.23.12" + framer-motion: "npm:^12.23.26" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -5104,7 +4817,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/95670351c14d6925e891fac3d12719ed2e46000366abd38096870c46271071e3f22d0e9fa523809e614ee58aaeb1865fc19398dc8b408c2594512dc0eef392db + checksum: 10/c5e683d2c67e55816587cbbd451c5600dc0602aa04108ab0bf8d4472befd490e90b3177362dbc8b9f96316ec16383924cc574eb7c9e031540f1e88e64a2704ae languageName: node linkType: hard @@ -5148,36 +4861,36 @@ __metadata: linkType: hard "next-mdx-remote-client@npm:^2.1.2": - version: 2.1.3 - resolution: "next-mdx-remote-client@npm:2.1.3" + version: 2.1.7 + resolution: "next-mdx-remote-client@npm:2.1.7" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@mdx-js/mdx": "npm:^3.1.0" - "@mdx-js/react": "npm:^3.1.0" - remark-mdx-remove-esm: "npm:^1.2.0" + "@mdx-js/mdx": "npm:^3.1.1" + "@mdx-js/react": "npm:^3.1.1" + remark-mdx-remove-esm: "npm:^1.2.1" serialize-error: "npm:^12.0.0" vfile: "npm:^6.0.3" vfile-matter: "npm:^5.0.1" peerDependencies: - react: ^19.1.0 - react-dom: ^19.1.0 - checksum: 10/3837c63edf7d707de2eea9b4c565eeb7e505a769e93c2673f746ec9993c644f7cd0aacb8bad911eff9ea63190f48c2c16ffe98d47c39591c5dac29c8bf9a9e0c + react: ^19.1 + react-dom: ^19.1 + checksum: 10/8aaf6c2f11b0ed398703165c35c0355d1f058521780191684e6fe7e45698effe50ba68b154bae9db84b13e82dff98f380224d588acca6e389aeb73062ecdd31d languageName: node linkType: hard -"next@npm:15.4.7": - version: 15.4.7 - resolution: "next@npm:15.4.7" +"next@npm:15.5.9": + version: 15.5.9 + resolution: "next@npm:15.5.9" dependencies: - "@next/env": "npm:15.4.7" - "@next/swc-darwin-arm64": "npm:15.4.7" - "@next/swc-darwin-x64": "npm:15.4.7" - "@next/swc-linux-arm64-gnu": "npm:15.4.7" - "@next/swc-linux-arm64-musl": "npm:15.4.7" - "@next/swc-linux-x64-gnu": "npm:15.4.7" - "@next/swc-linux-x64-musl": "npm:15.4.7" - "@next/swc-win32-arm64-msvc": "npm:15.4.7" - "@next/swc-win32-x64-msvc": "npm:15.4.7" + "@next/env": "npm:15.5.9" + "@next/swc-darwin-arm64": "npm:15.5.7" + "@next/swc-darwin-x64": "npm:15.5.7" + "@next/swc-linux-arm64-gnu": "npm:15.5.7" + "@next/swc-linux-arm64-musl": "npm:15.5.7" + "@next/swc-linux-x64-gnu": "npm:15.5.7" + "@next/swc-linux-x64-musl": "npm:15.5.7" + "@next/swc-win32-arm64-msvc": "npm:15.5.7" + "@next/swc-win32-x64-msvc": "npm:15.5.7" "@swc/helpers": "npm:0.5.15" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" @@ -5220,7 +4933,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/e611751247d5cfff9337a84979918bcd61919d7b66a411c982569336c3dba7e7a8704eead7e082e579807200fa8bc80602a3e2ce4423a8b5387aab3353400918 + checksum: 10/ac27b82de08c9720e8e99cd64102af5e30306a8c861630d50da347a02c288cc441273ada64875c47e7a4d5991ff69cc92b23e906ce3271f9835a7911a0f060cc languageName: node linkType: hard @@ -5360,25 +5073,14 @@ __metadata: languageName: node linkType: hard -"oniguruma-to-es@npm:^4.3.3": - version: 4.3.3 - resolution: "oniguruma-to-es@npm:4.3.3" +"oniguruma-to-es@npm:^4.3.4": + version: 4.3.4 + resolution: "oniguruma-to-es@npm:4.3.4" dependencies: oniguruma-parser: "npm:^0.12.1" regex: "npm:^6.0.1" regex-recursion: "npm:^6.0.2" - checksum: 10/49b372569d335077c32bda066ac1da4a3f15dd25b717025cf43417fabd71d56e1152debcd8a832596e180d721b808822c88e65eb12abb26665ab8fe017f3c861 - languageName: node - linkType: hard - -"open@npm:^8.0.4": - version: 8.4.2 - resolution: "open@npm:8.4.2" - dependencies: - define-lazy-prop: "npm:^2.0.0" - is-docker: "npm:^2.1.1" - is-wsl: "npm:^2.2.0" - checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9 + checksum: 10/29be3f677cd948da7ea77c5ad62d0405daec58b015b4e4706c97d9f3dcd3a82a68a7314b90ec5dcd261a934aca9aa0bd8100180e2cbd252bc8a20e32fc9ab851 languageName: node linkType: hard @@ -5554,13 +5256,6 @@ __metadata: languageName: node linkType: hard -"process@npm:^0.11.10": - version: 0.11.10 - resolution: "process@npm:0.11.10" - checksum: 10/dbaa7e8d1d5cf375c36963ff43116772a989ef2bb47c9bdee20f38fd8fc061119cf38140631cf90c781aca4d3f0f0d2c834711952b728953f04fd7d238f59f5b - languageName: node - linkType: hard - "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -5644,19 +5339,6 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.23.5": - version: 0.23.11 - resolution: "recast@npm:0.23.11" - dependencies: - ast-types: "npm:^0.16.1" - esprima: "npm:~4.0.0" - source-map: "npm:~0.6.1" - tiny-invariant: "npm:^1.3.3" - tslib: "npm:^2.0.1" - checksum: 10/a622b7848efe13a59a40c9a1a3a8178433eae1048780e04d7392406e2d67fc29e3efa84b3aa8cfda28fd58989f4b59fa968bed295b739987a666bd11cc57a5b2 - languageName: node - linkType: hard - "recma-build-jsx@npm:^1.0.0": version: 1.0.0 resolution: "recma-build-jsx@npm:1.0.0" @@ -5773,16 +5455,16 @@ __metadata: languageName: node linkType: hard -"remark-mdx-remove-esm@npm:^1.2.0": - version: 1.2.0 - resolution: "remark-mdx-remove-esm@npm:1.2.0" +"remark-mdx-remove-esm@npm:^1.2.1": + version: 1.2.1 + resolution: "remark-mdx-remove-esm@npm:1.2.1" dependencies: "@types/mdast": "npm:^4.0.4" mdast-util-mdxjs-esm: "npm:^2.0.1" unist-util-remove: "npm:^4.0.0" peerDependencies: unified: ^11 - checksum: 10/0a1155e08395ab73f877948455fc04d7457864f7556617efabecae7ea1b441cec2ca7ee9672e16a6cd56f2a16c57a4778f901e77d19052d1dd245de5bdf4618f + checksum: 10/092e95ca0649cfe08c47900d9922172b531e1d60b0f90955f51a7282620a1ae53a95dddde3f8d789cdd114a2d515a9372bc72b3e46200ff6f930bd89f0985982 languageName: node linkType: hard @@ -5821,10 +5503,10 @@ __metadata: languageName: node linkType: hard -"require-directory@npm:^2.1.1": - version: 2.1.1 - resolution: "require-directory@npm:2.1.1" - checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf +"reselect@npm:^5.1.1": + version: 5.1.1 + resolution: "reselect@npm:5.1.1" + checksum: 10/1fdae11a39ed9c8d85a24df19517c8372ee24fefea9cce3fae9eaad8e9cefbba5a3d4940c6fe31296b6addf76e035588c55798f7e6e147e1b7c0855f119e7fa5 languageName: node linkType: hard @@ -5928,15 +5610,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.8.1": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -5994,7 +5667,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.7.1, semver@npm:^7.7.2": +"semver@npm:^7.3.5, semver@npm:^7.6.0, semver@npm:^7.7.1, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -6143,26 +5816,19 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.8.1": - version: 1.8.3 - resolution: "shell-quote@npm:1.8.3" - checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c - languageName: node - linkType: hard - "shiki@npm:^3.13.0": - version: 3.13.0 - resolution: "shiki@npm:3.13.0" + version: 3.19.0 + resolution: "shiki@npm:3.19.0" dependencies: - "@shikijs/core": "npm:3.13.0" - "@shikijs/engine-javascript": "npm:3.13.0" - "@shikijs/engine-oniguruma": "npm:3.13.0" - "@shikijs/langs": "npm:3.13.0" - "@shikijs/themes": "npm:3.13.0" - "@shikijs/types": "npm:3.13.0" + "@shikijs/core": "npm:3.19.0" + "@shikijs/engine-javascript": "npm:3.19.0" + "@shikijs/engine-oniguruma": "npm:3.19.0" + "@shikijs/langs": "npm:3.19.0" + "@shikijs/themes": "npm:3.19.0" + "@shikijs/types": "npm:3.19.0" "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" - checksum: 10/8d7cd039ac8df548f724047b4ba81cd84c09e8b96f411eb7c781d3d105c96ff2ec6f64e1bac724fd6fc56ac2926322bc2466124ea279d04c5ef668d5a04219bb + checksum: 10/004dabc26a9db25c69f1aa6ed0555c8a5a34153084a1aa5f42fa03edf800ca13dd468ad6d7ba4cbf8f36dc8d316a7de29cecec228ea8d2d252f4c4c95eb162e6 languageName: node linkType: hard @@ -6272,13 +5938,6 @@ __metadata: languageName: node linkType: hard -"source-map@npm:~0.6.1": - version: 0.6.1 - resolution: "source-map@npm:0.6.1" - checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff - languageName: node - linkType: hard - "space-separated-tokens@npm:^2.0.0": version: 2.0.2 resolution: "space-separated-tokens@npm:2.0.2" @@ -6286,13 +5945,6 @@ __metadata: languageName: node linkType: hard -"spawn-command@npm:0.0.2": - version: 0.0.2 - resolution: "spawn-command@npm:0.0.2" - checksum: 10/f13e8c3c63abd4a0b52fb567eba5f7940d480c5ed3ec61781d38a1850f179b1196c39e6efa2bbd301f82c1bf1cd7807abc8fbd8fc8e44bcaa3975a124c0d1657 - languageName: node - linkType: hard - "ssri@npm:^12.0.0": version: 12.0.0 resolution: "ssri@npm:12.0.0" @@ -6319,25 +5971,7 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.6.12": - version: 8.6.14 - resolution: "storybook@npm:8.6.14" - dependencies: - "@storybook/core": "npm:8.6.14" - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - bin: - getstorybook: ./bin/index.cjs - sb: ./bin/index.cjs - storybook: ./bin/index.cjs - checksum: 10/104932fe29ebf49bef24c90285741cec964d1c36b3f0b38da1dace31ac664be457f2a510e41ba69fe8aa0d90c20e3446fec27d1ced41e5eefb3a9ca713d99e79 - languageName: node - linkType: hard - -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -6488,21 +6122,21 @@ __metadata: languageName: node linkType: hard -"style-to-js@npm:1.1.17, style-to-js@npm:^1.0.0": - version: 1.1.17 - resolution: "style-to-js@npm:1.1.17" +"style-to-js@npm:1.1.21, style-to-js@npm:^1.0.0": + version: 1.1.21 + resolution: "style-to-js@npm:1.1.21" dependencies: - style-to-object: "npm:1.0.9" - checksum: 10/431f2fca8a55a61939a83ff0f58638e2996621ad93a97cf93f2be5115f411330d4e506ccf18621bd45607ec161546b763bb6961ad08238ad939b6261ff377230 + style-to-object: "npm:1.0.14" + checksum: 10/5e30b4c52ed4e0294324adab2a43a0438b5495a77a72a6b1258637eebfc4dc8e0614f5ac7bf38a2f514879b3b448215d01fecf1f8d7468b8b95d90bed1d05d57 languageName: node linkType: hard -"style-to-object@npm:1.0.9": - version: 1.0.9 - resolution: "style-to-object@npm:1.0.9" +"style-to-object@npm:1.0.14": + version: 1.0.14 + resolution: "style-to-object@npm:1.0.14" dependencies: - inline-style-parser: "npm:0.2.4" - checksum: 10/fd0c131a83103fe4025afd8e0fd90c605054d485ad80f2ab402e7afa79f482f4b05fff40b6aa661cb1b835e5c56bb0644dc38cbf9b3d2982fc552435db3dae50 + inline-style-parser: "npm:0.2.7" + checksum: 10/06b86a5cf435dafac908d19082842983f9052d8cf3682915b1bd9251e3fe9b8065dbd2aef060dc5dfa0fa2ee24d717b587a5205f571513a10f30e3947f9d28ff languageName: node linkType: hard @@ -6531,15 +6165,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.1.1": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 - languageName: node - linkType: hard - "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -6547,17 +6172,23 @@ __metadata: languageName: node linkType: hard +"tabbable@npm:^6.3.0": + version: 6.3.0 + resolution: "tabbable@npm:6.3.0" + checksum: 10/3e54a0b770d26bc20c3de5837652be19f5efa8bfa869f580af24bcf60de934506e9401a577213186b5e86ebcf6b5290a5429d354cc3041471815f5095e44e51a + languageName: node + linkType: hard + "tar@npm:^7.4.3": - version: 7.4.3 - resolution: "tar@npm:7.4.3" + version: 7.5.6 + resolution: "tar@npm:7.5.6" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" - minizlib: "npm:^3.0.1" - mkdirp: "npm:^3.0.1" + minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/12a2a4fc6dee23e07cc47f1aeb3a14a1afd3f16397e1350036a8f4cdfee8dcac7ef5978337a4e7b2ac2c27a9a6d46388fc2088ea7c80cb6878c814b1425f8ecf + checksum: 10/cf4a84d79b9327fcf765f2ea16de4702b9b8dd7dc6b1840b16e0f999628d96b81b2c7efbf83d4eb42b0164856f1db887a5a61ffef97d36cdb77cac742219f9ee languageName: node linkType: hard @@ -6568,13 +6199,6 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.3.3": - version: 1.3.3 - resolution: "tiny-invariant@npm:1.3.3" - checksum: 10/5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -6594,15 +6218,6 @@ __metadata: languageName: node linkType: hard -"tree-kill@npm:^1.2.2": - version: 1.2.2 - resolution: "tree-kill@npm:1.2.2" - bin: - tree-kill: cli.js - checksum: 10/49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7 - languageName: node - linkType: hard - "trim-lines@npm:^3.0.0": version: 3.0.1 resolution: "trim-lines@npm:3.0.1" @@ -6638,7 +6253,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": +"tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -6722,22 +6337,22 @@ __metadata: linkType: hard "typescript@npm:^5": - version: 5.9.2 - resolution: "typescript@npm:5.9.2" + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/cc2fe6c822819de5d453fa25aa9f32096bf70dde215d481faa1ad84a283dfb264e33988ed8f6d36bc803dd0b16dbe943efa311a798ef76d5b3892a05dfbfd628 + checksum: 10/c089d9d3da2729fd4ac517f9b0e0485914c4b3c26f80dc0cffcb5de1719a17951e92425d55db59515c1a7ddab65808466debb864d0d56dcf43f27007d0709594 languageName: node linkType: hard "typescript@patch:typescript@npm%3A^5#optional!builtin": - version: 5.9.2 - resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/bd810ab13e8e557225a8b5122370385440b933e4e077d5c7641a8afd207fdc8be9c346e3c678adba934b64e0e70b0acf5eef9493ea05170a48ce22bef845fdc7 + checksum: 10/696e1b017bc2635f4e0c94eb4435357701008e2f272f553d06e35b494b8ddc60aa221145e286c28ace0c89ee32827a28c2040e3a69bdc108b1a5dc8fb40b72e3 languageName: node linkType: hard @@ -6760,7 +6375,7 @@ __metadata: languageName: node linkType: hard -"unified@npm:^11.0.0": +"unified@npm:^11.0.0, unified@npm:^11.0.4": version: 11.0.5 resolution: "unified@npm:11.0.5" dependencies: @@ -6861,6 +6476,13 @@ __metadata: languageName: node linkType: hard +"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": + version: 7.0.3 + resolution: "universal-user-agent@npm:7.0.3" + checksum: 10/c497e85f8b11eb8fa4dce584d7a39cc98710164959f494cafc3c269b51abb20fff269951838efd7424d15f6b3d001507f3cb8b52bb5676fdb642019dfd17e63e + languageName: node + linkType: hard + "unrs-resolver@npm:^1.6.2": version: 1.11.1 resolution: "unrs-resolver@npm:1.11.1" @@ -6937,16 +6559,12 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.5": - version: 0.12.5 - resolution: "util@npm:0.12.5" - dependencies: - inherits: "npm:^2.0.3" - is-arguments: "npm:^1.0.4" - is-generator-function: "npm:^1.0.7" - is-typed-array: "npm:^1.1.3" - which-typed-array: "npm:^1.1.2" - checksum: 10/61a10de7753353dd4d744c917f74cdd7d21b8b46379c1e48e1c4fd8e83f8190e6bd9978fc4e5102ab6a10ebda6019d1b36572fa4a325e175ec8b789a121f6147 +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e languageName: node linkType: hard @@ -7033,7 +6651,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies: @@ -7077,7 +6695,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -7106,28 +6724,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3": - version: 8.18.3 - resolution: "ws@npm:8.18.3" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 - languageName: node - linkType: hard - -"y18n@npm:^5.0.5": - version: 5.0.8 - resolution: "y18n@npm:5.0.8" - checksum: 10/5f1b5f95e3775de4514edbb142398a2c37849ccfaf04a015be5d75521e9629d3be29bd4432d23c57f37e5b61ade592fb0197022e9993f81a06a5afbdcda9346d - languageName: node - linkType: hard - "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -7151,28 +6747,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.1.1": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: 10/9dc2c217ea3bf8d858041252d43e074f7166b53f3d010a8c711275e09cd3d62a002969a39858b92bbda2a6a63a585c7127014534a560b9c69ed2d923d113406e - languageName: node - linkType: hard - -"yargs@npm:^17.7.2": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" - dependencies: - cliui: "npm:^8.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.3" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^21.1.1" - checksum: 10/abb3e37678d6e38ea85485ed86ebe0d1e3464c640d7d9069805ea0da12f69d5a32df8e5625e370f9c96dd1c2dc088ab2d0a4dd32af18222ef3c4224a19471576 - languageName: node - linkType: hard - "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md index a9127426f0..84c0300543 100644 --- a/docs/api/deprecations.md +++ b/docs/api/deprecations.md @@ -124,7 +124,7 @@ createApiFactory({ ``` Provider specific factory implementations, copy the code you need into the -factory method depending on which apiRef you previously used. +factory method depending on which API ref you previously used. ```ts // samlAuthApiRef diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index fbb71221fc..8d882227ec 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -10,31 +10,31 @@ Backstage plugins strive to be self-contained, with as much functionality as pos Backstage provides two primary methods for plugins to communicate across their boundaries in client-side code. The first one being the -[`createPlugin`](../reference/core-plugin-api.createplugin.md) API along with the +[`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html) API along with the extensions that it can provide, and the second one being Utility APIs. While the -[`createPlugin`](../reference/core-plugin-api.createplugin.md) API is focused on +[`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html) API is focused on the initialization plugins and the app, the Utility APIs provide ways for plugins to communicate during their entire life cycle. ## Consuming APIs -Each Utility API is tied to an [`ApiRef`](../reference/core-plugin-api.apiref.md) +Each Utility API is tied to an [`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) instance, which is a global singleton object without any additional state or functionality, its only purpose is to reference Utility APIs. -[`ApiRef`](../reference/core-plugin-api.apiref.md)s are created using -[`createApiRef`](../reference/core-plugin-api.createapiref.md), which is exported -by [`@backstage/core-plugin-api`](../reference/core-plugin-api.md). There are also +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html)s are created using +[`createApiRef`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiRef.html), which is exported +by [`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html). There are also many predefined Utility APIs in -[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), and they're all +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html), and they're all exported with a name of the pattern `*ApiRef`, for example -[`errorApiRef`](../reference/core-plugin-api.errorapiref.md). +[`errorApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.errorApiRef.html). To access one of the Utility APIs inside a React component, use the -[`useApi`](../reference/core-plugin-api.useapi.md) hook exported by -[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), or the -[`withApis`](../reference/core-plugin-api.withapis.md) HOC if you prefer class +[`useApi`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.useApi.html) hook exported by +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html), or the +[`withApis`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.withApis.html) HOC if you prefer class components. For example, the -[`ErrorApi`](../reference/core-plugin-api.errorapi.md) can be accessed like this: +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html) can be accessed like this: ```tsx import { useApi, errorApiRef } from '@backstage/core-plugin-api'; @@ -52,14 +52,14 @@ export const MyComponent = () => { ``` Note that there is no explicit type given for -[`ErrorApi`](../reference/core-plugin-api.errorapi.md). This is because the -[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) has the type -embedded, and [`useApi`](../reference/core-plugin-api.useapi.md) is able to infer +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html). This is because the +[`errorApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.errorApiRef.html) has the type +embedded, and [`useApi`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.useApi.html) is able to infer the type. Also note that consuming Utility APIs is not limited to plugins; it can be done from any component inside Backstage, including the ones in -[`@backstage/core-plugin-api`](../reference/core-plugin-api.md). The only +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html). The only requirement is that they are beneath the `AppProvider` in the react tree. ## Supplying APIs @@ -67,15 +67,15 @@ requirement is that they are beneath the `AppProvider` in the react tree. ### API Factories APIs are registered in the form of -[`ApiFactory`](../reference/core-plugin-api.apifactory.md) instances, which encapsulate +[`ApiFactory`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiFactory.html) instances, which encapsulate the process of instantiating an API. It is a collection of three things: the -[`ApiRef`](../reference/core-plugin-api.apiref.md) of the API to instantiate, a +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) of the API to instantiate, a list of all required dependencies, and a factory function that returns a new API instance. For example, this is the default -[`ApiFactory`](../reference/core-plugin-api.apifactory.md) for the -[`ErrorApi`](../reference/core-plugin-api.errorapi.md): +[`ApiFactory`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiFactory.html) for the +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html): ```ts createApiFactory({ @@ -89,25 +89,25 @@ createApiFactory({ }); ``` -In this example, the [`errorApiRef`](../reference/core-plugin-api.errorapiref.md) +In this example, the [`errorApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.errorApiRef.html) is our API, which encapsulates the -[`ErrorApi`](../reference/core-plugin-api.errorapi.md) type. The -[`alertApiRef`](../reference/core-plugin-api.alertapiref.md) is our single +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html) type. The +[`alertApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.alertApiRef.html) is our single dependency, which we give the name `alertApi`, and is then passed on to the factory function, which returns an implementation of the -[`ErrorApi`](../reference/core-plugin-api.errorapi.md). +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html). -The [`createApiFactory`](../reference/core-plugin-api.createapifactory.md) +The [`createApiFactory`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiFactory.html) function is a thin wrapper that enables TypeScript type inference. You may notice that there are no type annotations in the above example, and that is because we're able to infer all types from the -[`ApiRef`](../reference/core-plugin-api.apiref.md)s. TypeScript will make sure +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html)s. TypeScript will make sure that the return value of the `factory` function matches the type embedded in -`api`'s [`ApiRef`](../reference/core-plugin-api.apiref.md), in this case the -[`ErrorApi`](../reference/core-plugin-api.errorapi.md). It will also match the +`api`'s [`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html), in this case the +[`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html). It will also match the types between the `deps` and the parameters of the `factory` function, again using the type embedded within the -[`ApiRef`](../reference/core-plugin-api.apiref.md)s. +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html)s. ## Registering API Factories @@ -119,13 +119,13 @@ app, and the app itself. Starting with the Backstage core library, it provides implementations for all of the core APIs. The core APIs are the ones exported by -[`@backstage/core-plugin-api`](../reference/core-plugin-api.md), such as the -[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) and -[`configApiRef`](../reference/core-plugin-api.configapiref.md). +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html), such as the +[`errorApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.errorApiRef.html) and +[`configApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.configApiRef.html). The core APIs are loaded for any app created with -[`createApp`](../reference/app-defaults.createapp.md) from -[`@backstage/core-plugin-api`](../reference/app-defaults.md), which means that +[`createApp`](https://backstage.io/api/stable/functions/_backstage_app-defaults.createApp.html) from +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html), which means that there is no step that needs to be taken to include these APIs in an app. ### Plugin APIs @@ -133,13 +133,13 @@ there is no step that needs to be taken to include these APIs in an app. In addition to the core APIs, plugins can define and export their own APIs. While doing so, they should usually also provide default implementations of their own APIs; for example, the `catalog` plugin exports `catalogApiRef` and also -supplies a default [`ApiFactory`](../reference/core-plugin-api.apifactory.md) of +supplies a default [`ApiFactory`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiFactory.html) of that API using the `CatalogClient`. There is one restriction to plugin-provided API Factories: plugins may not supply factories for core APIs; trying to do so will cause the app to refuse to start. Plugins supply their APIs through the `apis` option of -[`createPlugin`](../reference/core-plugin-api.createplugin.md), for example: +[`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html), for example: ```ts export const techdocsPlugin = createPlugin({ @@ -164,7 +164,7 @@ Lastly, the app itself is the final point where APIs can be added, and what has the final say in what APIs will be loaded at runtime? The app may override the factories for any of the core or plugin APIs, with the exception of the config, app theme, and identity APIs. These are static APIs that are tied into the -[`createApp`](../reference/app-defaults.createapp.md) implementation and +[`createApp`](https://backstage.io/api/stable/functions/_backstage_app-defaults.createApp.html) implementation and therefore, not possible to override. Overriding APIs is useful for apps that want to switch out behavior to tailor it @@ -227,33 +227,33 @@ const app = createApp({ ``` Note that the above line will cause an error if `IgnoreErrorApi` does not fully -implement the [`ErrorApi`](../reference/core-plugin-api.errorapi.md), as it is +implement the [`ErrorApi`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ErrorApi.html), as it is checked by the type embedded in the -[`errorApiRef`](../reference/core-plugin-api.errorapiref.md) at compile time. +[`errorApiRef`](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.errorApiRef.html) at compile time. ## Defining custom Utility APIs Plugins are free to define their own Utility APIs. Simply define the TypeScript interface for the API and create an -[`ApiRef`](../reference/core-plugin-api.apiref.md) using -[`createApiRef`](../reference/core-plugin-api.createapiref.md) exported from -[`@backstage/core-plugin-api`](../reference/core-plugin-api.md). Also, be sure to +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) using +[`createApiRef`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiRef.html) exported from +[`@backstage/core-plugin-api`](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.html). Also, be sure to provide at least one implementation of the API and to declare a default factory -for the API in [`createPlugin`](../reference/core-plugin-api.createplugin.md). +for the API in [`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html). Custom Utility APIs can be either public or private, which is up to the plugin to choose. Private APIs do not expose an external API surface, and it's therefore possible to make breaking changes to the API without affecting other users of the plugin. If an API is made public, however, it opens up for other plugins to make use of the API, and it also makes it possible for users for your plugin to override the API in the app. It is, however, important to maintain backward compatibility of public APIs, as you may otherwise break apps that are using your plugin. To make an API public, simply export the -[`ApiRef`](../reference/core-plugin-api.apiref.md) of the API, and any associated +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) of the API, and any associated types. To make an API private, just avoid exporting the -[`ApiRef`](../reference/core-plugin-api.apiref.md), but still be sure to supply a -default factory to [`createPlugin`](../reference/core-plugin-api.createplugin.md). +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html), but still be sure to supply a +default factory to [`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html). Private APIs are useful for plugins that want to depend on other APIs outside of React components, but not have to expose an entire API surface to maintain. When using private APIs, it is fine to use the `typeof` of an implementing class as the type parameter passed to -[`createApiRef`](../reference/core-plugin-api.createapiref.md), while public APIs +[`createApiRef`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiRef.html), while public APIs should always define a separate TypeScript interface type. Plugins may depend on APIs from other plugins, both in React components and as @@ -262,13 +262,13 @@ dependencies between plugins. ## Architecture -The [`ApiRef`](../reference/core-plugin-api.apiref.md) instances mentioned above +The [`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) instances mentioned above provide a point of indirection between consumers and producers of Utility APIs. It allows for plugins and components to depend on APIs in a type-safe way, without having a direct reference to a concrete implementation of the APIs. The Apps are also given a lot of flexibility in what implementations to provide. As long as they adhere to the contract established by an -[`ApiRef`](../reference/core-plugin-api.apiref.md), they are free to choose any +[`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html), they are free to choose any implementation they want. The figure below shows the relationship between @@ -291,16 +291,16 @@ The indirection provided by Utility APIs also makes it straightforward to test components that depend on APIs, and to provide a standard common development environment for plugins. A proper test wrapper with mocked API implementations is not yet ready, but it will be provided as a part of -[`@backstage/test-utils`](../reference/test-utils.md). It will provide mocked +[`@backstage/test-utils`](https://backstage.io/api/stable/modules/_backstage_test-utils.html). It will provide mocked variants of APIs, with additional methods for asserting a component's interaction with the API. The common development environment for plugins is included in -[`@backstage/dev-utils`](../reference/dev-utils.md), where the exported -[`createDevApp`](../reference/dev-utils.createdevapp.md) function creates an +[`@backstage/dev-utils`](https://backstage.io/api/stable/modules/_backstage_dev-utils.html), where the exported +[`createDevApp`](https://backstage.io/api/stable/functions/_backstage_dev-utils.createDevApp.html) function creates an application with implementations for all core APIs already present. Contrary to the method for wiring up Utility API implementations in an app created with -[`createApp`](../reference/app-defaults.createapp.md), -[`createDevApp`](../reference/dev-utils.createdevapp.md) uses automatic dependency +[`createApp`](https://backstage.io/api/stable/functions/_backstage_app-defaults.createApp.html), +[`createDevApp`](https://backstage.io/api/stable/functions/_backstage_dev-utils.createDevApp.html) uses automatic dependency injection. This is to make it possible to replace any API implementation, and having that be reflected in dependents of that API. diff --git a/docs/assets/getting-started/create-app-output.png b/docs/assets/getting-started/create-app-output.png index 1987ea41e1..305f2e001a 100644 Binary files a/docs/assets/getting-started/create-app-output.png and b/docs/assets/getting-started/create-app-output.png differ diff --git a/docs/assets/getting-started/login-to-app-as-guest-user.png b/docs/assets/getting-started/login-to-app-as-guest-user.png new file mode 100644 index 0000000000..79c897dcb6 Binary files /dev/null and b/docs/assets/getting-started/login-to-app-as-guest-user.png differ diff --git a/docs/assets/getting-started/portal.png b/docs/assets/getting-started/portal.png index b3133f2278..fd7d52d193 100644 Binary files a/docs/assets/getting-started/portal.png and b/docs/assets/getting-started/portal.png differ diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index fa44ae9405..e20ecb2ef7 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -80,6 +80,7 @@ This provider includes several resolvers out of the box that you can use: - `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`. - `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`. - `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`. +- `userIdMatchingUserEntityAnnotation`: Matches the GitHub user ID with the User entity that has a matching `github.com/user-id`. If no match is found, it will throw a `NotFoundError`. :::note Note diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index a76c03d10f..d6751f20ff 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -72,6 +72,7 @@ This provider includes several resolvers out of the box that you can use: - `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found, it will throw a `NotFoundError`. - `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`. - `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found, it will throw a `NotFoundError`. +- `userIdMatchingUserEntityAnnotation`: Matches the GitLab user ID with the User entity that has a matching `gitlab.com/user-id` annotation (or `{integration-host}/user-id` for self-hosted GitLab instances). If no match is found, it will throw a `NotFoundError`. :::note Note diff --git a/docs/auth/index.md b/docs/auth/index.md index bff1f3df26..1d668f6961 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -340,7 +340,7 @@ The method with which frontend plugins request access to third-party services is through [Utility APIs](../api/utility-apis.md) for each service provider. These are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a full list of providers, see the -[@backstage/core-plugin-api](../reference/core-plugin-api.md#variables) reference. +[@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. ## Custom Authentication Provider @@ -459,6 +459,42 @@ providerFactories: { }, ``` +In the new backend system you can leverage the `authProvidersExtensionPoint` for this: + +```ts +// your-auth-plugin-module.ts +export const gheAuth = createBackendModule({ + // This ID must be exactly "auth" because that's the plugin it targets + pluginId: 'auth', + // This ID must be unique, but can be anything + moduleId: 'ghe-auth-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + logger: coreServices.logger, + }, + async init({ providers, logger }) { + providers.registerProvider({ + // This ID must match the actual provider config, e.g. addressing + // auth.providers.ghe means that this must be "ghe". + providerId: 'ghe', + factory: createOAuthProviderFactory({ + authenticator: githubAuthenticator, + signInResolverFactories: { + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); + +// backend index.ts +backend.add(gheAuth); +``` + ## Configuring token issuers By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 250736cec7..55827ab1dc 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -1,7 +1,7 @@ --- id: oidc title: OIDC provider from scratch -description: This section shows how to use an OIDC provider from scratch, same steps apply for custom providers. +description: This section shows how to enable and use the Backstage OIDC provider. --- :::info @@ -11,78 +11,65 @@ system, you may want to read [its own article](https://github.com/backstage/back instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! ::: -This section shows how to use an OIDC provider from scratch, same steps apply for custom -providers. Please note these steps are for using a provider, not how to implement one, -and Backstage recommends creating custom providers specific to the IDP, so we'll use a -`azureOIDC` provider throughout this example, feel free to change any of those refs -to your provider name. +This section shows how to enable and use the Backstage OIDC provider. ## Summary -To add providers not enabled by default like OIDC, we need to follow some steps, we -assume you already have a sign-in page to which we'll add the provider so users can -sign in through the provider. In simple steps here's how you enable the provider: +OIDC is a protocol which has numerous implementations. It's likely that many of your users won't know what the OIDC **protocol** is, but they will recognise your OIDC **implementation**. Backstage supplies a generic `oidc` authorization strategy. You should re-badge this with the name and branding of your OIDC implementation, so that your users will recognise it on the Backstage sign-in page. + +For example, if your organization uses [Keycloak](https://www.keycloak.org), you would re-badge the OIDC provider as `Keycloak` and tell users to `Sign In using Keycloak`. + +## Steps + +The Backstage OIDC provider is not enabled by default. You need to manually enable the provider, and tell it which OIDC server you want to use. + +To enable the Backstage OIDC provider: - Create an API reference to identify the provider. - Create the API factory that will handle the authentication. - Add or reuse an auth provider so you can authenticate. - Add or reuse a resolver to handle the result from the authentication. - Configure the provider to access your 3rd party auth solution. -- Add the provider to sign in page so users can login with it. +- Add the provider to the Backstage sign-in page. + +For simplicity, we assume that you only have a single OIDC provider in your Backstage installation. (If you need to have multiple OIDC providers in Backstage, the steps will be different.) We'll explain each step more in detail next. -### The API reference +### The API Reference -An API reference exist for the sake of **Dependency Injection**, check [Utility APIs][4] -for extended explanation. +An API reference exists to enable **Dependency Injection**. (See [Utility APIs][4] for an extended explanation.) -In this OIDC example, we'll create the API reference directly in the -`packages/app/src/apis.ts` file, it is not a requirement to put the reference in this -file. Any location will do as long as it's available to be imported to where the API -factory is, as well as easily accessible to the rest of the application so any package -and plugin can inject the API instance when necessary. - -An example of such would be when you use an auth provider from a library installed with -NPM, or any other library repository, you would import the API ref from the library. +In this example, we'll create the API ref directly in the `packages/app/src/apis.ts` file. It is not a requirement to put the ref in this file. Any location will do as long as it's available to be imported to where the API factory is, as well as easily accessible to the rest of the application so any package and plugin can inject the API instance when necessary. ```ts -export const azureOIDCAuthApiRef: ApiRef< +export const keycloakAuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi > = createApiRef({ - id: 'auth.my-custom-provider', + id: 'auth.keycloak', }); ``` -Please note a few things, the ID can be anything you want as long as it doesn't conflict -with other refs, backstage recommends to use a custom name that references your custom -provider, for example we are using OIDC protocol with Azure, so we could use something -like `auth.azure.oidc` as well. +The `id` of the API ref can be anything you want, as long as it doesn't conflict with other refs. Backstage recommends to use a custom name that references your custom provider. -Also we're exporting this reference, as well as the `typings`, we need to -be able to import this reference anywhere in the app, and the `typings` will tell typescript +:::note TypeScript Note +As we're exporting this API reference, as well as the TypeScript types, we need to +be able to import this reference anywhere in the app. The types will tell TypeScript what instance we're getting from DI when injecting the API. In this case we are defining an API for authentication, so we tell TS that this instance complies with 4 API interfaces: -- The OICD API that will handle authentication. +- The OIDC API that will handle authentication. - Profile API for requesting user profile info from the auth provider in question. - Backstage identity API to handle and associate the user profile with backstage identity. -- Session API, to handle the session the user will have while logged in. +- Session API, to handle the session the user will have while signed in. + ::: -### The API Factory +### The API Factory (and auth provider) -A factory is a function that can take some parameters or dependencies and return an -instance of something, in our case it will be a function that requests some backstage -APIs and use them to create an instance of an OIDC API provider. +The Backstage API factories are part of the Backstage Dependency Injection system. The factory function runs once, when something in your Backstage app first attempts to use an instance of the API it provides. The instance is then cached by the DI system for subsequent lookups. -Please note that this function only runs (creates the instance) when somewhere else in -the app you request the DI to give you an instance of the OIDC provider using the API ref -defined above, and the DI will only run this function the first time, from then on any -other DI injection will just receive the same instance created the first time, basically -the instance is cached by the DI library, a singleton. - -Let's add our OIDC API factory to the APIs array in the `packages/app/src/apis.ts` file: +Let's add a new API factory to the `apis` array in the `packages/app/src/apis.ts` file. We will tell it to use the OIDC auth provider internally. ```ts title="packages/app/src/apis.ts" /* highlight-add-next-line */ @@ -91,26 +78,29 @@ import { OAuth2 } from '@backstage/core-app-api'; export const apis: AnyApiFactory[] = [ /* highlight-add-start */ createApiFactory({ - api: azureOIDCAuthApiRef, + api: keycloakAuthApiRef, deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef, configApi: configApiRef, }, factory: ({ discoveryApi, oauthRequestApi, configApi }) => + // delegate auth to the OAuth2 strategy OAuth2.create({ configApi, discoveryApi, oauthRequestApi, provider: { - id: 'my-auth-provider', - title: 'My custom auth provider', + // this value MUST be 'oidc' + // it maps our Keycloak-branded sign-in provider onto Backstage's generic OIDC auth strategy + id: 'oidc', + title: 'Keycloak', icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), defaultScopes: ['openid', 'profile', 'email'], popupOptions: { - // optional, used to customize login in popup size + // optional, used to customize sign-in window size size: { fullscreen: true, }, @@ -125,27 +115,14 @@ export const apis: AnyApiFactory[] = [ }), }), /* highlight-add-end */ - // .. ]; ``` -Please note we're importing the `OAuth2` class from `@backstage/core-app-api` effectively -delegating the authentication to it. Also we're using the `my-auth-provider` ID to tell -`OAuth2` to use the auth provider we'll define in the next section, and added the default -scopes to request ID, profile, email and user read permissions. - -## The Auth Provider - -The Auth Provider is responsible for authenticating with the 3rd party service, and give -us back the credentials, here's where you pick which protocol to use, be it Auth0, OAuth2, -OIDC, SAML or any other that your 3rd party IDP provider supports. - ### The Resolver -Resolvers exist to map user identity from the 3rd party (in this case an azure IDP -provider) to the backstage user identity. +Resolvers exist to map the user identity from the 3rd party (in this case Keycloak) to the Backstage user identity. -The default OIDC provider has built-in resolvers, here is how you configure them: +The default OIDC provider has a choice of built-in resolvers, here is how you configure them: ```yaml title="app-config.yaml" auth: @@ -159,7 +136,7 @@ auth: - resolver: emailMatchingUserEntityProfileEmail ``` -But you can also write a custom resolver as well, see an example below: +If none of the built-in resolvers are suitable, you can alternatively write a custom resolver. See an example below: ```ts title="in packages/backend/src/index.ts" /* highlight-add-start */ @@ -174,15 +151,15 @@ const myAuthProviderModule = createBackendModule({ // This ID must be exactly "auth" because that's the plugin it targets pluginId: 'auth', // This ID must be unique, but can be anything - moduleId: 'my-auth-provider', + moduleId: 'keycloak-auth-provider', register(reg) { reg.registerInit({ deps: { providers: authProvidersExtensionPoint }, async init({ providers }) { providers.registerProvider({ // This ID must match the actual provider config, e.g. addressing - // auth.providers.azure means that this must be "azure". - providerId: 'my-auth-provider', + // auth.providers.keycloak means that this must be "keycloak". + providerId: 'keycloak', // Use createProxyAuthProviderFactory instead if it's one of the proxy // based providers rather than an OAuth based one factory: createOAuthProviderFactory({ @@ -215,76 +192,91 @@ backend.add(myAuthProviderModule); //... ``` -For a more a detailed explanation about resolvers check the -[Identity Resolver][1] page. +For a more detailed explanation about resolvers check the [Identity Resolver][1] page. -### The configuration +### The Configuration -Since we are using our custom OIDC Auth Provider, we need to add a configuration based -on the provider used, in this case based on OIDC protocol (remember the 3rd party has to -support the protocol). +We will now configure our Keycloak-branded OIDC Auth Provider in Backstage, so that it can talk to our Keycloak server. -In this example we'll configure OIDC with `my-auth-provider`, to do so we need to -[Create app registration][2] in the Azure console, the only difference is that the -`http://localhost:7007/api/auth/microsoft/handler/frame` URL needs to change to -`http://localhost:7007/api/auth/my-auth-provider/handler/frame`. +The first step is to register an OIDC client app for Backstage in your Keycloak server. -Then we need to configure the env variables for the provider, based on the provider's code -in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following variables -in the `app-config.yaml`: +Then we need to configure the provider. Based on the provider's code in `plugins/auth-backend/src/providers/oidc/provider.ts` we need the following parameters in the `app-config.yaml`: ```yaml title="app-config.yaml" auth: environment: development - ### Providing an auth.session.secret will enable session support in the auth-backend session: - secret: ${SESSION_SECRET} + secret: ${AUTH_SESSION_SECRET} providers: - my-auth-provider: + oidc: development: metadataUrl: https://example.com/.well-known/openid-configuration - clientId: ${AUTH_MY_CLIENT_ID} - clientSecret: ${AUTH_MY_CLIENT_SECRET} + clientId: ${AUTH_OIDC_CLIENT_ID} + clientSecret: ${AUTH_OIDC_CLIENT_SECRET} ``` -Anything enclosed in `${}` can be replaced directly in the yaml, or provided as -environment variables, the way you obtain all these except `scope` and `prompt` is to -check the App Registration you created: +Anything enclosed in `${}` can be replaced directly in the YAML, or provided as environment variables. + +#### Required Parameters + +These parameters must always be set. - `clientId`: Grab from the Overview page. - `clientSecret`: Can only be seen when creating the secret, if you lose it you'll need a new secret. - `metadataUrl`: In Overview > Endpoints tab, grab OpenID Connect metadata document URL. + +The OIDC provider **also** requires the `auth.session.secret` to be set. + +#### Optional Parameters + +These parameters have implicit default values. Don't override them unless you know what you're doing. + - `authorizationUrl` and `tokenUrl`: Open the `metadataUrl` in a browser, that json will hold these 2 urls somewhere in there. -- `tokenEndpointAuthMethod`: Don't define it, use the default unless you know what it does. -- `tokenSignedResponseAlg`: Don't define it, use the default unless you know what it does. +- `tokenEndpointAuthMethod` +- `tokenSignedResponseAlg` - `scope`: Only used if we didn't specify `defaultScopes` in the provider's factory, basically the same thing. -- `prompt`: Recommended to use `auto` so the browser will request login to the IDP if the +- `prompt`: Recommended to use `auto` so the browser will request sign-in to the IDP if the user has no active session. -- `sessionDuration` (optional): Lifespan of the user session. +- `sessionDuration`: Lifespan of the user session. +- `startUrlSearchParams`: This is a dictionary of search (query) parameters for the OIDC + authorization start URL. Don't define it unless you want to change the identity + provider's behavior. (For example, you could set the `organization` parameter to guide + users towards a particular sign-in option that your organization prefers.) **Note:** the + start URL is controlled by the browser, so this feature is only for improving the + Backstage user experience. -Note that for the time being, any change in this yaml file requires a restart of the app, -also you need to have the `session.secret` part to use OIDC (some other providers might -need this as well) to support user sessions. +:::note Config Reloading +Backstage does not yet support hot reloading of auth provider configuration. Any changes to this YAML file require a restart of Backstage. +::: -### The Sign In provider +### The Sign-In Page -The last step is to add the provider to the `SignInPage` so users can sign in with your -new provider, please follow the [Sign In Configuration][3] docs, here's where you import -and use the API reference we defined earlier. +The last step is to add the provider to the sign-in page, so users can sign in with your new provider. + +If you are using the standard Backstage [`SignInPage`][3] component, you can just add it to the `providers` array like this: + +```ts title="in packages/app/src/identityProviders.ts" +export const providers = [ + // other providers... + { + id: 'keycloak-auth-provider', + title: 'Keycloak', + message: 'Sign In using Keycloak', + apiRef: keycloakAuthApiRef, + }, +]; +``` :::note Note - -These steps apply to most if not all the providers, including custom providers, the main -difference between different providers will be the contents of the API factory, the code +These steps apply to most auth providers. The main +difference between providers will be the contents of the API factory, the code in the Auth Provider Factory, the resolver, and the different variables each provider needs in the YAML config or env variables. - ::: [1]: https://backstage.io/docs/auth/identity-resolver -[2]: https://backstage.io/docs/auth/microsoft/provider#create-an-app-registration-on-azure [3]: https://backstage.io/docs/auth/#sign-in-configuration [4]: https://backstage.io/docs/api/utility-apis diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index 8f903eb7a8..4c9857df86 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -38,3 +38,28 @@ Once you have added all desired features we call the `.start()` method. This cau Underneath the hood, `createBackend` calls `createSpecializedBackend` from `@backstage/backend-app-api` which is responsible for actually creating the backend instance, without any services or features. You can think of `createBackend` more of a 'batteries included' approach, while `createSpecializedBackend` is 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. + +### Backend Startup Result + +The `Backend.start()` method returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed. + +```ts +backend.start( + ({ result }) => { + console.log(`Backend startup result: ${JSON.stringify(result, null, 2)}`); + }, + error => { + if (error instanceof BackendStartupError) { + console.error( + `Backend startup failed: ${JSON.stringify(error.result, null, 2)}`, + ); + } else { + console.error( + `Unexpected error during backend startup: ${error.message}`, + ); + } + }, +); +``` + +This information is mostly useful if you want to add additional monitoring or debugging tools to your backend. The information is a structured representation of what is already logged during startup. diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index 0f0a8e01ea..a38c1d44e5 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -14,7 +14,7 @@ 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 +## 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. diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index ad4d04b50a..a23332409b 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -11,24 +11,84 @@ This service lets your plugin interact with a cache. It is bound to your plugin The cache service can be configured using the `backend.cache` section in your `app-config.yaml`: +### In-Memory (default) + ```yaml backend: cache: - store: redis # or 'valkey', 'memcache', 'memory' + store: memory +``` + +### Memcache + +```yaml +backend: + cache: + store: memcache + connection: user:pass@cache.example.com:11211 +``` + +### Redis + +```yaml +backend: + cache: + store: redis connection: redis://localhost:6379 - # Store-specific configuration (Redis/Valkey only) + # Store-specific configuration (optional) redis: client: - # Optional: Global namespace prefix for all cache keys + # Global namespace prefix for all cache keys namespace: 'my-app' - # Optional: Separator used between namespace and plugin ID (default: ':') + # Separator used between namespace and plugin ID (default: ':') keyPrefixSeparator: ':' # Other Redis-specific options... clearBatchSize: 1000 useUnlink: false ``` +### Valkey + +```yaml +backend: + cache: + store: valkey + connection: redis://localhost:6379 + + # Store-specific configuration (optional) + valkey: + # Global namespace prefix for all cache keys (including separator used between namespace and plugin ID) + keyPrefix: 'my-app:' +``` + +### Infinispan + +```yaml +backend: + cache: + store: infinispan + + # Store-specific configuration (optional) + infinispan: + servers: + # IP address or hostname of the server (default: '127.0.0.1') + - host: 127.0.0.1 + # Port number of the server (default: '11222') + port: 11222 + # Name of the cache (default: 'cache') + cacheName: cache + mediaType: application/json + authentication: + # Whether authentication is enabled (default: 'false') + enabled: true + userName: yourusername + password: yourpassword + saslMechanism: PLAIN +``` + +A full list of configuration items is available [here](https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/module-infinispan.html), including support for backup clusters. + ### Namespace Configuration For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys: @@ -36,9 +96,10 @@ For Redis and Valkey stores, you can configure a global namespace that will be p - **Without namespace**: Cache keys use only the plugin ID (e.g., `catalog:some-key`) - **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`) -The `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). +For **Redis**, `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). +For **Valkey**, you set the full `keyPrefix` including the separator. -**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly. +**Note**: In-memory, Memcache and Infinispan stores do not support namespace configuration and will always use the plugin ID directly. ## Using the service diff --git a/docs/backend-system/core-services/root-instance-metadata.md b/docs/backend-system/core-services/root-instance-metadata.md new file mode 100644 index 0000000000..109042f1a5 --- /dev/null +++ b/docs/backend-system/core-services/root-instance-metadata.md @@ -0,0 +1,44 @@ +--- +id: root-instance-metadata +title: Root Instance Metadata Service +sidebar_label: Root Instance Metadata +description: Documentation for the Root Instance Metadata service +--- + +The root instance metadata service provides information about the running Backstage backend instance. Currently, it provides a list of all installed backend plugins. + +:::note Note + +The root instance metadata service only provides information about the specific Backstage instance you're running on. In more complex deployments with multiple Backstage instances, this service will not provide a complete list of all plugins across all instances. + +::: + +## Using the service + +The following example shows how to use the root instance metadata service in your `example` backend plugin to access the list of installed backend plugins. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async init({ instanceMetadata }) { + const plugins = instanceMetadata.getInstalledPlugins(); + console.log('Installed plugins:', plugins); + }, + }); + }, +}); +``` + +## Dynamic plugin registration + +The root instance metadata service picks up plugins that are registered at start time through a `backend.start()` call. You need to restart the running backend instance to pick up newly installed plugins. diff --git a/docs/conf/reading.md b/docs/conf/reading.md index 61cff2502b..b6119b0da9 100644 --- a/docs/conf/reading.md +++ b/docs/conf/reading.md @@ -7,7 +7,7 @@ description: Documentation on Reading Backstage Configuration ## Config API There's a common configuration API for by both frontend and backend plugins. An -API reference can be found [here](../reference/config.config.md). +API reference can be found [here](https://backstage.io/api/stable/types/_backstage_config.Config.html). The configuration API is tailored towards failing fast in case of missing or bad config. That's because configuration errors can always be considered programming @@ -115,7 +115,7 @@ example `getString`. These will throw an error if there is no value available. ## Accessing ConfigApi in Frontend Plugins -The [ConfigApi](../reference/core-plugin-api.configapi.md) in the frontend is a +The [ConfigApi](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ConfigApi.html) in the frontend is a [UtilityApi](../api/utility-apis.md). It's accessible as usual via the `configApiRef` exported from `@backstage/core-plugin-api`: diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index 81acfdf720..e0fb645d78 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -103,13 +103,13 @@ Backstage UI is using light by default under `:root` but you can target it more [data-theme-mode='light'] { /* Light theme specific styles */ -  --bui-bg: #f8f8f8; +  --bui-bg-surface-0: #f8f8f8; --bui-fg-primary: #000; } [data-theme-mode='dark'] { /* Dark theme specific styles */ -  --bui-bg: #333333; +  --bui-bg-surface-0: #333333; --bui-fg-primary: #fff; } ``` @@ -122,18 +122,18 @@ We recommend starting with a core set of CSS variables to quickly achieve a bran And if you’d like to go even further, you can target specific component class names for advanced customization. -| Token Name | Description | -| -------------------- | ----------------------------------------------------------------------------------- | -| `--bui-bg` | This is used to define the background color of your app. It will only be used once. | -| `--bui-bg-surface-1` | We ar using this color to sit on top of `--bui-bg` mostly for `Card`, `Dialog`, ... | -| `--bui-bg-surface-2` | This is for content inside elevated components. This colour is less common. | -| `--bui-bg-solid` | This is used for main actions like primary buttons. | -| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | -| `--bui-fg-primary` | Your primary text or icon colours. | -| `--bui-fg-secondary` | Your secondary text or icon colours. | -| `--bui-fg-link` | Used for links. | -| `--bui-border` | Main borders around surfaces like `Card`, `Dialog`, ... | -| `--bui-font-regular` | The main font of your app. | +| Token Name | Description | +| -------------------- | --------------------------------------------------------------------------------------------- | +| `--bui-bg-surface-0` | This is used to define the background color of your app. It will only be used once. | +| `--bui-bg-surface-1` | We ar using this color to sit on top of `--bui-bg-surface-0` mostly for `Card`, `Dialog`, ... | +| `--bui-bg-surface-2` | This is for content inside elevated components. This colour is less common. | +| `--bui-bg-solid` | This is used for main actions like primary buttons. | +| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | +| `--bui-fg-primary` | Your primary text or icon colours. | +| `--bui-fg-secondary` | Your secondary text or icon colours. | +| `--bui-fg-link` | Used for links. | +| `--bui-border` | Main borders around surfaces like `Card`, `Dialog`, ... | +| `--bui-font-regular` | The main font of your app. |
All available CSS variables @@ -157,13 +157,14 @@ These colors are used for special purposes like ring, scrollbar, ... #### Core background colors -These colors are used for the background of your application. We are mostly using for now a single elevated background for panels. `--bui-bg` should mostly use as the main background color of your app. +These colors are used for the background of your application. We are mostly using for now a single elevated background for panels. `--bui-bg-surface-0` should mostly use as the main background color of your app. | Token Name | Description | | ------------------------- | ------------------------------------------------ | -| `--bui-bg` | The background color of your Backstage instance. | +| `--bui-bg-surface-0` | The background color of your Backstage instance. | | `--bui-bg-surface-1` | Use for any panels or elevated surfaces. | | `--bui-bg-surface-2` | Use for any panels or elevated surfaces. | +| `--bui-bg-surface-3` | Use for any panels or elevated surfaces. | | `--bui-bg-solid` | Used for solid background colors. | | `--bui-bg-solid-hover` | Used for solid background colors when hovered. | | `--bui-bg-solid-pressed` | Used for solid background colors when pressed. | diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 04b72da445..852248d543 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -53,7 +53,7 @@ Once the host build is complete, we are ready to build our image. The following `Dockerfile` is included when creating a new app with `@backstage/create-app`: ```dockerfile -FROM node:22-bookworm-slim +FROM node:24-trixie-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -178,7 +178,7 @@ the repo root: ```dockerfile # Stage 1 - Create yarn install skeleton layer -FROM node:22-bookworm-slim AS packages +FROM node:24-trixie-slim AS packages WORKDIR /app COPY backstage.json package.json yarn.lock ./ @@ -193,7 +193,7 @@ COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ # Stage 2 - Install dependencies and build packages -FROM node:22-bookworm-slim AS build +FROM node:24-trixie-slim AS build # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -231,7 +231,7 @@ RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \ && tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle # Stage 3 - Build the actual backend image and install production dependencies -FROM node:22-bookworm-slim +FROM node:24-trixie-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/docs/deployment/scaling.md b/docs/deployment/scaling.md index 2c3b687f67..dd765d3c1a 100644 --- a/docs/deployment/scaling.md +++ b/docs/deployment/scaling.md @@ -17,7 +17,7 @@ into multiple different services, each running a different set of plugins. This is a more advanced approach and requires you to be able to route requests to the appropriate backends based on the plugin ID. Both for ingress, but also internal traffic between Backstage backends, which is done by creating a custom -implementation of the [DiscoveryService](../reference/backend-plugin-api.discoveryservice.md) interface. See the [backend system docs](../backend-system/building-backends/01-index.md#split-into-multiple-backends) for more details on how to separate your deployment into multiple backend instances. +implementation of the [DiscoveryService](https://backstage.io/api/stable/interfaces/_backstage_backend-plugin-api.index.DiscoveryService.html) interface. See the [backend system docs](../backend-system/building-backends/01-index.md#split-into-multiple-backends) for more details on how to separate your deployment into multiple backend instances. Lastly, you can also replicate the Backstage deployments across multiple regions. This is not a pattern that there is built-in support for and typically only makes diff --git a/docs/faq/technical.md b/docs/faq/technical.md index 32511351f0..5e01da00d2 100644 --- a/docs/faq/technical.md +++ b/docs/faq/technical.md @@ -7,7 +7,7 @@ description: General technical questions about Backstage. ### What technology does Backstage use? Backstage is a large scale [TypeScript](https://www.typescriptlang.org/) -application whose frontend parts use [React](https://react.dev/) and +framework whose frontend parts use [React](https://react.dev/) and [Material UI](https://material-ui.com/), while the backend parts use [Node.js](https://nodejs.org/) and the [Express](https://expressjs.com/) framework. diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 5b293cce1c..3ed027a0a4 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -181,28 +181,30 @@ this remote source, users cannot also register new entities with e.g. the [catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import) plugin. -## Clean up orphaned entities +## Automatic removal of orphaned entities -In short, entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons, the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning). +Entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. The default behavior is to automatically remove orphaned entities. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning). -However, if you do wish to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted. +However, if you wish to keep orphaned entities, you can use the following configuration, and automatic cleanup will be disabled. ```yaml catalog: - orphanStrategy: delete + orphanStrategy: keep ``` -## Clean up entities from orphaned entity providers +## Automatic removal of entities from orphaned entity providers -By default, if an entity provider which has provided entities to the catalog, is no longer configured, then the entities remain in the catalog until they are manually unregistered. +By default, if an entity provider which has provided entities to the catalog is no longer configured, then the entities it provided will be automatically removed. -To remove these entities automatically, you can use the following configuration. +To keep these entities instead, you can use the following configuration. ```yaml catalog: - orphanProviderStrategy: delete + orphanProviderStrategy: keep ``` +If you have had providers installed in the past that ingested entities into the catalog that you want to keep, the recommendation is to add the provider back to the catalog. If you don’t want the provider to run, you can schedule it with a very large interval. + ## Processing Interval The [processing loop](./life-of-an-entity.md#processing) is diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index c16fb8d523..ca4de99734 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -254,8 +254,8 @@ either, it becomes _orphaned_. The end result is as follows: the child entity. - The child entity is _not_ removed from the catalog, but stays around until explicitly deleted via the catalog API, implicitly if `orphanStrategy: delete` - configuration is set, or until it is "reclaimed" by the original parent - or another parent starting to reference it. + configuration is set (the default), or until it is "reclaimed" by the original + parent or another parent starting to reference it. - The catalog page in Backstage for the child entity detects the new annotation and informs users about the orphan status. @@ -282,21 +282,13 @@ Orphaning can occur in several different scenarios. > to inform the owner that something is wrong. But processing and other > behaviors continue as usual. -The reason that the orphaning mechanism exists instead of having an eager -deletion triggered, is safety. Scenarios like these can happen purely by -accident, due to the asynchronous nature of the system and the fallible nature -of humans. In particular when external systems start consuming and relying on -the catalog, there could be substantial consequences to suddenly dropping -entities without explicit owner consent. The catalog therefore takes the stance -that entities that often were added by direct user action should also be deleted -only by direct user action. - -However, if you want to delete orphaned entities automatically anyway, you can -enable the automated clean up with the following app-config option. +The default behavior of the catalog is to automatically remove orphaned +entities. However, if you want to keep them instead, you can disable the +automated cleanup with the following app-config option. ``` catalog: - orphanStrategy: delete + orphanStrategy: keep ``` ## Implicit Deletion diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index d8a2e1d232..0143d3b4d5 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -251,6 +251,46 @@ browser when viewing that user. This annotation can be used on a [User entity](descriptor-format.md#kind-user) to note that it originated from that user on GitHub. +### github.com/user-id + +```yaml +# Example: +metadata: + annotations: + github.com/user-id: '123456' +``` + +The value of this annotation is the numeric user ID 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. Unlike the username, +which can be changed by the user, the user ID is immutable. + +This annotation can be used on a [User entity](descriptor-format.md#kind-user) +to note that it originated from that user on GitHub. It enables the +`userIdMatchingUserEntityAnnotation` sign-in resolver to match users by their +GitHub user ID during authentication. + +### gitlab.com/user-id + +```yaml +# Example: +metadata: + annotations: + gitlab.com/user-id: '123456' +``` + +The value of this annotation is the numeric user ID that identifies a user on +[GitLab](https://gitlab.com) (either the public one, or a private GitLab +installation) that is related to this entity. For self-hosted GitLab instances, +the annotation key will be `{integration-host}/user-id` where +`{integration-host}` is the hostname of your GitLab instance. Unlike the +username, which can be changed, the user ID is immutable. + +This annotation can be used on a [User entity](descriptor-format.md#kind-user) +to note that it originated from that user on GitLab. It enables the +`userIdMatchingUserEntityAnnotation` sign-in resolver to match users by their +GitLab user ID during authentication. + ### gocd.org/pipelines ```yaml diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index a692335ffe..bbf35557ce 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -48,6 +48,65 @@ add the `repoVisibility` key within a software template: repoVisibility: public # or 'internal' or 'private' ``` +### Default Environment + +The scaffolder supports a `defaultEnvironment` configuration that provides default parameters and secrets to all templates. This reduces template complexity and improves security by centralizing common values. + +```yaml +scaffolder: + defaultEnvironment: + parameters: + region: eu-west-1 + organizationName: acme-corp + defaultRegistry: registry.acme-corp.com + secrets: + AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} + GITHUB_TOKEN: ${GITHUB_TOKEN} + DOCKER_REGISTRY_TOKEN: ${DOCKER_REGISTRY_TOKEN} +``` + +#### Default parameters + +Default parameters are accessible via `${{ environment.parameters.* }}` in templates. Default parameters are isolated in their own context to avoid naming conflicts. + +```yaml + parameters: + - title: Fill in some steps + required: + - organizationName + properties: + organizationName: + title: organizationName + type: string + description: Unique name of the organization + ui:autofocus: true + ui:options: + rows: 5 + + steps: + - id: deploy + name: Deploy Application + action: aws:deploy + input: + region: ${{ environment.parameters.region }} # Resolves to defaultEnvironment.parameters.region + organization: ${{ parameters.organizationName }} # Resolves to frontend input value + otherOrganization: ${{ environment.parameters.organizationName }} # Resolves to defaultEnvironment.parameters.organizationName +``` + +#### Secrets + +Default secrets are resolved from environment variables and accessible via `${{ environment.secrets.* }}` in template actions. Secrets are only available during action execution, not in frontend forms. + +```yaml +- id: deploy + name: Deploy with credentials + action: aws:deploy + input: + accessKey: ${{ environment.secrets.AWS_ACCESS_KEY }} # Resolves to defaultEnvironment.secrets.AWS_ACCESS_KEY +``` + +**Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend. + ## Disabling Docker in Docker situation (Optional) Software templates use the `fetch:template` action by default, which requires no diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 1a024569a6..845da8e60b 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -468,3 +468,59 @@ repoUrl: The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. If you're also using the `RepoUrlPicker` field extension, you should simply duplicate this part from there. + +## RepoOwnerPicker + +The input props that can be specified under `ui:options` for the `RepoOwnerPicker` field extension. + +### `host` + +The SCM integration host that owners should be fetched from for autocompletion. + +- Fetch owners from `github.com` + +```yaml +repoUrl: + title: Repository Owner + type: string + ui:field: RepoOwnerPicker + ui:options: + host: github.com +``` + +### `excludedOwners` + +List of owners that should be excluded from autocompletion. + +- Exclude owner `owner_1` from autocompletion + +```yaml +repoUrl: + title: Repository Owner + type: string + ui:field: RepoOwnerPicker + ui:options: + excludedOwners: + - owner_1 +``` + +### `requestUserCredentials` + +If defined will request user credentials to auth against the given SCM platform. + +```yaml +repoUrl: + title: Repository Owner + type: string + ui:field: RepoOwnerPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write +``` + +`secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. + +The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 32f17f945f..5d9fcba603 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -555,6 +555,35 @@ The `RepoBranchPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) +### The Repository Owner Picker + +Similar to the repository picker, there is a picker for owners to support autocompletion. A full example could look like this: + +```yaml +- title: Choose an owner + required: + - repoOwner + properties: + repoOwner: + title: Repository Owner + type: string + ui:field: RepoOwnerPicker + ui:options: + host: github.com + excludedOwners: + - backstage + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN +``` + +Passing the `requestUserCredentials` and `host` properties is required for autocompletion to work. For more information regarding the `requestUserCredentials` object, please refer to the [Using the Users `oauth` token](#using-the-users-oauth-token) section under [The Repository Picker](#the-repository-picker). + +For a list of all possible `ui:options` input props for `RepoOwnerPicker`, please visit [here](./ui-options-examples.md#repoownerpicker). + +The `RepoOwnerPicker` is a custom field that we provide part of the +`plugin-scaffolder`. You can provide your own custom fields by +[writing your own Custom Field Extensions](./writing-custom-field-extensions.md) + ### Accessing the signed-in users details Sometimes when authoring templates, you'll want to access the user that is running the template, and get details from the profile or the users `Entity` in the Catalog. diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 42024ffbb2..2d372dc48f 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -172,15 +172,17 @@ TechDocs will publish documentation to this bucket and will fetch files from here to serve documentation in Backstage. Note that the bucket names are globally unique. -Set the config `techdocs.publisher.awsS3.bucketName` in your `app-config.yaml` -to the name of the bucket you just created. +Set the bucket name and region in your `app-config.yaml` to the name of the bucket you just created: ```yaml techdocs: publisher: type: 'awsS3' +/* highlight-add-start */ awsS3: bucketName: 'name-of-techdocs-storage-bucket' + region: 'us-east-1' +/* highlight-add-end */ ``` **3. Create minimal AWS IAM policies to manage TechDocs** @@ -266,7 +268,7 @@ 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). -**4b. Authentication using app-config.yaml** +**4b. Authentication using app-config.yaml via aws.accounts** AWS credentials and region can be provided to the AWS SDK via `app-config.yaml`. If the configs below are present, they will be used over existing `AWS_*` @@ -290,7 +292,45 @@ aws: Refer to the [official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html). -**4c. Authentication using an assumed role** Users with multiple AWS accounts +**4c. Authentication using app-config.yaml via integrations.awsS3** + +If you already have an [AWS S3 integration](../../integrations/aws-s3/locations.md), you can use it to authenticate with AWS S3: + +```yaml +techdocs: + publisher: + type: 'awsS3' + awsS3: + bucketName: 'name-of-techdocs-storage-bucket' + region: 'eu-west-1' +integrations: + awsS3: + - accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} +``` + +This will use the credentials from the integration to authenticate with AWS S3 and it does not require any additional configuration in the `app-config.yaml`. However, **if you have multiple S3 integrations**, you **must** specify the target integration by setting the `accessKeyId` in the `techdocs.publisher.awsS3.credentials` config: + +```yaml +techdocs: + publisher: + type: 'awsS3' + awsS3: + bucketName: 'name-of-techdocs-storage-bucket' + region: 'eu-west-1' +/* highlight-add-start */ + credentials: + accessKeyId: ${AWS_ACCESS_KEY_ID_1} +/* highlight-add-end */ +integrations: + awsS3: + - accessKeyId: ${AWS_ACCESS_KEY_ID_1} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY_1} + - accessKeyId: ${AWS_ACCESS_KEY_ID_2} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY_2} +``` + +**4d. Authentication using an assumed role** Users with multiple AWS accounts may want to use a role for S3 storage that is in a different AWS account. Using the `roleArn` parameter as seen below, you can instruct the TechDocs publisher to assume a role before accessing S3. diff --git a/docs/frontend-system/architecture/20-extensions.md b/docs/frontend-system/architecture/20-extensions.md index 24a5f24e12..a6d00a3131 100644 --- a/docs/frontend-system/architecture/20-extensions.md +++ b/docs/frontend-system/architecture/20-extensions.md @@ -352,3 +352,69 @@ const extension = createExtension({ }, }); ``` + +## Relative attachment points + +When creating an extension or an [extension blueprint](./23-extension-blueprints.md) you can specify an attachment point that is relative to the current plugin. This is particularly useful for groups of blueprints that are part of a common hierarchy, with extensions from one blueprint attaching to extensions from the other blueprint. For example, the following pair of extension definitions could be installed multiple times in different plugins, each creating their own hierarchy: + +```tsx +// Parent extension with a fixed attachment point +const parentExtension = createExtension({ + kind: 'section', + attachTo: [{ id: 'app/some-fixed-extension', input: 'children' }], + inputs: { + content: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( +
+

Section Title

+ {inputs.content.get(coreExtensionData.reactElement)} +
, + ), + ]; + }, +}); + +// Child extension with a relative attachment point +const childExtension = createExtension({ + kind: 'section-content', + attachTo: [{ relative: { kind: 'section' }, input: 'content' }], + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(

Section Content

)]; + }, +}); +``` + +## Extension input references + +Building on the relative attachment point concept, you can also reference extension inputs directly via the `inputs` property of an extension definition. This provides a more convenient and type-safe way to attach child extensions, especially when using blueprints that provide a nested hierarchy of extensions. + +Extension inputs references are always relative, this means that they can only be used for referencing extensions within the same plugin. + +Each extension definition exposes an `inputs` property that contains references to all of its defined inputs. These references can be passed directly to the `attachTo` option when creating child extensions: + +```tsx +const parent = createExtension({ + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + // other options... +}); + +// Create a child extension that attaches to the parent's input +const child = createExtension({ + attachTo: page.inputs.children, // Direct reference to the input + output: [coreExtensionData.reactElement], // Outputs are verified against the parent input + // other options... +}); +``` + +These references are a type-safe way to attach child extensions, it both ensures that the parent input is present, as well as the child providing the required data for the parent. + +Under the hood, input references are resolved in the same way as relative attachment points, using the extension's kind, namespace, and name to construct the final attachment target. diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index 0c96bdf15d..ed84aeafb2 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -14,16 +14,16 @@ A Backstage App is a monorepo setup that includes everything you need to run Bac To create a new Backstage app we recommend using the `@backstage/create-app` command line, and the easiest way to run this package is with `npx`: :::note -The create-app CLI requires Node.js Active LTS Release. +The create-app CLI requires Node.js Active LTS Release, see the [prerequisites documentation](../../getting-started/index.md) for all the details. ::: ```sh # The command bellow creates a Backstage App inside the current folder. # The name of the app-folder is the name that was provided when prompted. -npx @backstage/create-app@latest +npx @backstage/create-app@latest --next ``` -The created-app is currently templated for legacy frontend system applications, so the app wiring code it creates needs to be migrated, see [the app instance](#the-app-instance) section for an example. +Using the `--next` flag will result in a Backstage app using the New Frontend System which will be further explained in the sections below. ## The app instance diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index f57c22db96..3ac5109fc7 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -9,7 +9,7 @@ description: How to migrate existing apps to the new frontend system This section describes how to migrate an existing Backstage app package to use the new frontend system. The app package is typically found at `packages/app` in your project and is responsible for wiring together the Backstage frontend application. -> **Who is this for?** +> **Who is this for?** > This guide is intended for maintainers of Backstage app packages (`packages/app`) who want to upgrade from the legacy frontend system to the new extension-based architecture. > **Prerequisites:** @@ -22,10 +22,10 @@ This section describes how to migrate an existing Backstage app package to use t We recommend a **two-phase migration process** to ensure a smooth and manageable transition: -- **Phase 1: Minimal Changes for Hybrid Configuration** +- **Phase 1: Minimal Changes for Hybrid Configuration** In this phase, you make the smallest set of changes necessary to enable your app to run in a hybrid mode. This allows you to start using the new frontend system while still relying on compatibility helpers and legacy code. The goal is to unblock your migration quickly, so you can benefit from the new system without a full rewrite. -- **Phase 2: Complete Transition to the New Frontend System** +- **Phase 2: Complete Transition to the New Frontend System** After your app is running in hybrid mode, you can gradually refactor your codebase to remove legacy code and compatibility helpers. This phase focuses on fully adopting the new frontend architecture, ensuring your codebase is clean, maintainable, and takes full advantage of the new features. :::warning @@ -157,36 +157,6 @@ const app = createApp({ }); ``` -If you were binding routes from a legacy `createApp`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system. - -For example, if both the `catalogPlugin` and `scaffolderPlugin` are legacy plugins, you can bind their routes like this: - -```ts -import { createApp } from '@backstage/frontend-defaults'; -import { - // ... - convertLegacyRouteRefs, - convertLegacyRouteRef, -} from '@backstage/core-compat-api'; - -// Ommitting converted options changes -//... - -const app = createApp({ - features: [ - // ... - convertedOptionsModule, - ], - // highlight-add-start - bindRoutes({ bind }) { - bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), { - createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root), - }); - }, - // highlight-add-end -}); -``` - ### 3) Fixing the `app.createRoot` call The `app.createRoot(...)` no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/00-index.md) section. @@ -474,7 +444,7 @@ const app = createApp({ Can be converted to the following extension: ```tsx -import { SignInPageBlueprint } from '@backstage/frontend-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; const signInPage = SignInPageBlueprint.make({ params: { @@ -522,7 +492,7 @@ const app = createApp({ Can be converted to the following extension: ```tsx -import { ThemeBlueprint } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/plugin-app-react'; const customLightThemeExtension = ThemeBlueprint.make({ name: 'custom-light', @@ -565,7 +535,7 @@ const app = createApp({ Icons are now installed as extensions, using the `IconBundleBlueprint` to make new instances which can be added to the app. ```ts -import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; +import { IconBundleBlueprint } from '@backstage/plugin-app-react'; const exampleIconBundle = IconBundleBlueprint.make({ name: 'example-bundle', @@ -590,21 +560,6 @@ const app = createApp({ Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/36-routes.md#binding-external-route-references) for more information. -Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyAppRoot`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system. - -For example, if both the `catalogPlugin` and `scaffolderPlugin` are legacy plugins, you can bind their routes like this: - -```ts -const app = createApp({ - features: convertLegacyAppRoot(...), - bindRoutes({ bind }) { - bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), { - createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root), - }); - }, -}); -``` - #### `__experimentalTranslations` Translations are now installed as extensions, created using `TranslationBlueprint`. @@ -631,10 +586,8 @@ Can be converted to the following extension: ```tsx import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; -import { - createTranslationMessages, - TranslationBlueprint, -} from '@backstage/frontend-plugin-api'; +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; const catalogTranslations = TranslationBlueprint.make({ name: 'catalog-overrides', @@ -707,7 +660,7 @@ const convertedRootFeatures = convertLegacyAppRoot( ); ``` -Any app root wrapper needs to be migrated to be an extension, created using `AppRootWrapperBlueprint`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. +Any app root wrapper needs to be migrated to be an extension, created using `AppRootWrapperBlueprint` from `@backstage/plugin-app-react`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. Here is an example converting the `CustomAppBarrier` into extension: @@ -721,8 +674,6 @@ createApp({ AppRootWrapperBlueprint.make({ name: 'custom-app-barrier', params: { - // Whenever your component uses legacy core packages, wrap it with "compatWrapper" - // e.g. props => compatWrapper() Component: CustomAppBarrier, }, }), @@ -749,37 +700,33 @@ export const navModule = createFrontendModule({ }); ``` -Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where the component that is passed to the `compatWrapper` is the entire `Sidebar` component from your `Root` component. - -The `compatWrapper` is there to ensure that any legacy plugins using things like `useRouteRef` work well in the new system, so if you run into some errors which look like compatibility issues, make sure that this wrapper is used in the relevant places. +Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where you implement the entire `Sidebar` component. ```tsx title="in packages/app/src/modules/nav/Sidebar.tsx" -import { compatWrapper } from '@backstage/core-compat-api'; -import { NavContentBlueprint } from '@backstage/frontend-plugin-api'; +import { NavContentBlueprint } from '@backstage/plugin-app-react'; export const SidebarContent = NavContentBlueprint.make({ params: { - component: ({ items }) => - compatWrapper( - - - } to="/search"> - - - - }> - ... - - - - {/* Items in this group will be scrollable if they run out of space */} - {items.map((item, index) => ( - - ))} - - - , - ), + component: ({ items }) => ( + + + } to="/search"> + + + + }> + ... + + + + {/* Items in this group will be scrollable if they run out of space */} + {items.map((item, index) => ( + + ))} + + + + ), }, }); ``` diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 283c162fa7..d9acf76e15 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -124,7 +124,7 @@ export interface ExampleApi { } export const exampleApiRef = createApiRef({ - id: 'plugin.example', + id: 'plugin.example.api', }); export class DefaultExampleApi implements ExampleApi { @@ -220,6 +220,6 @@ export const examplePlugin = createFrontendPlugin({ }); ``` -The `ExampleEntityContent` itself is again a regular React component where you can implement any functionality you want. To access the entity that the content is being rendered for, you can use the `useEntity` hook from `@backstage/plugin-catalog-react`. You can see a full list of APIs provided by the catalog React library in [the API reference](../../reference/plugin-catalog-react.md). +The `ExampleEntityContent` itself is again a regular React component where you can implement any functionality you want. To access the entity that the content is being rendered for, you can use the `useEntity` hook from `@backstage/plugin-catalog-react`. You can see a full list of APIs provided by the catalog React library in [the API reference](https://backstage.io/api/stable/modules/_backstage_plugin-catalog-react.index.html). For a more complete list of the different kinds of extensions that you can create for your plugin, see the [extension blueprints](./03-common-extension-blueprints.md) section. diff --git a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md index 71989e898f..aff66f3cc6 100644 --- a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md +++ b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md @@ -11,38 +11,54 @@ This section covers many of the [extension blueprints](../architecture/23-extens These are the [extension blueprints](../architecture/23-extension-blueprints.md) provided by the Backstage frontend framework itself. -### Api - [Reference](../../reference/frontend-plugin-api.apiblueprint.md) +### Api - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.ApiBlueprint.html) An API extension is used to add or override [Utility API factories](../utility-apis/01-index.md) in the app. They are commonly used by plugins for both internal and shared APIs. There are also many built-in Api extensions provided by the framework that you are able to override. -### NavItem - [Reference](../../reference/frontend-plugin-api.navitemblueprint.md) +### NavItem - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavItemBlueprint.html) Navigation item extensions are used to provide menu items that link to different parts of the app. By default nav items are attached to the app nav extension, which by default is rendered as the left sidebar in the app. -### Page - [Reference](../../reference/frontend-plugin-api.pageblueprint.md) +### Page - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.PageBlueprint.html) Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes. -### SignInPage - [Reference](../../reference/frontend-plugin-api.signinpageblueprint.md) +## Extension blueprints in `@backstage/frontend-plugin-api/alpha` + +### Plugin Wrapper - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.packages-frontend-plugin-api_src_alpha.PluginWrapperBlueprint.html) + +Plugin wrappers allow you to install components that will wrap all elements rendered as part of a plugin. This can be useful if you need to add a global provider, for example for a query client. The provided wrapper will be rendered as separate elements for each wrapped plugin element, so be sure to use a central store like a [Utility API](../utility-apis/01-index.md) if you want to share state between wrapper instances. + +## Extension blueprints in `@backstage/plugin-app-react` + +### SignInPage - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.SignInPageBlueprint.html) Sign-in page extension have a single purpose - to implement a custom sign-in page. They are always attached to the app root extension and are rendered before the rest of the app until the user is signed in. -### SwappableComponent - [Reference](../../reference/frontend-plugin-api.swappablecomponentblueprint.md) +### SwappableComponent - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.SwappableComponentBlueprint.html) Swappable Components are extensions that are used to replace the implementations of components in the app and plugins. -### Theme - [Reference](../../reference/frontend-plugin-api.themeblueprint.md) +### Theme - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.ThemeBlueprint.html) Theme extensions provide custom themes for the app. They are always attached to the app extension and you can have any number of themes extensions installed in an app at once, letting the user choose which theme to use. -### Icons - [Reference](../../reference/frontend-plugin-api.iconbundleblueprint.md) +### Icons - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.IconBundleBlueprint.html) Icon bundle extensions provide the ability to replace or provide new icons to the app. You can use the above blueprint to make new extension instances which can be installed into the app. -### Translation - [Reference](../../reference/frontend-plugin-api.translationblueprint.md) +### Translation - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.TranslationBlueprint.html) Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages. +### NavContent - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavContentBlueprint.html) + +Nav content extensions allow you to replace the entire navbar with your own component. They are always attached to the app nav extension. + +### Router - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.RouterBlueprint.html) + +Router extensions allow you to replace the router component used by the app. They are always attached to the app root extension. + ## Extension blueprints in `@backstage/plugin-catalog-react/alpha` These are the [extension blueprints](../architecture/23-extension-blueprints.md) provided by the Catalog plugin. diff --git a/docs/frontend-system/building-plugins/05-migrating.md b/docs/frontend-system/building-plugins/05-migrating.md index 2d954cb526..b0e9816a17 100644 --- a/docs/frontend-system/building-plugins/05-migrating.md +++ b/docs/frontend-system/building-plugins/05-migrating.md @@ -38,7 +38,6 @@ In order to migrate the actual definition of the plugin you need to recreate the ```ts title="my-plugin/src/alpha.tsx" import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - import { convertLegacyRouteRefs } from '@backstage/core-compat-api'; export default createFrontendPlugin({ // The plugin ID is now provided as `pluginId` instead of `id` @@ -47,15 +46,12 @@ In order to migrate the actual definition of the plugin you need to recreate the // bind all the extensions to the plugin /* highlight-next-line */ extensions: [/* APIs will go here, but don't worry about those yet */], - // convert old route refs to the new system - /* highlight-next-line */ - routes: convertLegacyRouteRefs({ + routes: { ... - }), - /* highlight-next-line */ - externalRoutes: convertLegacyRouteRefs({ + }, + externalRoutes: { ... - }), + }, }); ``` @@ -110,25 +106,16 @@ it can be migrated as the following, keeping in mind that you may need to switch ```tsx import { PageBlueprint } from '@backstage/frontend-plugin-api'; -import { - compatWrapper, - convertLegacyRouteRef, -} from '@backstage/core-compat-api'; const fooPage = PageBlueprint.make({ params: { // This is the path that was previously defined in the app code. // It's labelled as the default one because it can be changed via configuration. path: '/foo', - // You can reuse the existing routeRef by wrapping it with convertLegacyRouteRef. - routeRef: convertLegacyRouteRef(rootRouteRef), + // You can reuse the existing routeRef. + routeRef: rootRouteRef, // these inputs usually match the props required by the component. - loader: () => - import('./components/').then(m => - // The compatWrapper utility allows you to keep using @backstage/core-plugin-api in the - // implementation of the component and switch to @backstage/frontend-plugin-api later. - compatWrapper(), - ), + loader: () => import('./components/').then(m => ), }, }); ``` diff --git a/docs/frontend-system/building-plugins/06-swappable-components.md b/docs/frontend-system/building-plugins/06-swappable-components.md index a26396bbff..786388b3c8 100644 --- a/docs/frontend-system/building-plugins/06-swappable-components.md +++ b/docs/frontend-system/building-plugins/06-swappable-components.md @@ -52,11 +52,8 @@ In order to override a Swappable Component, you need to create a `SwappableCompo There are two different ways to add extensions to the `app` plugin, both are documented below in an example of overriding the `Progress` Swappable Component. ```tsx title="in packages/app/src/App.tsx" -import { - Progress, - SwappableComponentBlueprint, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; +import { Progress, createFrontendModule } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { MyCustomProgress } from './CustomProgress'; import { createApp } from '@backstage/frontend-defaults'; import appPlugin from '@backstage/plugin-app'; diff --git a/docs/frontend-system/utility-apis/02-creating.md b/docs/frontend-system/utility-apis/02-creating.md index c632d9a909..2f6959e0ce 100644 --- a/docs/frontend-system/utility-apis/02-creating.md +++ b/docs/frontend-system/utility-apis/02-creating.md @@ -36,6 +36,9 @@ export const workApiRef = createApiRef({ Both of these are properly exported publicly from the package, so that consumers can reach them. +The frontend system infers the owning plugin for an API from the `ApiRef` id, so +use the pattern `plugin..*` to make ownership explicit. This ensures that other plugins can't mistakenly override your API. + ## Providing an extension through your plugin The plugin itself now wants to provide this API and its default implementation, in the form of an API extension. Doing so means that when users install the Example plugin, an instance of the Work utility API will also be automatically available in their apps - both to the Example plugin itself, and to others. We do this in the main plugin package, not the `-react` package. diff --git a/docs/frontend-system/utility-apis/04-configuring.md b/docs/frontend-system/utility-apis/04-configuring.md index 1f21d9b1ba..53527301ef 100644 --- a/docs/frontend-system/utility-apis/04-configuring.md +++ b/docs/frontend-system/utility-apis/04-configuring.md @@ -36,6 +36,8 @@ Well written input-enabled extension often have extension creator functions that Like with other extension types, you replace Utility APIs with your own custom implementation using [extension overrides](../architecture/25-extension-overrides.md). +Note that it is only possible to override a Utility API using a module for the plugin that originally provided the API. Attempting to override an API using a different plugin or module for a different plugin will result in a conflict error. + ```tsx title="in your app" /* highlight-add-start */ import { createFrontendModule } from '@backstage/frontend-plugin-api'; diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index eed1586dc8..d325c73d5f 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -129,13 +129,13 @@ You can run Postgres in a Docker container, this is great for local development First we need to pull down the container image, we'll use Postgres 17, check out the [Postgres Version Policy](../../overview/versioning-policy.md#postgresql-releases) to learn which versions are supported. ```shell -docker pull postgres:17.0-bookworm +docker pull postgres:17.0-trixie ``` Then we just need to start up the container. ```shell -docker run -d --name postgres --restart=always -p 5432:5432 -e POSTGRES_PASSWORD= postgres:17.0-bookworm +docker run -d --name postgres --restart=always -p 5432:5432 -e POSTGRES_PASSWORD= postgres:17.0-trixie ``` This will run Postgres in the background for you, but remember to start it up again when you reboot your system. @@ -149,7 +149,7 @@ version: '4' services: postgres: - image: postgres:17.0-bookworm + image: postgres:17.0-trixie environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 876af4f0a5..c7aabb7bd5 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,8 +1,8 @@ --- id: index -title: Creating your Backstage App -sidebar_label: Introduction -description: How to install Backstage for your own use. +title: Standalone Installation +sidebar_label: Standalone Installation +description: How to create and run a Standalone Backstage. --- Audience: Developers and Admins @@ -11,11 +11,11 @@ Audience: Developers and Admins It is not required, although recommended to have a basic understanding of [Yarn](https://www.pluralsight.com/guides/yarn-a-package-manager-for-node-js) and [npm](https://docs.npmjs.com/about-npm) before starting this guide. ::: -## Summary +## Overview -This guide walks through how to get started creating your very own Backstage customizable app. This is the first step in evaluating, developing on, or demoing Backstage. +This guide walks through how to create your own Backstage customizable app. This is the first step in evaluating, developing on, or demoing Backstage. -By the end of this guide, you will have a standalone Backstage installation running locally with a `SQLite` database and demo content. To be clear, this is not a production-ready installation, and it does not contain information specific to your organization. +By the end of this guide, you will have a standalone Backstage installation running locally with an in-memory `SQLite` database and demo content. To be clear, this is not a production-ready installation, and it does not contain information specific to your organization until you set up integrations with your specific data sources! :::note Contributors @@ -23,46 +23,9 @@ If you are planning to contribute a new feature or bug fix to the Backstage proj ::: -## Prerequisites +The instructions make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. -This guide also assumes a basic understanding of working on a Linux based operating system and have some experience with the terminal, specifically, these commands: `npm`, `yarn`. - -- Access to a Unix-based operating system, such as Linux, macOS or - [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) -- A GNU-like build environment available at the command line. - For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. - On macOS, you will want to run `xcode-select --install` to get the XCode command line build tooling in place. -- An account with elevated rights to install the dependencies -- `curl` or `wget` installed -- Node.js [Active LTS Release](../overview/versioning-policy.md#nodejs-releases) installed using one of these - methods: - - Using `nvm` (recommended) - - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) - - [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm) - - Node 20 is a good starting point, this can be installed using `nvm install lts/iron` - - [Binary Download](https://nodejs.org/en/download/) - - [Package manager](https://nodejs.org/en/download/package-manager/) - - [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md) -- `yarn` [Installation](https://yarnpkg.com/getting-started/install) - - Backstage currently uses Yarn 4.4.1, once you've ran `corepack enable` you'll want to then run `yarn set version 4.4.1` -- `docker` [installation](https://docs.docker.com/engine/install/) -- `git` [installation](https://github.com/git-guides/install-git) -- If the system is not directly accessible over your network the following ports - need to be opened: 3000, 7007. This is quite uncommon, unless you're installing in a container, VM or remote system. - -## 1. Create your Backstage App - -:::caution - -The Backstage app we'll be creating will only have demo data until we set up integrations with your specific data sources! - -::: - -To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we run the command, let's discuss what it does. - -This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as subdirectory in your current working directory. - -![create app](../assets/getting-started/create-app-output.png) +This command creates a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as a subdirectory in your current working directory. Inside that directory, it will generate all the files and folder structure needed for you to run your app. @@ -100,65 +63,122 @@ app and [TechDocs](https://backstage.io/docs/features/techdocs/) amongst other things. -Now, that we know what it does, let's run it! +## Prerequisites -```bash -npx @backstage/create-app@latest -``` +This guide also assumes a basic understanding of working on a Linux based operating system and have some experience with the terminal, specifically, these commands: `npm`, `yarn`. + +- A minimum of 20 GB disk space to run the standalone Backstage application with demo data. NOTE: As you add more modules and plugins to an installation, the disk space requirements will increase, accordingly. +- A minimum of 6 GB memory. +- Access to a Unix-based operating system, such as Linux, macOS or + [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/). The Linux version must support the required Node.js version. +- A GNU-like build environment available at the command line. + For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. + On macOS, you will want to run `xcode-select --install` to get the XCode command line build tooling in place. +- An account with elevated rights to install the dependencies +- `curl` or `wget` installed +- Node.js [Active LTS Release](../overview/versioning-policy.md#nodejs-releases) installed using one of these + methods: + - Using `nvm` (recommended) + - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) + - [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm) + - Node 24 is a good starting point, this can be installed using `nvm install lts/krypton` + - [Binary Download](https://nodejs.org/en/download/) + - [Package manager](https://nodejs.org/en/download/package-manager/) + - [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md) +- Install the `isolated-vm` module, following their [requirements section](https://github.com/laverdet/isolated-vm#requirements). +- `yarn` [Installation](https://yarnpkg.com/getting-started/install) + - Backstage currently uses Yarn 4.4.1, once you've ran `corepack enable` you'll want to then run `yarn set version 4.4.1` +- `docker` [installation](https://docs.docker.com/engine/install/) +- `git` [installation](https://github.com/git-guides/install-git) +- If the system is not directly accessible over your network the following ports + need to be opened: 3000, 7007. This is quite uncommon, unless you're installing in a container, VM or remote system. + +## Creating and running a Backstage application This may take a few minutes to fully install everything. Don't stress if the loading seems to be spinning nonstop, there's a lot going on in the background. -:::note +To create the application: -If this fails on the `yarn install` step, it's likely that you will need to install some additional dependencies which are used to configure `isolated-vm`. You can find out more in their [requirements section](https://github.com/laverdet/isolated-vm#requirements), and then run `yarn install` manually again after you've completed those steps. +1. Type the following command to install the Backstage application. -::: +```bash + npx @backstage/create-app@latest +``` -## 2. Run the Backstage app +2. If this is the first time that you are installing a Backstage application on this device, the following question is displayed. Enter `y` and select `Enter` to proceed with the installation. + +``` + Need to install the following packages: + @backstage/create-app@0.7.4 + ok to proceed? (y) +``` + +3. Enter the name for your application and select `Enter`. This is the root directory of your application. In this example, the name is set to `my-backstage-app`. + + ![create app](../assets/getting-started/create-app-output.png) Your Backstage app is fully installed and ready to be run! Now that the installation is complete, you can go to the application directory and start the app using the `yarn start` command. The `yarn start` command will run both the frontend and backend as separate processes (named `[0]` and `[1]`) in the same window. -```bash -cd my-backstage-app # your app name -yarn start +To run the application: + +1. Change to the root directory of your Backstage app. This is the same as the name of your application that you provided during the installation. In this example, it is `my-backstage-app`. + + ```bash + cd my-backstage-app + ``` + +2. Start the Backstage application. + + ```bash + yarn start + ``` + +As the frontend and backend are starting, you will see output similar to the following. The output shows that the app and backend are starting up with the configuration coming from `app-config.yaml`. You will see the plugins being initialized, and authorization and permissions being setup. In addition you will see a series of REST API calls for those plugins that use a service backend, such as the service catalog. + +``` +Starting app, backend +Loaded config from app-config.yaml +. +. +2025-10-15T12:26:41.564Z backstage info Plugin initialization started: 'app', 'proxy', 'scaffolder', 'techdocs', 'auth', 'catalog', 'permission', 'search', 'kubernetes', 'notifications', 'signals' type="initialization" +Rspack compiled successfully +. +. +2025-10-15T15:17:21.130Z auth info Created new signing key eec1a9e4-4395-4698-9a9f-f1b5cbcf152b component="token-factory" +2025-10-15T15:17:21.139Z auth info Issuing token for user:development/guest, with entities user:development/guest component="token-factory" +2025-10-15T15:17:21.223Z rootHttpRouter info [2025-10-15T15:17:21.223Z] "GET /api/auth/guest/refresh HTTP/1.1" 200 802 "http://localhost:3000/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0" type="incomingRequest" date="2025-10-15T15:17:21.223Z" method="GET" url="/api/auth/guest/refresh" status=200 httpVersion="1.1" userAgent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0" contentLength=802 referrer="http://localhost:3000/" +. +. +2025-10-15T15:17:24.051Z rootHttpRouter info [2025-10-15T15:17:24.051Z] "GET /api/catalog/entities?fields=metadata,kind,spec.profile&filter=kind%3Dgroup%2Crelations.hasMember%3Duser%3Adevelopment%2Fguest HTTP/1.1" 304 0 "http://localhost:3000/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0" type="incomingRequest" date="2025-10-15T15:17:24.051Z" method="GET" url="/api/catalog/entities?fields=metadata,kind,spec.profile&filter=kind%3Dgroup%2Crelations.hasMember%3Duser%3Adevelopment%2Fguest" status=304 httpVersion="1.1" userAgent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0" referrer="http://localhost:3000/" +. +. ``` -![Screenshot of the command output, with the message web pack compiled successfully](../assets/getting-started/startup.png) +Once the Backstage UI is displayed, you can start exploring the demo immediately. -Here again, there's a small wait for the frontend to start up. Once the frontend is built, your browser window should automatically open. +:::tip Browser window didn't open with yarn start -:::tip Browser window didn't open - -When you see the message `[0] webpack compiled successfully`, you can navigate directly to `http://localhost:3000` to see your Backstage app. +When you see the message `Rspack compiled successfully`, you can navigate directly to `http://localhost:3000` to see your Backstage app. ::: -You can start exploring the demo immediately. - ![Screenshot of the Backstage portal.](../assets/getting-started/portal.png) -## Recap - -This tutorial walked through how to deploy Backstage using the `npx @backstage/create-app@latest` command. That command created a new directory that holds your new Backstage app. That app is currently only configured for development purposes, as it is using an in-memory database and contains demo data. - ## Next steps Choose the correct next steps for your user role, if you're likely to be deploying and managing a Backstage instance for your organization, look through the [Admin](#admin) section. If you're likely to be developing on/for Backstage, take a look through the [Developer](#developer) section. ### Admin -- Deploying to production - +- Configuring Backstage - [Setting up authentication](./config/authentication.md) - [Configuring a database](./config/database.md) - - [Deploying with Docker](../deployment/docker.md) - - [Deploying with Kubernetes](../deployment/k8s.md) - -- Configuring Backstage - - [Adding plugins](./configure-app-with-plugins.md) - [Customizing Your App's UI](../conf/user-interface/index.md) - [Populating the homepage](./homepage.md) +- Deploying to production + - [Deploying with Docker](../deployment/docker.md) + - [Deploying with Kubernetes](../deployment/k8s.md) ### Developer @@ -171,8 +191,4 @@ Share your experiences, comments, or suggestions with us: [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/community-plugins/issues/new/choose), -or -[bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) -you have, and feel free to -[contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! +[plugin suggestions](https://github.com/backstage/community-plugins/issues/new/choose) diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index e0fab954ac..3f5920be5d 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -54,6 +54,8 @@ You need to decide how you want to receive events from external sources like - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Further documentation: @@ -72,6 +74,8 @@ Additionally, you need to decide how you want to receive events from external so - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Set up your provider diff --git a/docs/integrations/bitbucketCloud/locations.md b/docs/integrations/bitbucketCloud/locations.md index f0a032353c..807076c9e9 100644 --- a/docs/integrations/bitbucketCloud/locations.md +++ b/docs/integrations/bitbucketCloud/locations.md @@ -14,11 +14,31 @@ plugin. ## Configuration +API token usage example (recommended): + ```yaml integrations: bitbucketCloud: - - username: ${BITBUCKET_CLOUD_USERNAME} - appPassword: ${BITBUCKET_CLOUD_PASSWORD} + - username: user@domain.com # username -> user email + token: my-token +``` + +Legacy: + +```yaml +integrations: + bitbucketCloud: + - username: username + appPassword: my-password +``` + +OAuth 2.0 client credentials flow: + +```yaml +integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret ``` :::note Note @@ -30,7 +50,7 @@ convenience, so you only need to list it if you want to supply credentials. :::note Note -The credential used for this is type [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). An Atlassian Account API key will not work. +The credential required for this type is either an [Api token](https://support.atlassian.com/bitbucket-cloud/docs/using-api-tokens/), an [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) or an [OAuth 2.0 client credentials](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/). An Atlassian Account API key will not work. ::: @@ -42,4 +62,7 @@ This one entry will have the following elements: - `username`: The Bitbucket Cloud username to use in API requests. If neither a username nor token are supplied, anonymous access will be used. +- `token`: The token used to authenticate requests. - `appPassword`: The app password for the Bitbucket Cloud user. +- `clientId`: The OAuth client ID for Bitbucket Cloud (used with `clientSecret` for OAuth 2.0 client credentials flow). +- `clientSecret`: The OAuth client secret for Bitbucket Cloud (used with `clientId` for OAuth 2.0 client credentials flow). diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index 350bc33aed..bbdd105af4 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -42,6 +42,8 @@ You need to decide how you want to receive events from external sources like - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Further documentation: diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index e01729dff0..e17f514058 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -141,7 +141,7 @@ events: region: us-east-2 ``` -The [AWS SQS module `README`](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-aws-sqs/README.md#configuration) has more details on the configuration options, the example above includes on the required options. +The [AWS SQS module `README`](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-aws-sqs/README.md#configuration) has more details on the configuration options, the example above includes only the required options. ### Events Setup using Google Pub/Sub module @@ -179,7 +179,53 @@ events: targetTopic: 'github.{{ event.attributes.x-github-event }}' ``` -The [Google Pub/Sub module `README`](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-google-pubsub/README.md#configuration) has more details on the configuration options, the example above includes on the required options. +The [Google Pub/Sub module `README`](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-google-pubsub/README.md#configuration) has more details on the configuration options, the example above includes only the required options. + +### Events Setup using Kafka module + +Alternatively to using the HTTP endpoint you can use the Kafka module, here's how. + +First we need to add the package: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-events-backend-module-kafka +``` + +Then we need to add it to your backend: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend-module-github')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-events-backend-module-kafka')); +/* highlight-add-end */ +``` + +Finally you will want to configure it: + +```yaml title="app-config.yaml +events: + modules: + kafka: + kafkaConsumingEventPublisher: + # Client ID used by Backstage to identify when connecting to the Kafka cluster. + clientId: your-client-id + # List of brokers in the Kafka cluster to connect to. + brokers: + - broker1 + - broker2 + topics: + # Replace with actual topic name as expected by subscribers + - topic: 'backstage.topic' + kafka: + # The Kafka topics to subscribe to. + topics: + - topic1 + # The GroupId to be used by the topic consumers. + groupId: your-group-id +``` + +The [Kafka module `README`](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-kafka/README.md#configuration) has more details on the configuration options, the example above includes only the required options. ## Configuration @@ -210,6 +256,8 @@ catalog: filters: # optional filters branch: 'develop' # optional string repository: '.*' # optional Regex + pageSizes: + repositories: 25 wildcardProviderId: organization: 'new-org' # string catalogPath: '/groups/**/*.yaml' # this will search all folders for files that end in .yaml @@ -308,6 +356,10 @@ If you do so, `default` will be used as provider ID. The amount of time that should pass before the first invocation happens. - **`scope`** _(optional)_: `'global'` or `'local'`. Sets the scope of concurrency control. +- **`pageSizes`** _(optional)_: + Configure page sizes for GitHub GraphQL API queries. This can help prevent `RESOURCE_LIMITS_EXCEEDED` errors. + - **`repositories`** _(optional)_: + Number of repositories to fetch per page. Defaults to `25`. Reduce this value if hitting API resource limits. ## GitHub API Rate Limits diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 473d408b0b..2dac8b8866 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -79,6 +79,10 @@ catalog: initialDelay: { seconds: 30 } frequency: { hours: 1 } timeout: { minutes: 50 } + pageSizes: + teams: 25 + teamMembers: 50 + organizationMembers: 50 - id: ghe githubUrl: https://ghe.mycompany.com orgs: ['internal-1', 'internal-2', 'internal-3'] @@ -86,6 +90,7 @@ catalog: initialDelay: { seconds: 30 } frequency: { hours: 1 } timeout: { minutes: 50 } + excludeSuspendedUsers: true ``` Directly under the `githubOrg` is a list of configurations, each entry is a structure with the following elements: @@ -94,6 +99,15 @@ Directly under the `githubOrg` is a list of configurations, each entry is a stru - `githubUrl`: The target that this provider should consume - `orgs` (optional): The list of the GitHub orgs to consume. If you only list a single org the generated group entities will use the `default` namespace, otherwise they will use the org name as the namespace. By default the provider will consume all accessible orgs on the given GitHub instance (support for GitHub App integration only). - `schedule`: The refresh schedule to use, matches the structure of [`SchedulerServiceTaskScheduleDefinitionConfig`](https://backstage.io/docs/reference/backend-plugin-api.schedulerservicetaskscheduledefinitionconfig/) +- `pageSizes` (optional): Configure page sizes for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors. You can configure the following page sizes: + + - `teams`: Number of teams to fetch per page when querying organization teams (default: 25) + - `teamMembers`: Number of team members to fetch per page when querying team members (default: 50) + - `organizationMembers`: Number of organization members to fetch per page (default: 50) + + Reducing page sizes will result in more API calls and slightly longer sync times, but will prevent API resource limits for organizations with large number of teams and members. + +- `excludeSuspendedUsers` (optional): Whether to exclude suspended users when querying organization users. Only for GitHub Enterprise instances. Will error if used against GitHub.com API. ### Events Support @@ -118,6 +132,8 @@ You can decide between the following options (extensible): - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `organization`,`team` and `membership` events. diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 5e02c853fa..3d47edba6d 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -43,6 +43,8 @@ You need to decide how you want to receive events from external sources like - [via HTTP endpoint](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md#configuration) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Further documentation: @@ -92,6 +94,8 @@ Additionally, you need to decide how you want to receive events from external so - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Set up your provider diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index 8249a97c24..2908bb7c09 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -49,6 +49,8 @@ You need to decide how you want to receive events from external sources like - [via HTTP endpoint](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md#configuration) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Further documentation: @@ -101,6 +103,8 @@ Additionally, you need to decide how you want to receive events from external so - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) +- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) +- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) Set up your provider diff --git a/docs/notifications/index.md b/docs/notifications/index.md index 59ff2962ed..dce58d3214 100644 --- a/docs/notifications/index.md +++ b/docs/notifications/index.md @@ -164,6 +164,61 @@ You can customize the origin names shown in the UI by passing an object where th Each notification processor will receive its own row in the settings page, where the user can enable or disable notifications from that processor. +### Default notification settings + +You can configure default notification settings for all users in your `app-config.yaml` file. This allows you to set up notification preferences globally, such as disabling specific channels or origins by default, implementing an opt-in strategy instead of opt-out. + +#### Channel-level defaults + +You can set a default enabled state for an entire channel. When set to `false`, the channel uses an opt-in strategy where notifications are disabled by default unless explicitly enabled by the user or for specific origins. + +```yaml +notifications: + defaultSettings: + channels: + - id: 'Web' + enabled: false # Opt-in strategy: channel disabled by default + - id: 'Email' + enabled: true # Opt-out strategy: channel enabled by default (default behavior) +``` + +#### Origin-level defaults + +You can also configure defaults for specific origins within a channel: + +```yaml +notifications: + defaultSettings: + channels: + - id: 'Web' + enabled: true # Channel is enabled by default + origins: + - id: 'plugin:scaffolder' + enabled: false # Disable scaffolder notifications by default + - id: 'plugin:catalog' + enabled: true # Enable catalog notifications by default +``` + +#### Topic-level defaults + +For even more granular control, you can set defaults for specific topics within origins: + +```yaml +notifications: + defaultSettings: + channels: + - id: 'Email' + enabled: false # Email is opt-in by default + origins: + - id: 'plugin:catalog' + enabled: true # But catalog notifications are enabled + topics: + - id: 'entity:validation:error' + enabled: false # Except validation errors +``` + +**Note:** If a channel's `enabled` flag is not set, it defaults to `true` for backwards compatibility. When a channel is set to `enabled: false`, all origins within that channel default to disabled unless explicitly enabled. + ### Automatic notification cleanup Notifications are deleted automatically after a certain period of time to prevent the database from growing indefinitely diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index c25b669ed6..af0b9f8f93 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -149,11 +149,63 @@ notifications: broadcastChannels: # Optional, if you wish to support broadcast notifications. - C12345678 username: 'Backstage Bot' # Optional, defaults to the name of the Slack App. + concurrencyLimit: 20 # Optional, number of messages allowed per interval. Defaults to 10. + throttleInterval: 1m # Optional, Accepts ISO-8601 duration, ms-style ("1m", "30s"), or HumanDuration ({ minutes: 2 }). Defaults to 1 minute ``` Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send messages to more than one Slack workspace. Org-Wide App installation is not currently supported. +### Broadcast Channel Routing + +For more granular control over where broadcast notifications are sent, you can use `broadcastRoutes` to route notifications to different Slack channels based on their origin and/or topic. This is useful when you want different types of notifications to go to different channels. + +```yaml +notifications: + processors: + slack: + - token: xoxb-XXXXXXXXX + # Legacy option - used as fallback when no routes match + broadcastChannels: + - general-notifications + # Route broadcasts based on origin and/or topic + broadcastRoutes: + # Most specific: matches both origin AND topic + - origin: plugin:catalog + topic: alerts + channel: catalog-alerts + # Origin only: all notifications from this origin + - origin: plugin:catalog + channel: catalog-updates + # Topic only: all notifications with this topic (any origin) + - topic: security + channel: security-team + # Multiple channels: send to several channels at once + - origin: external:monitoring + channel: + - ops-team + - on-call-alerts +``` + +#### Route Matching Precedence + +Routes are evaluated in the following order of priority: + +1. **Origin + Topic match** (most specific) - A route that specifies both `origin` and `topic` will match first +2. **Origin-only match** - A route with only `origin` specified (no `topic`) +3. **Topic-only match** - A route with only `topic` specified (no `origin`) +4. **Default fallback** - If no routes match, falls back to `broadcastChannels` + +The first matching route wins. If no routes match and no `broadcastChannels` are configured, the broadcast notification will not be sent to Slack. + +#### Configuration Options + +| Property | Type | Description | +| --------- | ---------------------- | ------------------------------------------------------------------------------------------ | +| `origin` | `string` | Optional. The notification origin to match (e.g., `plugin:catalog`, `external:my-service`) | +| `topic` | `string` | Optional. The notification topic to match (e.g., `alerts`, `updates`, `security`) | +| `channel` | `string` or `string[]` | Required. The Slack channel(s) to send to. Can be channel IDs, channel names, or user IDs | + ### Entity Requirements Entities must be annotated with the following annotation: diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 669a982e1a..420b6489ae 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -177,7 +177,7 @@ When we say _Supporting_ a Node.js release, that means the following: - New Backstage projects created with `@backstage/create-app` will have their `engines.node` version set accordingly. - Dropping compatibility with unsupported releases is not considered a breaking change. This includes using new syntax or APIs, as well as bumping dependencies that drop support for these versions. -Based on the above Backstage supports Node.js 20 and 22 as of the `1.33.0` release. +Based on the above Backstage supports Node.js 22 and 24 as of the `1.46.0` release. ## TypeScript Releases diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index fa1c75de44..024bea8f81 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -39,8 +39,6 @@ Out of the box, Backstage includes: Backstage is a CNCF Incubation project after graduating from Sandbox. Read the announcement [here](https://backstage.io/blog/2022/03/16/backstage-turns-two#out-of-the-sandbox-and-into-incubation). -CNCF logo - ## Benefits - For _engineering managers_, it allows you to maintain standards and best diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 7bfcd1220a..d90f36e59d 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -159,7 +159,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer ```typescript title="packages/backend/src/modules/catalogPermissionRules.ts" import { createBackendModule } from '@backstage/backend-plugin-api'; import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; - import { isInSystemRule } from './permissionPolicyExtension'; + import { isInSystemRule } from './permissionsPolicyExtension'; export default createBackendModule({ pluginId: 'catalog', diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index bfd210aeb9..9b8b63fc89 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -48,11 +48,20 @@ This will think for a bit, and then say `Listening on :7007`. In a different terminal window, now run ```sh -curl localhost:7007/api/carmen/health +curl localhost:7007/api/carmen/todos ``` -This should return `{"status":"ok"}`. Success! Press `Ctrl + c` to stop it -again. +You should see the following response: + +```json +{ + "items": [] +} +``` + +:::note Note: The route shown here matches the default in the current backend plugin template. If you want a `/health` endpoint for health checks, you can add it to your router yourself. + +::: ## Developing your Backend Plugin diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md deleted file mode 100644 index 181650adc1..0000000000 --- a/docs/plugins/existing-plugins.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -id: existing-plugins -title: Existing plugins -description: Lists of existing open source plugins ---- - -## The Plugin Directory - -Open source plugins that you can add to your Backstage deployment can be found -at: - -https://backstage.io/plugins - -![](https://backstage.io/blog/assets/marketplace.png) - -## Links - -- [[blog] The Plugin Directory is open](https://backstage.io/blog/2020/09/30/plugin-marketplace) diff --git a/docs/plugins/index.md b/docs/plugins/index.md index f709afa894..ef45152f55 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -23,3 +23,9 @@ This approach is equally beneficial if you conceive an idea for a potentially im ## Integration with the Software Catalog Should your plugin complement the Software Catalog rather than exist as a standalone feature (for instance, as an additional tab or a card within an "Overview" tab), you'll find comprehensive guidance on achieving this integration in the [Integrating Plugin into Software Catalog guide](integrating-plugin-into-software-catalog.md). + +## Existing Plugins + +There are a plethora of existing community made plugins which can be found by going to the [Backstage Plugin Directory](https://backstage.io/plugins) + +You can read more about it in this blog post: [The Plugin Directory is now open](https://backstage.io/blog/2020/09/30/plugin-marketplace/) diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index ed2e25c601..3542e073cb 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -32,7 +32,7 @@ Creating the plugin... ### Reading entities from within your plugin You can access the currently selected entity using the backstage api -[`useEntity`](../reference/plugin-catalog-react.useentity.md). For example, +[`useEntity`](https://backstage.io/api/stable/functions/_backstage_plugin-catalog-react.index.useEntity.html). For example, ```tsx import { useEntity } from '@backstage/plugin-catalog-react'; diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 0d18942320..7cfaca4561 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -81,8 +81,8 @@ export const ExamplePage = examplePlugin.provide( ``` This is where the plugin is created and where it creates and exports extensions -that can be imported and used the app. See reference docs for -[`createPlugin`](../reference/core-plugin-api.createplugin.md) or introduction to +that can be imported and used in the app. See reference docs for +[`createPlugin`](https://backstage.io/api/stable/functions/_backstage_core-plugin-api.index.createPlugin.html) or introduction to the new [Composability System](./composability.md). ## Components diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 7292ac9754..1297d516f3 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -4,6 +4,12 @@ title: Testing with Jest description: Documentation on How to do unit testing with Jest --- +:::note Note + +You may want to consider migrating to Jest 30, to do this, you can follow this guide: [Migrating to Jest 30](../tutorials/jest30-migration.md) + +::: + Backstage uses [Jest](https://facebook.github.io/jest/) for all our unit testing needs. @@ -185,6 +191,11 @@ loading is broken, not that the loading indicator is broken. ## Examples +For more specific examples of how to test your Backstage **backend plugins** and **modules** or **frontend plugins** you can check out the following guides: + +- [Testing Backend Plugins and Modules](../backend-system/building-plugins-and-modules/02-testing.md) +- [Testing Frontend Plugins](../frontend-system/building-plugins/02-testing.md) + ### Utility Functions A utility function is a function with no side effects. It takes in arguments and diff --git a/docs/publishing.md b/docs/publishing.md index e6c0b6b466..15b613f800 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -69,17 +69,23 @@ Once the release has been published edit the newly created release in the [GitHu **This emergency release process is intended only for the Backstage maintainers.** -Given one or more PRs towards master that we want to create a patch release for, run the following script from the repo root: +Given one or more PRs towards master that we want to create a patch release for, add them to the patch release queue by running the following command for each PR: ```bash -./scripts/patch-release-for-pr.js ... +yarn patch-pr ``` -Wait until the script has finished executing, at the end of the output you will find a link of the format `https://github.com/backstage/backstage/pull/new/patch-release-pr-...`. Open this link in your browser to create a PR for the patch release. Finish the sentence "This release fixes an issue where..." and create the PR. +This creates a patch file in the `.patches/` directory (e.g., `.patches/pr-12345.txt`) containing the description of the fix. The [sync_patch-release.yml](https://github.com/backstage/backstage/blob/master/.github/workflows/sync_patch-release.yml) workflow will automatically detect these patch files and create or update a "Patch Release" PR. -Once the PR has been approved and merged, the patch release will be automatically created. The patch release is complete when a notification has been posted to Discord in the `#announcements` channel. Keep an eye on "Deploy Packages" workflow and re-trigger if it fails. It is safe to re-trigger any part of this workflow, including the release step. +The workflow will: -If the above process fails, you can fall back to the manual process documented below. +- Automatically create a patch release PR if one doesn't exist +- Update the existing PR if patch files are added, modified, or removed +- Close and delete the PR branch if all patch files are removed + +Once the "Patch Release" PR has been approved and merged, the patch release will be automatically created. The patch files will be automatically removed from the master branch after the patch release is merged. From here the patch release process is the same as the usual release process, starting with the notification in the `#maintainers` channel on Discord. + +If the above process fails, you can fall back to the manual process documented below, or run the `./scripts/patch-release-for-pr.js ...` script to manually create a patch release PR. ### Old Process diff --git a/docs/releases/v1.45.0-changelog.md b/docs/releases/v1.45.0-changelog.md new file mode 100644 index 0000000000..23def9a969 --- /dev/null +++ b/docs/releases/v1.45.0-changelog.md @@ -0,0 +1,2607 @@ +# Release v1.45.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.45.0](https://backstage.github.io/upgrade-helper/?to=1.45.0) + +## @backstage/backend-app-api@1.3.0 + +### Minor Changes + +- a17d9df: Updates API for `instanceMetadata` service to return a list of plugins not features. + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + +## @backstage/backend-plugin-api@1.5.0 + +### Minor Changes + +- a17d9df: Promote `instanceMetadata` service to main entrypoint. +- 62fc2de: Explicitly mark `coreServices.rootInstanceMetadata` as a root service. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/backend-test-utils@1.10.0 + +### Minor Changes + +- d57b13b: Added support for Postgres 18 to the available `TestDatabases`. + + Note that the set of _default_ databases to test against for users of the `TestDatabases` class was also updated to include Postgres 14 and 18, instead of 13 and 17. If you need to override this, you can pass in an explicit `ids` argument, for example `ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3']`. + +### Patch Changes + +- f3001fd: Tweak some of the mock services to have more precise types +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/backend-app-api@1.3.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/core-plugin-api@1.12.0 + +### Minor Changes + +- 83439b1: All route references are now forwards compatible with the new frontend system, i.e. `@backstage/frontend-plugin-api`. This means they no longer need to be converted with `convertLegacyRouteRef` or `convertLegacyRouteRefs` from `@backstage/core-compat-api`. + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/eslint-plugin@0.2.0 + +### Minor Changes + +- 926389b: Added `@backstage/no-ui-css-imports-in-non-frontend` rule, which ensures that CSS from `@backstage/ui` is not imported outside of the frontend app. + +## @backstage/frontend-plugin-api@0.13.0 + +### Minor Changes + +- 7d87b4f: Renamed `ExtensionDefinition` to `OverridableExtensionDefinition` and introduced a slimmer `ExtensionDefinition` type that does not include override methods. The overridable type is generally used as an output type, while plain `ExtensionDefinition`s are used for input. This reduces type conflicts across different of `@backstage/frontend-plugin-api`, improving long-term compatibility. + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. + +- 7c6a66d: Added support for plugin-relative `attachTo` declarations for extension definitions. This allows for the creation of extension and extension blueprints that attach to other extensions of a particular `kind` in the same plugin, rather than needing to provide the exact extension ID. This is particularly useful when wanting to provide extension blueprints with a built-in hierarchy where the extensions created from one blueprint attach to extensions created from the other blueprint, for example: + + ```ts + // kind: 'tabbed-page' + const parentPage = TabbedPageBlueprint.make({ + params: {....} + }) + // attachTo: { kind: 'tabbed-page', input: 'tabs' } + const child1 = TabContentBlueprint.make({ + name: 'tab1', + params: {....} + }) + ``` + +- 878c251: Updated to `ExtensionInput` to make all type parameters optional. + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/repo-tools@0.16.0 + +### Minor Changes + +- 11c61f2: The `package-docs` command will now automatically use a `typedoc.json` file if one exists at the root of your project. + +### Patch Changes + +- 8f56eae: Updated knip-reports to detect dependencies in dev/alpha pattern +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config-loader@1.10.6 + - @backstage/cli-node@0.2.15 + - @backstage/catalog-model@1.7.6 + +## @backstage/ui@0.9.0 + +### Minor Changes + +- 539cf26: **BREAKING**: Migrated Avatar component from Base UI to custom implementation with size changes: + + - Base UI-specific props are no longer supported + - Size values have been updated: + - New `x-small` size added (1.25rem / 20px) + - `small` size unchanged (1.5rem / 24px) + - `medium` size unchanged (2rem / 32px, default) + - `large` size **changed from 3rem to 2.5rem** (40px) + - New `x-large` size added (3rem / 48px) + + Migration: + + ```diff + # Remove Base UI-specific props + - + + + + # Update large size usage to x-large for same visual size + - + + + ``` + + Added `purpose` prop for accessibility control (`'informative'` or `'decoration'`). + +- 5c614ff: **BREAKING**: Migrated Checkbox component from Base UI to React Aria Components. + + API changes required: + + - `checked` → `isSelected` + - `defaultChecked` → `defaultSelected` + - `disabled` → `isDisabled` + - `required` → `isRequired` + - `label` prop removed - use `children` instead + - CSS: `bui-CheckboxLabel` class removed + - Data attribute: `data-checked` → `data-selected` + - Use without label is no longer supported + + Migration examples: + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accept terms + + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + Option + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accessible label + + ``` + +- 134151f: Fixing styles on SearchField in Backstage UI after migration to CSS modules. `SearchField` has now its own set of class names. We previously used class names from `TextField` but this approach was creating some confusion so going forward in your theme you'll be able to theme `TextField` and `SearchField` separately. + +- a67670d: **BREAKING**: Removed central `componentDefinitions` object and related type utilities (`ComponentDefinitionName`, `ComponentClassNames`). + + Component definitions are primarily intended for documenting the CSS class API for theming purposes, not for programmatic use in JavaScript/TypeScript. + + **Migration Guide:** + + If you were using component definitions or class names to build custom components, we recommend migrating to either: + + - Use Backstage UI components directly as building blocks, or + - Duplicate the component CSS in your own stylesheets instead of relying on internal class names + +- b78fc45: **BREAKING**: Changed className prop behavior to augment default styles instead of being ignored or overriding them. + + Affected components: + + - Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator + - Switch + - Skeleton + - FieldLabel + - Header, HeaderToolbar + - HeaderPage + - Tabs, TabList, Tab, TabPanel + + If you were passing custom className values to any of these components that relied on the previous behavior, you may need to adjust your styles to account for the default classes now being applied alongside your custom classes. + +- 83c100e: **BREAKING**: Removed `Collapsible` component. Migrate to `Accordion` or use React Aria `Disclosure`. + + ## Migration Path 1: Accordion (Opinionated Styled Component) + + Accordion provides preset styling with a similar component structure. + + ```diff + - import { Collapsible } from '@backstage/ui'; + + import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui'; + + - + - } /> + - Content + - + + + + + + + Content + + + ``` + + CSS classes: `.bui-CollapsibleRoot` → `.bui-Accordion`, `.bui-CollapsibleTrigger` → `.bui-AccordionTrigger` (now on heading element), `.bui-CollapsiblePanel` → `.bui-AccordionPanel` + + ## Migration Path 2: React Aria Disclosure (Full Customization) + + For custom styling without preset styles: + + ```tsx + import { Disclosure, Button, DisclosurePanel } from 'react-aria-components'; + + + + Content + ; + ``` + +- 816af0f: **BREAKING**: The `SelectProps` interface now accepts a generic type parameter for selection mode. + + Added searchable and multiple selection support to Select component. The component now accepts `searchable`, `selectionMode`, and `searchPlaceholder` props to enable filtering and multi-selection modes. + + Migration: If you're using `SelectProps` type directly, update from `SelectProps` to `SelectProps<'single' | 'multiple'>`. Component usage remains backward compatible. + +### Patch Changes + +- d01de00: Fix broken external links in Backstage UI Header component. +- 35a3614: Fixed CSS issues in Select component including popover width constraints, focus outline behavior, and overflow handling. +- 01476f0: Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components. +- 26c6a78: Fix default text color in Backstage UI +- deaa427: Fixed Text component to prevent `truncate` prop from being spread to the underlying DOM element. +- 1059f95: Improved the Link component structure in Backstage UI. +- 836b0c7: Fixed dialog backdrop appearance in dark mode. +- 6874094: Migrated CellProfile component from Base UI Avatar to Backstage UI Avatar component. +- 719d772: Avatar components in x-small and small sizes now display only one initial instead of two, improving readability at smaller dimensions. +- 6d35a6b: Removed `@base-ui-components/react` dependency as all components now use React Aria Components. +- dac851f: Fix the default font size in Backstage UI. +- 3c0ea67: Fix CSS layer ordering in Backstage UI to make sure component styles are loaded after tokens and base declarations. +- 3b18d80: Fixed RadioGroup radio button ellipse distortion by preventing flex shrink and grow. +- 4eb455c: Fix font smoothing as default in Backstage UI. +- ff9f0c3: Enable tree-shaking of imports other than `*.css`. +- 7839e7b: Added `loading` prop to Button and ButtonIcon components for displaying spinner during async operations. +- a00fb88: Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the `href` prop. +- e16ece5: Set the color-scheme property depending on theme +- 1ef3ca4: Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. +- 00bfb83: Fix default font wight and font family in Backstage UI. + +## @backstage/plugin-catalog@1.32.0 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- b500bcc: Fix React warnings for elements not having stable keys. +- a87c0a5: Fixed responsiveness issues with catalog entity content layout using the new frontend system. +- aff04d3: Fixed missing margins for entity warning content area on catalog entity page in NFS. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/catalog-client@1.12.1 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-scaffolder-common@1.7.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-catalog-backend@3.2.0 + +### Minor Changes + +- 2d229b2: Enable YAML merge keys in yamlPlaceholderResolver +- 9d3ec06: Make YAML merge (<<:) support configurable in the Backstage Catalog instead of always being enabled +- 8c26af4: Enable YAML merge keys in yamlPlaceholderResolver + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/backend-openapi-utils@0.6.3 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.0 + +### Minor Changes + +- 980f240: Moved from `ldapjs` dependency to `ldapts` + + ### Breaking Changes + + **Type Migration** + + Custom transformers must now accept `Entry` from `ldapts` instead of `SearchEntry` + from `ldapjs` The Entry type provides direct property access without need for + `.object()` or `.raw()` methods. + + If you have custom user or group transformers, update the signature from: + + ```typescript + (vendor: LdapVendor, config: UserConfig, entry: SearchEntry) => + Promise; + ``` + + to + + ```typescript + (vendor: LdapVendor, config: UserConfig, entry: Entry) => + Promise; + ``` + + **Search Options** + + Updated LDAP search configuration `typesOnly: false` → `attributeValues: true` + This inverts the boolean logic: `ldapjs` used negative form while `ldapts` uses + positive form. Both achieve the same result: retrieving attribute values rather + than just attribute names. + + Update LDAP search options in configuration from + + ```yaml + options: + typesOnly: false + ``` + + to + + ```yaml + options: + attributeValues: true + ``` + + **API Changes** Removed `LdapClient.searchStreaming()` method. Users should + migrate to `LdapClient.search()` instead + + If you're using `searchStreaming` directly: + + ```typescript + // Before + await client.searchStreaming(dn, options, async entry => { + // process each entry + }); + + // After + const entries = await client.search(dn, options); + for (const entry of entries) { + // process each entry + } + ``` + + > **_NOTE:_**: Both methods have always loaded all entries into memory. The + > `searchStreaming` method was only needed internally to handle `ldapjs`'s + > event-based API. + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-node@1.20.0 + +### Minor Changes + +- 9d3ec06: Make YAML merge (<<:) support configurable in the Backstage Catalog instead of always being enabled +- 8c26af4: Enable YAML merge keys in yamlPlaceholderResolver + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-gateway-backend@1.1.0 + +### Minor Changes + +- 229f63e: Added hop count tracking to prevent proxy loops. The gateway now tracks the number of proxy hops using the `backstage-gateway-hops` header and rejects requests that exceed 3 hops with a 508 Loop Detected error. +- a17d9df: Update usage of the `instanceMetadata` service. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## @backstage/plugin-notifications-backend@0.6.0 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- 15fb764: Show default settings for notifications even before receiving first notification. + + Previously, it was not possible for the users to see or modify their notification settings until they had received at + least one notification from specific origin or topic. + This update ensures that default settings are displayed from the outset, + allowing users to customize their preferences immediately. + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-signals-node@0.1.26 + +## @backstage/plugin-notifications-common@0.2.0 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.10.0 + +### Minor Changes + +- ff96d7e: fix scaffolder action createDeployToken to allow usage of oauth tokens + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-search@1.5.0 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-react@1.10.0 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-techdocs@1.16.0 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- a4d4a70: Fixed an issue where the entire TechDocs page would re-render when navigating between pages within the same entity's documentation. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/integration@1.18.2 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/catalog-client@1.12.1 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-auth-react@0.1.21 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/app-defaults@1.7.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-permission-react@0.4.38 + +## @backstage/backend-defaults@0.13.1 + +### Patch Changes + +- 9bcfa77: Adjusted the log line wording of task worker starting + +- 91ab2eb: Fix a bug in the Gitlab URL reader where `search` did not handle multiple globs + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- b2f6a5a: Fix #31348 issue where BitbucketUrlReader ignored provided token and instead always used integration credentials + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/backend-app-api@1.3.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/backend-dynamic-feature-service@0.7.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/plugin-events-backend@0.5.8 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/plugin-scaffolder-node@0.12.1 + - @backstage/backend-openapi-utils@0.6.3 + - @backstage/plugin-app-node@0.1.39 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/backend-openapi-utils@0.6.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## @backstage/catalog-client@1.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + +## @backstage/catalog-model@1.7.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +## @backstage/cli@0.34.5 + +### Patch Changes + +- fc7cbfc: The templates executed with the `yarn new` command now supports templating filenames. +- da19cb5: Fix inconsistent behavior in the `new` command for the `@internal` scope: it now consistently defaults to the `backstage-plugin-` infix whether the `--scope` option is not set or it's set to `internal`. +- fc7cbfc: Added a template for the `yarn new` command to create an catalog entity provider. To add this template to an explicit list in the root `package.json`, use `@backstage/cli/templates/catalog-provider-module`. +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 279279c: Fixes an issue where using the `backstage-cli new --scope` command with a scope that already includes the `@` symbol (e.g., `@backstage-community`) would result in a double `@@` prefix in the generated package name, causing invalid `package.json` files. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/eslint-plugin@0.2.0 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/catalog-model@1.7.6 + +## @backstage/cli-node@0.2.15 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +## @backstage/config@1.3.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +## @backstage/config-loader@1.10.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/core-app-api@1.19.2 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + +## @backstage/core-compat-api@0.5.4 + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/core-components@0.18.3 + +### Patch Changes + +- 1c7e1ce: - Revert `viewbox` back to old values. + - Added `fullScreen` prop to `EntityCatalogGraphCard` +- 96ad674: Line numbers in LogViewer will not be selectable in UI anymore +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + +## @backstage/create-app@0.7.6 + +### Patch Changes + +- 2c1fe37: Bumped create-app version. +- 20fae88: Bumped create-app version. +- 9f939a6: Added `@backstage/plugin-app-visualizer` to the app in the `--next` template. +- fc7cbfc: Added the new `@backstage/cli/templates/catalog-provider-module` template to the explicit template configuration for the `next-app` template. + +## @backstage/dev-utils@1.1.17 + +### Patch Changes + +- b29a856: Fixed styling of the dev app by adding a lazy import of `@backstage/ui/css/styles.css`. +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/catalog-model@1.7.6 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + +## @backstage/frontend-app-api@0.13.2 + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/frontend-defaults@0.3.3 + +## @backstage/frontend-defaults@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/core-components@0.18.3 + - @backstage/config@1.3.6 + - @backstage/plugin-app@0.3.2 + +## @backstage/frontend-dynamic-feature-loader@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/config@1.3.6 + +## @backstage/frontend-test-utils@0.4.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/config@1.3.6 + - @backstage/plugin-app@0.3.2 + - @backstage/test-utils@1.7.13 + +## @backstage/integration@1.18.2 + +### Patch Changes + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/integration-aws-node@0.1.19 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/integration-react@1.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + +## @techdocs/cli@1.10.2 + +### Patch Changes + +- c2a2017: Fix for missing styles due to move to BUI. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-techdocs-node@1.13.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/test-utils@1.7.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/config@1.3.6 + +## @backstage/plugin-api-docs@0.13.1 + +### Patch Changes + +- 7c281a5: Add i18n support for Raw tab title and an error message +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-app@0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/integration-react@1.2.12 + +## @backstage/plugin-app-backend@0.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/plugin-app-node@0.1.39 + +## @backstage/plugin-app-node@0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config-loader@1.10.6 + +## @backstage/plugin-app-visualizer@0.1.25 + +### Patch Changes + +- e81b3f0: Improve tree visualizer to use a horizontal layout and fill the content space. +- 722e2df: Migrated to use `@backstage/ui`. +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-auth@0.1.2 + +### Patch Changes + +- 1609e79: Authentication content screen now uses application title. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + +## @backstage/plugin-auth-backend@0.25.6 + +### Patch Changes + +- a9315d0: Change internal `state` column to `text` to support state of over 255 characters + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- 51ff7d8: Allow configuring dynamic client registration token expiration with config `auth.experimentalDynamicClientRegistration.tokenExpiration`. + + Maximum expiration for the DCR token is 24 hours. Default expiration is 1 hour. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-backend@0.25.6 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.9 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-backend@0.25.6 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-node@0.6.9 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-auth-react@0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-bitbucket-cloud-common@0.3.4 + +### Patch Changes + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + +- Updated dependencies + - @backstage/integration@1.18.2 + +## @backstage/plugin-catalog-backend-module-aws@0.4.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-azure@0.3.11 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/backend-openapi-utils@0.6.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.4 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-github@0.11.2 + +### Patch Changes + +- 999d1c1: Added configurable `pageSizes` for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors with organizations with large number of repositories. Please see the [GitHub Discovery documentation](https://backstage.io/docs/integrations/github/discovery#configuration) for new configuration options. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.16 + +### Patch Changes + +- 999d1c1: Added configurable `pageSizes` for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors with organizations with large number of teams and members. Please see the [GitHub Org Data documentation](https://backstage.io/docs/integrations/github/org#configuration-details) for new configuration options. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-catalog-backend-module-github@0.11.2 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.5 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.6 + +### Patch Changes + +- 70745c5: Correctly handle entity removal computation when DB count query returns string +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-catalog-backend-module-logs@0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.2 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.16 + +### Patch Changes + +- a5bcb2a: fix wrong dereferencing for AsyncApi 3 documents +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-scaffolder-common@1.7.3 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.11 + +## @backstage/plugin-catalog-common@1.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-catalog-graph@0.5.3 + +### Patch Changes + +- a2d7ae7: Ensure the catalog graph entity card respects the height prop so the visualization scales down properly on wide screens. +- 1c7e1ce: - Revert `viewbox` back to old values. + - Added `fullScreen` prop to `EntityCatalogGraphCard` +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-catalog-import@0.13.7 + +### Patch Changes + +- f987bfd: Fixed missing `catalog.entity.create` permission authorization in the default page extension. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/integration@1.18.2 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-react@1.21.3 + +### Patch Changes + +- 36d7582: Added missing i18n +- 2b7924b: Apply default ordering of templates +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- 904d136: Fixed catalog filter "all" not appearing as selected when set as the initially selected filter. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + - @backstage/frontend-test-utils@0.4.1 + - @backstage/catalog-client@1.12.1 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.23 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-config-schema@0.1.74 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-devtools@0.1.33 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-devtools-common@0.1.19 + - @backstage/plugin-permission-react@0.4.38 + +## @backstage/plugin-devtools-backend@0.5.11 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-devtools-common@0.1.19 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-devtools-common@0.1.19 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-events-backend@0.5.8 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/backend-openapi-utils@0.6.3 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-azure@0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-events-backend-module-gerrit@0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-events-backend-module-github@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-gitlab@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-kafka@0.1.5 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-test-utils@0.1.50 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.17 + +## @backstage/plugin-events-node@0.4.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## @backstage/plugin-home@0.8.14 + +### Patch Changes + +- 2ac5d29: Allow customization of `VisitList` by adding optional `enrichVisit`, `transformPathname`, `canSave` functions to `VisitsStorageApi`, along with `VisitDisplayProvider` for colors, labels +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-home-react@0.1.32 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-home-react@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-kubernetes@0.12.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/plugin-kubernetes-react@0.5.13 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-kubernetes-backend@0.20.4 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/plugin-kubernetes-node@0.3.6 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-kubernetes-cluster@0.0.31 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/plugin-kubernetes-react@0.5.13 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-kubernetes-common@0.9.8 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-kubernetes-node@0.3.6 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-kubernetes-react@0.5.13 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/core-plugin-api@1.12.0 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-mcp-actions-backend@0.1.5 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-mui-to-bui@0.2.1 + +### Patch Changes + +- 5c614ff: Updated BUI checkbox preview example to align with new component API. +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-notifications@0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-signals-react@0.0.17 + +## @backstage/plugin-notifications-backend-module-email@0.3.16 + +### Patch Changes + +- 22a5362: Updated `AWS SES` client to version 2 to support `nodemailer` version 7. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-notifications-backend-module-slack@0.2.1 + +### Patch Changes + +- d959bec: When an error message is logged due to inability to send a message with the Slack SDK, include the Slack Channel ID in the message to aid debugging. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-notifications-node@0.2.21 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-signals-node@0.1.26 + +## @backstage/plugin-org@0.6.46 + +### Patch Changes + +- 6db9e7e: Improved responsiveness of GroupProfileCard component +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-org-react@0.1.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-permission-backend@0.7.6 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/plugin-permission-node@0.10.6 + +## @backstage/plugin-permission-common@0.9.3 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/config@1.3.6 + +## @backstage/plugin-permission-node@0.10.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/plugin-permission-react@0.4.38 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/config@1.3.6 + +## @backstage/plugin-proxy-backend@0.6.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-proxy-node@0.1.10 + +## @backstage/plugin-proxy-node@0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## @backstage/plugin-scaffolder@1.34.3 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/integration@1.18.2 + - @backstage/plugin-scaffolder-react@1.19.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/catalog-client@1.12.1 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-scaffolder-common@1.7.3 + +## @backstage/plugin-scaffolder-backend@3.0.1 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16 + - @backstage/plugin-bitbucket-cloud-common@0.3.4 + - @backstage/integration@1.18.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.10.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-scaffolder-node@0.12.1 + - @backstage/backend-openapi-utils@0.6.3 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.15 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.15 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2 + - @backstage/plugin-scaffolder-common@1.7.3 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16 + +### Patch Changes + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15 + +### Patch Changes + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- Updated dependencies + - @backstage/plugin-bitbucket-cloud-common@0.3.4 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.15 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.15 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.5 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-common@1.7.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-scaffolder-node@0.12.1 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-scaffolder-common@1.7.3 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.5 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-test-utils@1.10.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-scaffolder-node@0.12.1 + +## @backstage/plugin-scaffolder-react@1.19.3 + +### Patch Changes + +- 886a8a1: Fixed a bug in the Scaffolder's template parsing in the `useTemplateSchema` hook by removing the title instead of setting it to `undefined` +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-scaffolder-common@1.7.3 + +## @backstage/plugin-search-backend@2.0.8 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/backend-openapi-utils@0.6.3 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-catalog@0.3.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.8 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-explore@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-pg@0.5.50 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-techdocs@0.4.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/plugin-techdocs-node@1.13.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-node@1.3.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-common@1.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-signals@0.0.25 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- f0f006e: Fixes a bug where the `SignalClient` would try to subscribe to the same channel twice after an error, instead of just once. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-signals-react@0.0.17 + +## @backstage/plugin-signals-backend@0.3.10 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/plugin-signals-node@0.1.26 + +## @backstage/plugin-signals-node@0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## @backstage/plugin-signals-react@0.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/integration-react@1.2.12 + - @backstage/test-utils@1.7.13 + +## @backstage/plugin-techdocs-backend@2.1.2 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-techdocs-node@1.13.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.30 + +### Patch Changes + +- 6929480: ExpandableCollapse Techdocs Addon was breaking native sidebar collapse on Firefox +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/integration@1.18.2 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/integration-react@1.2.12 + +## @backstage/plugin-techdocs-node@1.13.9 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-techdocs-react@1.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-user-settings@0.8.29 + +### Patch Changes + +- 2b6fda3: Revert `storageApiRef` implementation +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-signals-react@0.0.17 + +## @backstage/plugin-user-settings-backend@0.3.8 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/plugin-signals-node@0.1.26 + +## example-app@0.2.115 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-api-docs@0.13.1 + - @backstage/ui@0.9.0 + - @backstage/plugin-org@0.6.46 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-signals@0.0.25 + - @backstage/frontend-app-api@0.13.2 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-search@1.5.0 + - @backstage/plugin-home@0.8.14 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30 + - @backstage/plugin-scaffolder-react@1.19.3 + - @backstage/plugin-catalog-graph@0.5.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-user-settings@0.8.29 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-catalog-import@0.13.7 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/plugin-scaffolder@1.34.3 + - @backstage/plugin-catalog-unprocessed-entities@0.2.23 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-mui-to-bui@0.2.1 + - @backstage/plugin-devtools@0.1.33 + - @backstage/plugin-kubernetes@0.12.13 + - @backstage/plugin-notifications@0.5.11 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-auth-react@0.1.21 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-kubernetes-cluster@0.0.31 + - @backstage/plugin-search-common@1.2.21 + +## example-app-next@0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-api-docs@0.13.1 + - @backstage/ui@0.9.0 + - @backstage/plugin-org@0.6.46 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-signals@0.0.25 + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-search@1.5.0 + - @backstage/plugin-home@0.8.14 + - @backstage/plugin-app-visualizer@0.1.25 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30 + - @backstage/plugin-scaffolder-react@1.19.3 + - @backstage/plugin-catalog-graph@0.5.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-user-settings@0.8.29 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-auth@0.1.2 + - @backstage/plugin-catalog-import@0.13.7 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/plugin-scaffolder@1.34.3 + - @backstage/plugin-catalog-unprocessed-entities@0.2.23 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/frontend-defaults@0.3.3 + - @backstage/plugin-app@0.3.2 + - @backstage/plugin-kubernetes@0.12.13 + - @backstage/plugin-notifications@0.5.11 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-auth-react@0.1.21 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-kubernetes-cluster@0.0.31 + - @backstage/plugin-search-common@1.2.21 + +## app-next-example-plugin@0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + +## example-backend@0.0.44 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.8 + - @backstage/plugin-kubernetes-backend@0.20.4 + - @backstage/plugin-techdocs-backend@2.1.2 + - @backstage/plugin-signals-backend@0.3.10 + - @backstage/plugin-notifications-backend@0.6.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.16 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-backend@0.7.6 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend@2.0.8 + - @backstage/plugin-auth-backend@0.25.6 + - @backstage/plugin-scaffolder-backend@3.0.1 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/plugin-events-backend@0.5.8 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8 + - @backstage/plugin-devtools-backend@0.5.11 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.6 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-mcp-actions-backend@0.1.5 + - @backstage/plugin-app-backend@0.5.8 + - @backstage/plugin-auth-backend-module-github-provider@0.3.9 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.14 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.6 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-proxy-backend@0.6.8 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16 + - @backstage/plugin-search-backend-module-catalog@0.3.10 + - @backstage/plugin-search-backend-module-explore@0.3.9 + - @backstage/plugin-search-backend-module-techdocs@0.4.8 + +## e2e-test@0.2.34 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.6 + +## @internal/frontend@0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + +## @internal/scaffolder@0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.3 + +## techdocs-cli-embedded-app@0.2.114 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/test-utils@1.7.13 + +## @internal/plugin-todo-list@1.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## @internal/plugin-todo-list-backend@1.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## @internal/plugin-todo-list-common@1.0.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 diff --git a/docs/releases/v1.45.0-next.1-changelog.md b/docs/releases/v1.45.0-next.1-changelog.md new file mode 100644 index 0000000000..098dd21e02 --- /dev/null +++ b/docs/releases/v1.45.0-next.1-changelog.md @@ -0,0 +1,9366 @@ +# Release v1.45.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.45.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.45.0-next.1) + +## @techdocs/cli@1.10.2-next.0 + +# @techdocs/cli + +## 1.10.1-next.0 + +### Patch Changes + +- c2a2017: Fix for missing styles due to move to BUI. +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.0 + - @backstage/plugin-techdocs-node@1.13.9-next.0 + - @backstage/config@1.3.6-next.0 + - @backstage/catalog-model@1.7.6-next.0 + - @backstage/cli-common@0.1.15 + +## 1.10.0 + +### Minor Changes + +- 43afbe5: Techdocs CLI serve supports automatic refresh, relying on `mkdocs` `watch` feature. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.0 + - @backstage/config@1.3.5 + - @backstage/plugin-techdocs-node@1.13.8 + +## 1.9.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.4-next.0 + - @backstage/backend-defaults@0.13.0-next.1 + - @backstage/plugin-techdocs-node@1.13.8-next.1 + +## 1.9.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.0-next.0 + - @backstage/plugin-techdocs-node@1.13.8-next.0 + - @backstage/catalog-model@1.7.5 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.3 + +## 1.9.8 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-techdocs-node@1.13.7 + +## 1.9.8-next.0 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.0 + - @backstage/plugin-techdocs-node@1.13.7-next.0 + +## 1.9.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.0 + - @backstage/plugin-techdocs-node@1.13.6 + +## 1.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.11.2-next.0 + - @backstage/plugin-techdocs-node@1.13.6-next.0 + - @backstage/catalog-model@1.7.5 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.3 + +## 1.9.5 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.3 + - @backstage/catalog-model@1.7.5 + - @backstage/backend-defaults@0.11.1 + - @backstage/plugin-techdocs-node@1.13.5 + +## 1.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.3-next.0 + - @backstage/catalog-model@1.7.5-next.0 + - @backstage/backend-defaults@0.11.1-next.1 + - @backstage/plugin-techdocs-node@1.13.5-next.1 + +## 1.9.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.11.1-next.0 + - @backstage/plugin-techdocs-node@1.13.5-next.0 + - @backstage/catalog-model@1.7.4 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.11.0 + - @backstage/catalog-model@1.7.4 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.4 + +## 1.9.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.11.0-next.2 + - @backstage/catalog-model@1.7.4 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.4-next.1 + +## 1.9.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.1-next.1 + - @backstage/catalog-model@1.7.4 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.4-next.1 + +## 1.9.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.1-next.0 + - @backstage/plugin-techdocs-node@1.13.4-next.0 + +## 1.9.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.0 + - @backstage/catalog-model@1.7.4 + - @backstage/plugin-techdocs-node@1.13.3 + - @backstage/config@1.3.2 + - @backstage/cli-common@0.1.15 + +## 1.9.3-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.0-next.3 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.3-next.3 + +## 1.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.0-next.2 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.3-next.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + +## 1.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.10.0-next.1 + - @backstage/plugin-techdocs-node@1.13.3-next.1 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.9.1-next.0 + - @backstage/plugin-techdocs-node@1.13.3-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.9.0 + - @backstage/plugin-techdocs-node@1.13.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.9.0-next.2 + - @backstage/plugin-techdocs-node@1.13.2-next.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.9.0-next.1 + - @backstage/plugin-techdocs-node@1.13.2-next.1 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.9.0-next.0 + - @backstage/plugin-techdocs-node@1.13.2-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.2 + - @backstage/plugin-techdocs-node@1.13.1 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.2-next.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.1-next.2 + +## 1.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.2-next.1 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.1-next.1 + +## 1.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.2-next.0 + - @backstage/plugin-techdocs-node@1.13.1-next.0 + +## 1.9.0 + +### Minor Changes + +- 8de3d2d: Allow configurable optional retries for publisher AWS S3 operations. + +### Patch Changes + +- 69f84ac: Internal update to work with dynamic imports. +- Updated dependencies + - @backstage/backend-defaults@0.8.0 + - @backstage/plugin-techdocs-node@1.13.0 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.0-next.3 + - @backstage/plugin-techdocs-node@1.13.0-next.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.9.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.0-next.2 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.0-next.1 + +## 1.9.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.8.0-next.1 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.13.0-next.0 + +## 1.9.0-next.0 + +### Minor Changes + +- 8de3d2d: Allow configurable optional retries for publisher AWS S3 operations. + +### Patch Changes + +- 69f84ac: Internal update to work with dynamic imports. +- Updated dependencies + - @backstage/backend-defaults@0.8.0-next.0 + - @backstage/plugin-techdocs-node@1.13.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + +## 1.8.25 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.7.0 + - @backstage/catalog-model@1.7.3 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/plugin-techdocs-node@1.12.16 + +## 1.8.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.7.0-next.1 + - @backstage/catalog-model@1.7.3-next.0 + - @backstage/config@1.3.2-next.0 + - @backstage/plugin-techdocs-node@1.12.16-next.1 + - @backstage/cli-common@0.1.15 + +## 1.8.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.7.0-next.0 + - @backstage/catalog-model@1.7.2 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + - @backstage/plugin-techdocs-node@1.12.16-next.0 + +## 1.8.24 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.6.0 + - @backstage/plugin-techdocs-node@1.12.15 + - @backstage/catalog-model@1.7.2 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + +## 1.8.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.6.0-next.2 + - @backstage/plugin-techdocs-node@1.12.15-next.2 + - @backstage/catalog-model@1.7.2-next.0 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1-next.0 + +## 1.8.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.6.0-next.1 + - @backstage/catalog-model@1.7.1 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.0 + - @backstage/plugin-techdocs-node@1.12.15-next.1 + +## 1.8.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.6.0-next.0 + - @backstage/plugin-techdocs-node@1.12.15-next.0 + - @backstage/catalog-model@1.7.1 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.0 + +## 1.8.22 + +### Patch Changes + +- 702f41d: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/config@1.3.0 + - @backstage/backend-defaults@0.5.3 + - @backstage/cli-common@0.1.15 + - @backstage/catalog-model@1.7.1 + - @backstage/plugin-techdocs-node@1.12.13 + +## 1.8.22-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.3-next.3 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.15-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.13-next.3 + +## 1.8.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.3-next.2 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.15-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.13-next.2 + +## 1.8.22-next.1 + +### Patch Changes + +- 702f41d: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.15-next.0 + - @backstage/backend-defaults@0.5.3-next.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.13-next.1 + +## 1.8.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.3-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.13-next.0 + +## 1.8.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.1 + - @backstage/plugin-techdocs-node@1.12.12 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.1-next.2 + - @backstage/plugin-techdocs-node@1.12.12-next.2 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.1-next.1 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.12-next.1 + +## 1.8.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.1-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.12-next.0 + +## 1.8.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.0 + - @backstage/plugin-techdocs-node@1.12.11 + - @backstage/catalog-model@1.7.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.0-next.2 + - @backstage/plugin-techdocs-node@1.12.11-next.2 + - @backstage/catalog-model@1.6.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.5.0-next.1 + - @backstage/catalog-model@1.6.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.11-next.1 + +## 1.8.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.11-next.0 + - @backstage/backend-defaults@0.5.0-next.0 + - @backstage/catalog-model@1.6.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2 + - @backstage/plugin-techdocs-node@1.12.9 + - @backstage/catalog-model@1.6.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.9-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.9-next.0 + +## 1.8.16 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.8-next.1 + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.15-next.0 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/plugin-techdocs-node@1.12.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.12 + +### Patch Changes + +- 2110d76: Removed `dockerode` dependency. +- Updated dependencies + - @backstage/backend-common@0.23.0 + - @backstage/plugin-techdocs-node@1.12.5 + - @backstage/cli-common@0.1.14 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 1.8.12-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.3 + - @backstage/cli-common@0.1.14-next.0 + - @backstage/backend-common@0.23.0-next.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 1.8.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## 1.8.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.11 + +### Patch Changes + +- 1a0e009: Fix cookie endpoint mock for `serve` +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/backend-common@0.22.0 + - @backstage/plugin-techdocs-node@1.12.4 + +## 1.8.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.0-next.1 + - @backstage/plugin-techdocs-node@1.12.4-next.1 + +## 1.8.10-next.0 + +### Patch Changes + +- 1a0e009: Fix cookie endpoint mock for `serve` +- Updated dependencies + - @backstage/catalog-model@1.5.0-next.0 + - @backstage/backend-common@0.21.8-next.0 + - @backstage/plugin-techdocs-node@1.12.4-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.7 + - @backstage/plugin-techdocs-node@1.12.3 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.7-next.1 + - @backstage/plugin-techdocs-node@1.12.3-next.1 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.3-next.0 + - @backstage/backend-common@0.21.7-next.0 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.8 + +### Patch Changes + +- 8caf2f7: Fix how the cli server mocks the new auth cookie endpoint. +- Updated dependencies + - @backstage/backend-common@0.21.6 + - @backstage/plugin-techdocs-node@1.12.2 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.5 + - @backstage/plugin-techdocs-node@1.12.1 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.6 + +### Patch Changes + +- 1bd4596: Removed the `ts-node` dev dependency. +- Updated dependencies + - @backstage/backend-common@0.21.4 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.0 + - @backstage/catalog-model@1.4.5 + - @backstage/cli-common@0.1.13 + +## 1.8.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.0-next.2 + - @backstage/backend-common@0.21.4-next.2 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0-next.1 + +## 1.8.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0-next.1 + - @backstage/backend-common@0.21.4-next.1 + - @backstage/plugin-techdocs-node@1.11.6-next.1 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/cli-common@0.1.13 + +## 1.8.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + +## 1.8.2 + +### Patch Changes + +- 6bb6f3e: Updated dependency `fs-extra` to `^11.2.0`. + Updated dependency `@types/fs-extra` to `^11.0.0`. +- d2e3ab9: Updated dependency `dockerode` to `^4.0.0`. +- 6ba64c4: Updated dependency `commander` to `^12.0.0`. +- d8d243c: fix: mkdocs parameter casing +- Updated dependencies + - @backstage/backend-common@0.21.0 + - @backstage/catalog-model@1.4.4 + - @backstage/plugin-techdocs-node@1.11.2 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.0-next.3 + - @backstage/plugin-techdocs-node@1.11.2-next.3 + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.0-next.2 + - @backstage/plugin-techdocs-node@1.11.2-next.2 + - @backstage/config@1.1.1 + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/cli-common@0.1.13 + +## 1.8.2-next.1 + +### Patch Changes + +- d8d243c: fix: mkdocs parameter casing +- Updated dependencies + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/backend-common@0.21.0-next.1 + - @backstage/plugin-techdocs-node@1.11.2-next.1 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.0-next.0 + - @backstage/plugin-techdocs-node@1.11.2-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.1 + - @backstage/plugin-techdocs-node@1.11.1 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.1-next.2 + - @backstage/plugin-techdocs-node@1.11.1-next.2 + +## 1.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.1-next.1 + - @backstage/config@1.1.1 + - @backstage/plugin-techdocs-node@1.11.1-next.1 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + +## 1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.1-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/plugin-techdocs-node@1.11.1-next.0 + +## 1.8.0 + +### Minor Changes + +- d15d483: Add command `--runAsDefaultUser` for `@techdocs/cli generate` to bypass running the docker builds as host user for macOS and Linux. +- b2dccad: Support passing additional `mkdocs-server` CLI parameters (`--dirtyreload`, `--strict` and `--clean`) when run in containerized mode. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.0 + - @backstage/plugin-techdocs-node@1.11.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.0-next.3 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/plugin-techdocs-node@1.11.0-next.3 + +## 1.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.0-next.2 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/plugin-techdocs-node@1.11.0-next.2 + +## 1.8.0-next.1 + +### Minor Changes + +- b2dccad7b3: Support passing additional `mkdocs-server` CLI parameters (`--dirtyreload`, `--strict` and `--clean`) when run in containerized mode. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.11.0-next.1 + - @backstage/backend-common@0.20.0-next.1 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.8.0-next.0 + +### Minor Changes + +- d15d483a49: Add command `--runAsDefaultUser` for `@techdocs/cli generate` to bypass running the docker builds as host user for macOS and Linux. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.20.0-next.0 + - @backstage/plugin-techdocs-node@1.11.0-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.7.0 + +### Minor Changes + +- 8600b86820: validate Docker status before running mkdocs server + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.10.0 + - @backstage/backend-common@0.19.9 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.6.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.9-next.2 + - @backstage/plugin-techdocs-node@1.10.0-next.2 + +## 1.6.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.10.0-next.1 + - @backstage/backend-common@0.19.9-next.1 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.9-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/plugin-techdocs-node@1.9.1-next.0 + +## 1.6.0 + +### Minor Changes + +- d06b30b050: Add possibility to use a mkdocs config file with a different name than `mkdocs. with the serve command using the `--mkdocs-config-file-name\` argument + +### Patch Changes + +- de42eebaaf: Bumped dev dependencies `@types/node` and `mock-fs`. +- 2b6e572051: Restructured tests. +- Updated dependencies + - @backstage/backend-common@0.19.8 + - @backstage/plugin-techdocs-node@1.9.0 + - @backstage/catalog-model@1.4.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + +## 1.6.0-next.2 + +### Minor Changes + +- d06b30b050: Add possibility to use a mkdocs config file with a different name than `mkdocs. with the serve command using the `--mkdocs-config-file-name\` argument + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.8-next.2 + - @backstage/plugin-techdocs-node@1.9.0-next.2 + - @backstage/catalog-model@1.4.3-next.0 + - @backstage/cli-common@0.1.13-next.0 + - @backstage/config@1.1.1-next.0 + +## 1.5.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.7-next.1 + - @backstage/plugin-techdocs-node@1.8.2-next.1 + - @backstage/config@1.1.0 + - @backstage/catalog-model@1.4.2 + - @backstage/cli-common@0.1.13-next.0 + +## 1.5.2-next.0 + +### Patch Changes + +- de42eebaaf: Bumped dev dependencies `@types/node` and `mock-fs`. +- 2b6e572051: Restructured tests. +- Updated dependencies + - @backstage/cli-common@0.1.13-next.0 + - @backstage/backend-common@0.19.7-next.0 + - @backstage/plugin-techdocs-node@1.8.2-next.0 + - @backstage/config@1.1.0 + - @backstage/catalog-model@1.4.2 + +## 1.5.0 + +### Minor Changes + +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5 + - @backstage/config@1.1.0 + - @backstage/catalog-model@1.4.2 + - @backstage/plugin-techdocs-node@1.8.0 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.2 + - @backstage/config@1.1.0-next.2 + - @backstage/backend-common@0.19.5-next.3 + - @backstage/cli-common@0.1.12 + - @backstage/plugin-techdocs-node@1.8.0-next.3 + +## 1.5.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.1 + +### Minor Changes + +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/cli-common@0.1.12 + +## 1.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## 1.4.5 + +### Patch Changes + +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + +## 1.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.1 + - @backstage/plugin-techdocs-node@1.7.4-next.1 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.4-next.0 + +## 1.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.1 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.3 + +## 1.4.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.1-next.0 + - @backstage/catalog-model@1.4.1-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.3-next.0 + +## 1.4.3 + +### Patch Changes + +- 68a21956ef52: Remove reference to deprecated import +- Updated dependencies + - @backstage/backend-common@0.19.0 + - @backstage/plugin-techdocs-node@1.7.2 + - @backstage/catalog-model@1.4.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.0-next.2 + - @backstage/plugin-techdocs-node@1.7.2-next.2 + - @backstage/catalog-model@1.4.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.0-next.1 + - @backstage/plugin-techdocs-node@1.7.2-next.1 + - @backstage/catalog-model@1.4.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.6-next.0 + - @backstage/config@1.0.7 + - @backstage/catalog-model@1.3.0 + - @backstage/cli-common@0.1.12 + - @backstage/plugin-techdocs-node@1.7.2-next.0 + +## 1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.5 + - @backstage/plugin-techdocs-node@1.7.1 + - @backstage/catalog-model@1.3.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.5-next.1 + - @backstage/plugin-techdocs-node@1.7.1-next.1 + - @backstage/config@1.0.7 + +## 1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.5-next.0 + - @backstage/plugin-techdocs-node@1.7.1-next.0 + - @backstage/catalog-model@1.3.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.1 + +### Patch Changes + +- b348420a804: Adding global-agent to enable the ability to publish through a proxy +- Updated dependencies + - @backstage/backend-common@0.18.4 + - @backstage/plugin-techdocs-node@1.7.0 + - @backstage/catalog-model@1.3.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.3.0-next.0 + - @backstage/backend-common@0.18.4-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/plugin-techdocs-node@1.6.1-next.3 + +## 1.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.4-next.2 + - @backstage/catalog-model@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/plugin-techdocs-node@1.6.1-next.2 + +## 1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.4-next.1 + - @backstage/catalog-model@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/plugin-techdocs-node@1.6.1-next.1 + +## 1.4.1-next.0 + +### Patch Changes + +- b348420a804: Adding global-agent to enable the ability to publish through a proxy +- Updated dependencies + - @backstage/backend-common@0.18.4-next.0 + - @backstage/config@1.0.7 + - @backstage/catalog-model@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/plugin-techdocs-node@1.6.1-next.0 + +## 1.4.0 + +### Minor Changes + +- 8e465ce52e2: Running `@techdocs/cli generate` with the `--verbose` flag will now print the mkdocs output. +- ea2bbef1b16: Added support for an HTTPS proxy for techdocs AWS S3 requests + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.6.0 + - @backstage/backend-common@0.18.3 + - @backstage/catalog-model@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + +## 1.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.6.0-next.2 + - @backstage/backend-common@0.18.3-next.2 + - @backstage/config@1.0.7-next.0 + +## 1.4.0-next.1 + +### Minor Changes + +- 8e465ce52e2: Running `@techdocs/cli generate` with the `--verbose` flag will now print the mkdocs output. +- ea2bbef1b16: Added support for an HTTPS proxy for techdocs AWS S3 requests + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.6.0-next.1 + - @backstage/backend-common@0.18.3-next.1 + - @backstage/cli-common@0.1.12-next.0 + - @backstage/config@1.0.7-next.0 + - @backstage/catalog-model@1.2.1-next.1 + +## 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 + +- bc18c902a2: Add `--preview-app-bundle-path` and `--preview-app-port` options to the `serve` command enabling previewing with apps other than the provided one + +### Patch Changes + +- 9f2b786fc9: Provide context for logged errors. +- Updated dependencies + - @backstage/backend-common@0.18.0 + - @backstage/catalog-model@1.1.5 + - @backstage/config@1.0.6 + - @backstage/cli-common@0.1.11 + - @backstage/plugin-techdocs-node@1.4.4 + +## 1.3.0-next.2 + +### Minor Changes + +- bc18c902a2: Add `--preview-app-bundle-path` and `--preview-app-port` options to the `serve` command enabling previewing with apps other than the provided one + +### Patch Changes + +- 9f2b786fc9: Provide context for logged errors. +- Updated dependencies + - @backstage/backend-common@0.18.0-next.1 + - @backstage/plugin-techdocs-node@1.4.4-next.2 + - @backstage/catalog-model@1.1.5-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6-next.0 + +## 1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.0-next.0 + - @backstage/config@1.0.6-next.0 + - @backstage/catalog-model@1.1.5-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/plugin-techdocs-node@1.4.4-next.1 + +## 1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.5-next.0 + - @backstage/backend-common@0.17.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.5 + - @backstage/plugin-techdocs-node@1.4.4-next.0 + +## 1.2.4 + +### Patch Changes + +- 8015ff1258: Tweaked wording to use inclusive terminology +- Updated dependencies + - @backstage/backend-common@0.17.0 + - @backstage/plugin-techdocs-node@1.4.3 + - @backstage/cli-common@0.1.11 + - @backstage/catalog-model@1.1.4 + - @backstage/config@1.0.5 + +## 1.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.3 + - @backstage/plugin-techdocs-node@1.4.3-next.3 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/cli-common@0.1.11-next.0 + - @backstage/config@1.0.5-next.1 + +## 1.2.4-next.2 + +### Patch Changes + +- 8015ff1258: Tweaked wording to use inclusive terminology +- Updated dependencies + - @backstage/backend-common@0.17.0-next.2 + - @backstage/cli-common@0.1.11-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.2 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/config@1.0.5-next.1 + +## 1.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-techdocs-node@1.4.3-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/cli-common@0.1.10 + +## 1.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.0 + +## 1.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-techdocs-node@1.4.2 + - @backstage/catalog-model@1.1.3 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + +## 1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-techdocs-node@1.4.2-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + +## 1.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.0 + - @backstage/plugin-techdocs-node@1.4.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + +## 1.2.2 + +### Patch Changes + +- 0b2a30dead: fixing techdocs-cli Docker client creation + + Docker client does not need to be created when --no-docker + option is provided. + + If you had DOCKER_CERT_PATH environment variable defined + the Docker client was looking for certificates + and breaking techdocs-cli generate command even with --no-docker + option. + +- Updated dependencies + - @backstage/catalog-model@1.1.2 + - @backstage/backend-common@0.15.2 + - @backstage/plugin-techdocs-node@1.4.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3 + +## 1.2.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.2-next.2 + - @backstage/plugin-techdocs-node@1.4.1-next.2 + - @backstage/catalog-model@1.1.2-next.2 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.2 + +## 1.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.2-next.1 + - @backstage/catalog-model@1.1.2-next.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.1 + - @backstage/plugin-techdocs-node@1.4.1-next.1 + +## 1.2.2-next.0 + +### Patch Changes + +- 0b2a30dead: fixing techdocs-cli Docker client creation + + Docker client does not need to be created when --no-docker + option is provided. + + If you had DOCKER_CERT_PATH environment variable defined + the Docker client was looking for certificates + and breaking techdocs-cli generate command even with --no-docker + option. + +- Updated dependencies + - @backstage/catalog-model@1.1.2-next.0 + - @backstage/plugin-techdocs-node@1.4.1-next.0 + - @backstage/backend-common@0.15.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.0 + +## 1.2.1 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. +- Updated dependencies + - @backstage/backend-common@0.15.1 + - @backstage/plugin-techdocs-node@1.4.0 + - @backstage/catalog-model@1.1.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.2 + +## 1.2.1-next.2 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. +- Updated dependencies + - @backstage/catalog-model@1.1.1-next.0 + - @backstage/cli-common@0.1.10-next.0 + - @backstage/config@1.0.2-next.0 + - @backstage/backend-common@0.15.1-next.3 + - @backstage/plugin-techdocs-node@1.4.0-next.2 + +## 1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.4.0-next.1 + - @backstage/backend-common@0.15.1-next.2 + +## 1.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.1-next.0 + - @backstage/plugin-techdocs-node@1.3.1-next.0 + +## 1.2.0 + +### Minor Changes + +- 855952db53: Added CLI option `--docker-option` to allow passing additional options to the `docker run` command executed my `serve` and `serve:mkdocs`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.0 + - @backstage/plugin-techdocs-node@1.3.0 + +## 1.2.0-next.2 + +### Minor Changes + +- 855952db53: Added CLI option `--docker-option` to allow passing additional options to the `docker run` command executed my `serve` and `serve:mkdocs`. + +## 1.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.0-next.1 + - @backstage/plugin-techdocs-node@1.3.0-next.1 + +## 1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.15.0-next.0 + - @backstage/plugin-techdocs-node@1.2.1-next.0 + +## 1.1.3 + +### Patch Changes + +- a5d73da942: Fix the flag parsing for `legacyCopyReadmeMdToIndexMd` in `techdocs-cli generate` command, and decouple it's logic from the `techdocs-ref` flag. +- 14ce0d9347: Fixed a bug that prevented docker images from being pulled by default when generating TechDocs. +- Updated dependencies + - @backstage/plugin-techdocs-node@1.2.0 + - @backstage/backend-common@0.14.1 + - @backstage/catalog-model@1.1.0 + +## 1.1.3-next.1 + +### Patch Changes + +- a5d73da942: Fix the flag parsing for `legacyCopyReadmeMdToIndexMd` in `techdocs-cli generate` command, and decouple it's logic from the `techdocs-ref` flag. +- Updated dependencies + - @backstage/plugin-techdocs-node@1.2.0-next.1 + - @backstage/catalog-model@1.1.0-next.1 + - @backstage/backend-common@0.14.1-next.1 + +## 1.1.3-next.0 + +### Patch Changes + +- 14ce0d9347: Fixed a bug that prevented docker images from being pulled by default when generating TechDocs. +- Updated dependencies + - @backstage/backend-common@0.14.1-next.0 + - @backstage/catalog-model@1.1.0-next.0 + - @backstage/plugin-techdocs-node@1.1.3-next.0 + +## 1.1.2 + +### Patch Changes + +- f96e98f4cd: Updated dependency `cypress` to `^10.0.0`. +- bff65e6958: Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()` from `@backstage/core-components`. +- Updated dependencies + - @backstage/backend-common@0.14.0 + - @backstage/plugin-techdocs-node@1.1.2 + - @backstage/catalog-model@1.0.3 + +## 1.1.2-next.2 + +### Patch Changes + +- f96e98f4cd: Updated dependency `cypress` to `^10.0.0`. +- Updated dependencies + - @backstage/backend-common@0.14.0-next.2 + - @backstage/plugin-techdocs-node@1.1.2-next.2 + +## 1.1.2-next.1 + +### Patch Changes + +- bff65e6958: Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()` from `@backstage/core-components`. +- Updated dependencies + - @backstage/backend-common@0.13.6-next.1 + - @backstage/catalog-model@1.0.3-next.0 + - @backstage/plugin-techdocs-node@1.1.2-next.1 + +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.6-next.0 + - @backstage/plugin-techdocs-node@1.1.2-next.0 + +## 1.1.1 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 +- 52fddad92d: The TechDocs CLI's embedded app now imports all API refs from the `@backstage/plugin-techdocs-react` package. +- c14e78a367: Update `techdocs-cli serve`'s `proxyEndpoint` to match the base URL of the embedded techdocs app. +- Updated dependencies + - @backstage/backend-common@0.13.3 + - @backstage/cli-common@0.1.9 + - @backstage/config@1.0.1 + - @backstage/plugin-techdocs-node@1.1.1 + - @backstage/catalog-model@1.0.2 + +## 1.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.2 + - @backstage/cli-common@0.1.9-next.0 + - @backstage/config@1.0.1-next.0 + - @backstage/catalog-model@1.0.2-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.1 + +## 1.1.1-next.1 + +### Patch Changes + +- 52fddad92d: The TechDocs CLI's embedded app now imports all API refs from the `@backstage/plugin-techdocs-react` package. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.1 + +## 1.1.1-next.0 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 733187987b: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + + **WARNING**: If you notice 404s in TechDocs after updating, check to make sure that all markdown files referenced in your `mkdocs.yml`s' `nav` sections exist. The following flag may be passed to the `generate` command to temporarily revert to the broken behavior. + + ```sh + techdocs-cli generate --legacyCopyReadmeMdToIndexMd + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- eb470ea54c: Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-techdocs-node@1.1.0 + +## 1.1.0-next.1 + +### Minor Changes + +- bcf1a2496c: BREAKING: The default Techdocs behavior will no longer attempt to copy `docs/README.md` or `README.md` to `docs/index.md` (if not found). To retain this behavior in your instance, you can set the following config in your `app-config.yaml`: + + ```yaml + techdocs: + generator: + mkdocs: + legacyCopyReadmeMdToIndexMd: true + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- eb470ea54c: Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + - @backstage/plugin-techdocs-node@1.1.0-next.2 + +## 1.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1-next.0 + - @backstage/backend-common@0.13.2-next.0 + - @backstage/plugin-techdocs-node@1.0.1-next.0 + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.1 + - @backstage/catalog-model@1.0.0 + - @backstage/config@1.0.0 + - @backstage/plugin-techdocs-node@1.0.0 + +## 0.8.17 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 91bf1e6c1a: Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0 + - @backstage/plugin-techdocs-node@0.11.12 + - @backstage/catalog-model@0.13.0 + +## 0.8.17-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 +- 91bf1e6c1a: Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. +- Updated dependencies + - @backstage/backend-common@0.13.0-next.0 + - @backstage/plugin-techdocs-node@0.11.12-next.0 + - @backstage/catalog-model@0.13.0-next.0 + +## 0.8.16 + +### Patch Changes + +- 853efd42bd: Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/techdocs-common@0.11.11 + +## 0.8.15 + +### Patch Changes + +- ed78516480: chore(deps-dev): bump `cypress` from 7.3.0 to 9.5.0 +- 209fd128e6: Updated usage of `github:` location types in docs to use `url:` instead. +- 61ff215e08: - Adds `cypress` and `cypress-plugin-snapshots` as dependencies for integration and visual regression tests. + - Updates README documentation with instructions for how to run tests. + - Clarifies output text for prepack script. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/techdocs-common@0.11.10 + +## 0.8.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/catalog-model@0.10.0 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/techdocs-common@0.11.8 + +## 0.8.13 + +### Patch Changes + +- b70c186194: Updated the HTTP server to allow for simplification of the development of the CLI itself. +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/techdocs-common@0.11.7 + +## 0.8.13-next.0 + +### Patch Changes + +- b70c186194: Updated the HTTP server to allow for simplification of the development of the CLI itself. +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/techdocs-common@0.11.7-next.0 + +## 0.8.12 + +### Patch Changes + +- 14472509a3: Use a local file dependency for techdocs-cli-embedded-app, to ensure that it's always pulled out of the workspace +- Updated dependencies + - @backstage/backend-common@0.10.6 + - @backstage/techdocs-common@0.11.6 + +## 0.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/techdocs-common@0.11.6-next.0 + +## 0.8.12-next.0 + +### Patch Changes + +- 14472509a3: Use a local file dependency for techdocs-cli-embedded-app, to ensure that it's always pulled out of the workspace + +## 0.8.11 + +### Patch Changes + +- 10086f5873: Bumped `react-dev-utils` from `^12.0.0-next.47` to `^12.0.0-next.60`. +- Updated dependencies + - @backstage/techdocs-common@0.11.5 + - @backstage/backend-common@0.10.5 + +## 0.8.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.4-next.0 + - @backstage/config@0.1.13-next.0 + - @backstage/techdocs-common@0.11.4-next.0 + - @backstage/catalog-model@0.9.10-next.0 + +## 0.8.10 + +### Patch Changes + +- 8fbc988bfc: remove internal and inline CSS from index.html +- Updated dependencies + - @backstage/techdocs-common@0.11.2 + - @backstage/backend-common@0.10.1 + +## 0.8.9 + +### Patch Changes + +- 5fdc8df0e8: The `index.html` template was updated to use the new `config` global. +- Updated dependencies + - @backstage/backend-common@0.10.0 + - @backstage/techdocs-common@0.11.1 + +## 0.8.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.13 + - @backstage/techdocs-common@0.11.0 + +## 0.8.7 + +### Patch Changes + +- e7230ef814: Bump react-dev-utils to v12 +- Updated dependencies + - @backstage/backend-common@0.9.12 + +## 0.8.6 + +### Patch Changes + +- e21e3c6102: Bumping minimum requirements for `dockerode` and `testcontainers` +- 1578ad341b: Add support for specifying bucketRootPath for AWS and GCS publishers +- f2694e3750: Adds ability to use encrypted S3 buckets by utilizing the SSE option in the AWS SDK +- Updated dependencies + - @backstage/catalog-model@0.9.7 + - @backstage/backend-common@0.9.10 + - @backstage/techdocs-common@0.10.7 + +## 0.8.5 + +### Patch Changes + +- Reunified the [techdocs-cli](https://github.com/backstage/techdocs-cli) monorepo code back into the main [backstage](https://github.com/backstage/backstage) repo + + See [7288](https://github.com/backstage/backstage/issues/7288)). The changes include some internal refactoring that do not affect functionality beyond the local development setup. + +## 0.8.4 + +### Patch Changes + +- 8333394: The [change](https://github.com/backstage/techdocs-cli/commit/b25014cec313d46ce1c9b4f324cc09047a00fc1f) updated the `@backstage/techdocs-common` from version `0.9.0` to `0.10.2` and one of the intermediate versions, the [0.10.0](https://github.com/backstage/backstage/blob/cac4afb95fdbd130a66e53a1b0430a1e62787a7f/packages/techdocs-common/CHANGELOG.md#patch-changes-2), introduced the use of search in context that requires an implementation for the Search API. + + Created a custom techdocs page to disable search in the Reader component, preventing it from using the Search API, as we don't want to provide search in preview mode. + +## 0.8.3 + +### Patch Changes + +- edbb988: Upgrades the techdocs common page to the latest version 0.10.2. + + See [@backstage/techdocs-common changelog](https://github.com/backstage/backstage/blob/cac4afb95fdbd130a66e53a1b0430a1e62787a7f/packages/techdocs-common/CHANGELOG.md#L3). + +- db4ebfc: Add an `etag` flag to the `generate` command that is stored in the `techdocs_metadata.json` file. + +## 0.8.2 + +### Patch Changes + +- 8fc7384: Allow to execute techdocs-cli serve using docker techdocs-container on Windows + +## 0.8.1 + +### Patch Changes + +- 0187424: Separate build and publish release steps + +## 0.8.0 + +### Minor Changes + +- c6f437a: OpenStack Swift configuration changed due to OSS SDK Client change in @backstage/techdocs-common, it was a breaking change. + PR Reference: + +### Patch Changes + +- 05f0409: Merge Jobs for Release Pull Requests and Package Publishes + +## 0.7.0 + +### Minor Changes + +- 9d1f8d8: The `techdocs-cli publish` command will now publish TechDocs content to remote + storage using the lowercase'd entity triplet as the storage path. This is in + line with the beta release of the TechDocs plugin (`v0.11.0`). + + If you have been running `techdocs-cli` prior to this version, you will need to + follow this [migration guide](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta). + +## 0.6.2 + +### Patch Changes + +- f1bcf1a: Changelog (from v0.6.1 to v0.6.2) + + #### :bug: Bug Fix + + - `techdocs-cli` + - [#105](https://github.com/backstage/techdocs-cli/pull/105) Add azureAccountKey parameter back to the publish command ([@emmaindal](https://github.com/emmaindal)) + + #### :house: Internal + + - `techdocs-cli-embedded-app` + - [#122](https://github.com/backstage/techdocs-cli/pull/122) chore(deps-dev): bump @types/node from 12.20.20 to 16.7.1 in /packages/techdocs-cli-embedded-app ([@dependabot\[bot\]](https://github.com/apps/dependabot)) + - [#120](https://github.com/backstage/techdocs-cli/pull/120) chore(deps-dev): bump @types/react-dom from 16.9.14 to 17.0.9 in /packages/techdocs-cli-embedded-app ([@dependabot\[bot\]](https://github.com/apps/dependabot)) + - [#119](https://github.com/backstage/techdocs-cli/pull/119) chore(deps-dev): bump @testing-library/user-event from 12.8.3 to 13.2.1 in /packages/techdocs-cli-embedded-app ([@dependabot\[bot\]](https://github.com/apps/dependabot)) + - [#118](https://github.com/backstage/techdocs-cli/pull/118) chore(deps-dev): bump @testing-library/react from 10.4.9 to 12.0.0 ([@dependabot\[bot\]](https://github.com/apps/dependabot)) + - Other + - [#117](https://github.com/backstage/techdocs-cli/pull/117) chore(deps): bump @backstage/plugin-catalog from 0.6.11 to 0.6.12 ([@dependabot\[bot\]](https://github.com/apps/dependabot)) + - [#124](https://github.com/backstage/techdocs-cli/pull/124) Update release process docs ([@emmaindal](https://github.com/emmaindal)) + - [#116](https://github.com/backstage/techdocs-cli/pull/116) ignore dependabot branches for project board workflow ([@emmaindal](https://github.com/emmaindal)) + - [#106](https://github.com/backstage/techdocs-cli/pull/106) Configure dependabot for all packages ([@emmaindal](https://github.com/emmaindal)) + - [#102](https://github.com/backstage/techdocs-cli/pull/102) readme: add information about running techdocs-common locally ([@vcapretz](https://github.com/vcapretz)) + - [#103](https://github.com/backstage/techdocs-cli/pull/103) Introduce changesets and improve the publish workflow ([@minkimcello](https://github.com/minkimcello)) + - [#101](https://github.com/backstage/techdocs-cli/pull/101) update yarn lockfile to get rid of old version of node-forge ([@emmaindal](https://github.com/emmaindal)) + + #### Committers: 3 + + Thank you for contributing ❤️ + + - `Emma Indal` ([@emmaindal](https://github.com/emmaindal)) + - `Min Kim` ([@minkimcello](https://github.com/minkimcello)) + - `Vitor Capretz` ([@vcapretz](https://github.com/vcapretz)) + +## @backstage/plugin-techdocs@1.15.3-next.0 + +# @backstage/plugin-techdocs + +## 1.15.2-next.0 + +### Patch Changes + +- a4d4a70: Fixed an issue where the entire TechDocs page would re-render when navigating between pages within the same entity's documentation. +- Updated dependencies + - @backstage/plugin-search-react@1.9.6-next.0 + - @backstage/plugin-catalog-react@1.21.3-next.0 + - @backstage/core-plugin-api@1.11.2-next.0 + - @backstage/config@1.3.6-next.0 + - @backstage/core-components@0.18.3-next.0 + - @backstage/catalog-model@1.7.6-next.0 + - @backstage/integration@1.18.2-next.0 + - @backstage/frontend-plugin-api@0.12.2-next.0 + - @backstage/catalog-client@1.12.1-next.0 + - @backstage/core-compat-api@0.5.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.12-next.0 + - @backstage/theme@0.7.0 + - @backstage/plugin-auth-react@0.1.21-next.0 + - @backstage/plugin-search-common@1.2.21-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.5-next.0 + +## 1.15.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.2 + - @backstage/core-components@0.18.2 + - @backstage/integration@1.18.1 + - @backstage/frontend-plugin-api@0.12.1 + - @backstage/plugin-search-react@1.9.5 + - @backstage/config@1.3.5 + - @backstage/theme@0.7.0 + - @backstage/core-compat-api@0.5.3 + - @backstage/plugin-techdocs-react@1.3.4 + - @backstage/core-plugin-api@1.11.1 + - @backstage/integration-react@1.2.11 + - @backstage/plugin-auth-react@0.1.20 + - @backstage/plugin-search-common@1.2.20 + +## 1.15.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + +## 1.15.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.4-next.0 + - @backstage/integration@1.18.1-next.1 + - @backstage/core-components@0.18.2-next.1 + - @backstage/core-plugin-api@1.11.1-next.0 + - @backstage/integration-react@1.2.11-next.1 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/core-compat-api@0.5.3-next.1 + - @backstage/plugin-catalog-react@1.21.2-next.1 + - @backstage/plugin-search-react@1.9.5-next.1 + - @backstage/frontend-plugin-api@0.12.1-next.1 + - @backstage/plugin-auth-react@0.1.20-next.1 + - @backstage/plugin-search-common@1.2.20-next.0 + - @backstage/catalog-client@1.12.0 + +## 1.15.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.2-next.0 + - @backstage/core-components@0.18.2-next.0 + - @backstage/integration@1.18.1-next.0 + - @backstage/core-compat-api@0.5.3-next.0 + - @backstage/frontend-plugin-api@0.12.1-next.0 + - @backstage/integration-react@1.2.11-next.0 + - @backstage/plugin-auth-react@0.1.20-next.0 + - @backstage/plugin-search-react@1.9.5-next.0 + - @backstage/plugin-techdocs-react@1.3.4-next.0 + - @backstage/catalog-client@1.12.0 + - @backstage/catalog-model@1.7.5 + - @backstage/config@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.6.8 + - @backstage/plugin-search-common@1.2.19 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.15.0 + +### Minor Changes + +- a0b604c: Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. + +### Patch Changes + +- 313cec7: Updated dependency `dompurify` to `^3.2.4`. +- 8d18d23: TechDocs page titles have been improved, especially for deeply nested pages. +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- 4ce5831: Support Techdocs redirect with dompurify 3.2.6+ +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## 1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## 1.14.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.20.2-next.1 + - @backstage/frontend-plugin-api@0.11.1-next.0 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-components@0.17.6-next.0 + - @backstage/core-compat-api@0.5.2-next.1 + - @backstage/plugin-search-react@1.9.4-next.0 + - @backstage/plugin-techdocs-react@1.3.3-next.0 + - @backstage/integration-react@1.2.10-next.0 + - @backstage/plugin-auth-react@0.1.19-next.0 + +## 1.14.2-next.0 + +### Patch Changes + +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- Updated dependencies + - @backstage/core-compat-api@0.5.2-next.0 + - @backstage/plugin-catalog-react@1.20.2-next.0 + - @backstage/catalog-client@1.11.0 + - @backstage/catalog-model@1.7.5 + - @backstage/config@1.3.3 + - @backstage/core-components@0.17.5 + - @backstage/core-plugin-api@1.10.9 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.11.0 + - @backstage/integration@1.17.1 + - @backstage/integration-react@1.2.9 + - @backstage/theme@0.6.8 + - @backstage/plugin-auth-react@0.1.18 + - @backstage/plugin-search-common@1.2.19 + - @backstage/plugin-search-react@1.9.3 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.2 + +## 1.14.0 + +### Minor Changes + +- cb0541f: Adds `additionalAllowedURIProtocols` to sanitizer config + +### Patch Changes + +- e4ddf22: Internal update to align with new blueprint parameter naming in the new frontend system. +- f2f133c: Internal update to use the new variant of `ApiBlueprint`. +- f231c2b: Fixes CSS to adjust based on whether or not the global Backstage sidebar is on the page. +- Updated dependencies + - @backstage/core-components@0.17.5 + - @backstage/frontend-plugin-api@0.11.0 + - @backstage/core-compat-api@0.5.0 + - @backstage/plugin-search-react@1.9.3 + - @backstage/plugin-catalog-react@1.20.0 + - @backstage/theme@0.6.8 + - @backstage/catalog-client@1.11.0 + - @backstage/plugin-auth-react@0.1.18 + - @backstage/plugin-techdocs-react@1.3.2 + +## 1.14.0-next.2 + +### Patch Changes + +- e4ddf22: Internal update to align with new blueprint parameter naming in the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.11.0-next.1 + - @backstage/core-compat-api@0.5.0-next.2 + - @backstage/plugin-search-react@1.9.3-next.1 + - @backstage/plugin-catalog-react@1.20.0-next.2 + - @backstage/core-components@0.17.5-next.1 + - @backstage/catalog-client@1.11.0-next.0 + - @backstage/catalog-model@1.7.5 + - @backstage/config@1.3.3 + - @backstage/core-plugin-api@1.10.9 + - @backstage/errors@1.2.7 + - @backstage/integration@1.17.1 + - @backstage/integration-react@1.2.9 + - @backstage/theme@0.6.8-next.0 + - @backstage/plugin-auth-react@0.1.18-next.0 + - @backstage/plugin-search-common@1.2.19 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.2-next.0 + +## 1.14.0-next.1 + +### Minor Changes + +- cb0541f: Adds `additionalAllowedURIProtocols` to sanitizer config + +### Patch Changes + +- f2f133c: Internal update to use the new variant of `ApiBlueprint`. +- Updated dependencies + - @backstage/core-compat-api@0.4.5-next.1 + - @backstage/plugin-catalog-react@1.20.0-next.1 + - @backstage/frontend-plugin-api@0.11.0-next.0 + - @backstage/theme@0.6.8-next.0 + - @backstage/catalog-client@1.11.0-next.0 + - @backstage/plugin-search-react@1.9.3-next.0 + - @backstage/plugin-techdocs-react@1.3.2-next.0 + - @backstage/core-components@0.17.5-next.0 + - @backstage/catalog-model@1.7.5 + - @backstage/config@1.3.3 + - @backstage/core-plugin-api@1.10.9 + - @backstage/errors@1.2.7 + - @backstage/integration@1.17.1 + - @backstage/integration-react@1.2.9 + - @backstage/plugin-auth-react@0.1.18-next.0 + - @backstage/plugin-search-common@1.2.19 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.19.2-next.0 + - @backstage/core-compat-api@0.4.5-next.0 + - @backstage/integration-react@1.2.9 + - @backstage/frontend-plugin-api@0.10.4 + - @backstage/plugin-search-react@1.9.2 + +## 1.13.2 + +### Patch Changes + +- 1debf7f: Fixed an issue causing TechDocs to not properly handle initial redirect. +- Updated dependencies + - @backstage/plugin-catalog-react@1.19.1 + - @backstage/config@1.3.3 + - @backstage/catalog-model@1.7.5 + - @backstage/catalog-client@1.10.2 + - @backstage/core-components@0.17.4 + - @backstage/core-plugin-api@1.10.9 + - @backstage/integration@1.17.1 + - @backstage/theme@0.6.7 + - @backstage/integration-react@1.2.9 + - @backstage/core-compat-api@0.4.4 + - @backstage/frontend-plugin-api@0.10.4 + - @backstage/plugin-auth-react@0.1.17 + - @backstage/plugin-search-common@1.2.19 + - @backstage/plugin-search-react@1.9.2 + - @backstage/plugin-techdocs-react@1.3.1 + +## 1.13.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.3-next.0 + - @backstage/catalog-model@1.7.5-next.0 + - @backstage/plugin-catalog-react@1.19.1-next.1 + - @backstage/catalog-client@1.10.2-next.0 + - @backstage/integration@1.17.1-next.1 + - @backstage/core-components@0.17.4-next.1 + - @backstage/core-plugin-api@1.10.9-next.0 + - @backstage/integration-react@1.2.9-next.1 + - @backstage/plugin-techdocs-react@1.3.1-next.1 + - @backstage/plugin-search-common@1.2.19-next.0 + - @backstage/core-compat-api@0.4.4-next.1 + - @backstage/plugin-search-react@1.9.2-next.1 + - @backstage/frontend-plugin-api@0.10.4-next.1 + - @backstage/plugin-auth-react@0.1.17-next.1 + +## 1.13.2-next.0 + +### Patch Changes + +- 1debf7f: Fixed an issue causing TechDocs to not properly handle initial redirect. +- Updated dependencies + - @backstage/integration@1.17.1-next.0 + - @backstage/integration-react@1.2.9-next.0 + - @backstage/theme@0.6.7-next.0 + - @backstage/plugin-catalog-react@1.19.1-next.0 + - @backstage/core-components@0.17.4-next.0 + - @backstage/plugin-search-react@1.9.2-next.0 + - @backstage/plugin-techdocs-react@1.3.1-next.0 + - @backstage/catalog-client@1.10.1 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.4.4-next.0 + - @backstage/core-plugin-api@1.10.8 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.10.4-next.0 + - @backstage/plugin-auth-react@0.1.17-next.0 + - @backstage/plugin-search-common@1.2.18 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.13.0 + +### Minor Changes + +- 3c59ece: **New Frontend System Only:** + The `TechDocs` plugin is now responsible for providing an entity icon link extension to read documentation from the catalog entity page. +- ec7b35d: Introduced `backstage.io/techdocs-entity-path` annotation which allows deep linking into another entities TechDocs in conjunction with `backstage.io/techdocs-entity`. + +### Patch Changes + +- 18c64e9: Added the `info.packageJson` option to the plugin instance for the new frontend system. +- 9dde3ba: Improved Keyboard accessibility in techdocs. +- Updated dependencies + - @backstage/core-components@0.17.3 + - @backstage/catalog-client@1.10.1 + - @backstage/core-plugin-api@1.10.8 + - @backstage/frontend-plugin-api@0.10.3 + - @backstage/plugin-catalog-react@1.19.0 + - @backstage/plugin-techdocs-react@1.3.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-search-react@1.9.1 + - @backstage/integration-react@1.2.8 + - @backstage/plugin-auth-react@0.1.16 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.4.3 + - @backstage/errors@1.2.7 + - @backstage/integration@1.17.0 + - @backstage/theme@0.6.6 + - @backstage/plugin-search-common@1.2.18 + +## 1.13.0-next.2 + +### Minor Changes + +- 3c59ece: **New Frontend System Only:** + The `TechDocs` plugin is now responsible for providing an entity icon link extension to read documentation from the catalog entity page. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.3-next.0 + - @backstage/plugin-catalog-react@1.19.0-next.2 + - @backstage/plugin-search-react@1.9.1-next.1 + - @backstage/frontend-plugin-api@0.10.3-next.1 + - @backstage/integration-react@1.2.7 + - @backstage/plugin-auth-react@0.1.16-next.0 + - @backstage/plugin-techdocs-react@1.3.0-next.1 + - @backstage/catalog-client@1.10.1-next.0 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.4.3-next.2 + - @backstage/core-plugin-api@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/integration@1.17.0 + - @backstage/theme@0.6.6 + - @backstage/plugin-search-common@1.2.18 + - @backstage/plugin-techdocs-common@0.1.1-next.0 + +## 1.13.0-next.1 + +### Patch Changes + +- 9dde3ba: Improved Keyboard accessibility in techdocs. +- Updated dependencies + - @backstage/catalog-client@1.10.1-next.0 + - @backstage/plugin-catalog-react@1.18.1-next.1 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.4.3-next.1 + - @backstage/core-components@0.17.2 + - @backstage/core-plugin-api@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.10.3-next.0 + - @backstage/integration@1.17.0 + - @backstage/integration-react@1.2.7 + - @backstage/theme@0.6.6 + - @backstage/plugin-auth-react@0.1.15 + - @backstage/plugin-search-common@1.2.18 + - @backstage/plugin-search-react@1.9.1-next.0 + - @backstage/plugin-techdocs-common@0.1.1-next.0 + - @backstage/plugin-techdocs-react@1.3.0-next.0 + +## 1.13.0-next.0 + +### Minor Changes + +- ec7b35d: Introduced `backstage.io/techdocs-entity-path` annotation which allows deep linking into another entities TechDocs in conjunction with `backstage.io/techdocs-entity`. + +### Patch Changes + +- 18c64e9: Added the `info.packageJson` option to the plugin instance for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.10.3-next.0 + - @backstage/plugin-techdocs-react@1.3.0-next.0 + - @backstage/plugin-techdocs-common@0.1.1-next.0 + - @backstage/core-compat-api@0.4.3-next.0 + - @backstage/plugin-catalog-react@1.18.1-next.0 + - @backstage/plugin-search-react@1.9.1-next.0 + - @backstage/integration-react@1.2.7 + +## 1.12.6 + +### Patch Changes + +- fb58f20: Internal update to use the new `pluginId` option of `createFrontendPlugin`. +- 7d445da: Update keyboard focus on when clicking hash links. This fixes the issue where the "skip to content" link rendered by Material MkDocs isn't focused when used. +- 72d019d: Removed various typos +- 2ffd273: Add hover and focus styling to the "copy to clipboard" button within codeblocks in techdocs. Also added an aria-label to the button for accessibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.10.2 + - @backstage/theme@0.6.6 + - @backstage/integration@1.17.0 + - @backstage/core-components@0.17.2 + - @backstage/catalog-model@1.7.4 + - @backstage/core-compat-api@0.4.2 + - @backstage/plugin-search-react@1.9.0 + - @backstage/plugin-catalog-react@1.18.0 + - @backstage/plugin-techdocs-react@1.2.17 + - @backstage/plugin-auth-react@0.1.15 + - @backstage/core-plugin-api@1.10.7 + - @backstage/catalog-client@1.10.0 + - @backstage/config@1.3.2 + - @backstage/integration-react@1.2.7 + - @backstage/errors@1.2.7 + - @backstage/plugin-search-common@1.2.18 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.17.0-next.3 + - @backstage/plugin-search-react@1.9.0-next.2 + - @backstage/core-compat-api@0.4.2-next.3 + - @backstage/core-components@0.17.2-next.1 + - @backstage/core-plugin-api@1.10.7-next.0 + - @backstage/plugin-catalog-react@1.18.0-next.3 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.10.2-next.1 + - @backstage/integration-react@1.2.7-next.3 + - @backstage/theme@0.6.6-next.0 + - @backstage/plugin-auth-react@0.1.15-next.1 + - @backstage/plugin-search-common@1.2.18-next.0 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.17-next.1 + +## 1.12.6-next.2 + +### Patch Changes + +- 7d445da: Update keyboard focus on when clicking hash links. This fixes the issue where the "skip to content" link rendered by Material MkDocs isn't focused when used. +- 2ffd273: Add hover and focus styling to the "copy to clipboard" button within codeblocks in techdocs. Also added an aria-label to the button for accessibility. +- Updated dependencies + - @backstage/integration@1.17.0-next.2 + - @backstage/core-plugin-api@1.10.7-next.0 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.4.2-next.2 + - @backstage/core-components@0.17.2-next.1 + - @backstage/frontend-plugin-api@0.10.2-next.1 + - @backstage/integration-react@1.2.7-next.2 + - @backstage/plugin-auth-react@0.1.15-next.1 + - @backstage/plugin-catalog-react@1.18.0-next.2 + - @backstage/plugin-search-react@1.9.0-next.1 + - @backstage/plugin-techdocs-react@1.2.17-next.1 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/errors@1.2.7 + - @backstage/theme@0.6.6-next.0 + - @backstage/plugin-search-common@1.2.18-next.0 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.6-next.1 + +### Patch Changes + +- fb58f20: Internal update to use the new `pluginId` option of `createFrontendPlugin`. +- 72d019d: Removed various typos +- Updated dependencies + - @backstage/theme@0.6.6-next.0 + - @backstage/core-components@0.17.2-next.0 + - @backstage/frontend-plugin-api@0.10.2-next.0 + - @backstage/core-compat-api@0.4.2-next.1 + - @backstage/integration@1.16.4-next.1 + - @backstage/plugin-search-react@1.9.0-next.0 + - @backstage/plugin-catalog-react@1.18.0-next.1 + - @backstage/plugin-techdocs-react@1.2.17-next.0 + - @backstage/plugin-auth-react@0.1.15-next.0 + - @backstage/integration-react@1.2.7-next.1 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-plugin-api@1.10.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-search-common@1.2.18-next.0 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.16.4-next.0 + - @backstage/core-compat-api@0.4.2-next.0 + - @backstage/plugin-catalog-react@1.18.0-next.0 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/integration-react@1.2.7-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-components@0.17.1 + - @backstage/core-plugin-api@1.10.6 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.10.1 + - @backstage/theme@0.6.5 + - @backstage/plugin-auth-react@0.1.14 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.8 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.16 + +## 1.12.5 + +### Patch Changes + +- a47fd39: Removes instances of default React imports, a necessary update for the upcoming React 19 migration. + + + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.16 + - @backstage/plugin-catalog-react@1.17.0 + - @backstage/frontend-plugin-api@0.10.1 + - @backstage/integration-react@1.2.6 + - @backstage/core-compat-api@0.4.1 + - @backstage/core-components@0.17.1 + - @backstage/core-plugin-api@1.10.6 + - @backstage/plugin-search-react@1.8.8 + - @backstage/plugin-auth-react@0.1.14 + - @backstage/theme@0.6.5 + - @backstage/integration@1.16.3 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.5-next.2 + +### Patch Changes + +- a47fd39: Removes instances of default React imports, a necessary update for the upcoming React 19 migration. + + + +- Updated dependencies + - @backstage/frontend-plugin-api@0.10.1-next.1 + - @backstage/integration-react@1.2.6-next.1 + - @backstage/core-compat-api@0.4.1-next.2 + - @backstage/core-components@0.17.1-next.1 + - @backstage/core-plugin-api@1.10.6-next.0 + - @backstage/plugin-techdocs-react@1.2.16-next.1 + - @backstage/plugin-catalog-react@1.17.0-next.2 + - @backstage/plugin-search-react@1.8.8-next.1 + - @backstage/plugin-auth-react@0.1.14-next.1 + - @backstage/theme@0.6.5-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/integration@1.16.3-next.0 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.16-next.0 + - @backstage/core-components@0.17.1-next.0 + - @backstage/integration-react@1.2.6-next.0 + - @backstage/integration@1.16.3-next.0 + - @backstage/frontend-plugin-api@0.10.1-next.0 + - @backstage/plugin-auth-react@0.1.14-next.0 + - @backstage/plugin-catalog-react@1.16.1-next.1 + - @backstage/plugin-search-react@1.8.8-next.0 + - @backstage/core-compat-api@0.4.1-next.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-plugin-api@1.10.5 + - @backstage/errors@1.2.7 + - @backstage/theme@0.6.4 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.16.1-next.0 + - @backstage/core-compat-api@0.4.1-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-components@0.17.0 + - @backstage/core-plugin-api@1.10.5 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.10.0 + - @backstage/integration@1.16.2 + - @backstage/integration-react@1.2.5 + - @backstage/theme@0.6.4 + - @backstage/plugin-auth-react@0.1.13 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.7 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.15 + +## 1.12.4 + +### Patch Changes + +- fffe3c0: Fixed double scrollbar issue that would appear on the Entity TechDocs view page that would stop the page from full scrolling to the top when navigating to a new page +- 065e6b9: Excludes SVG styling from sanitization +- b5a8208: Added `TechDocsAddonsBlueprint` extension to allow adding of techdocs addons. +- ed1cb3e: Adds the page name of techdocs to the document's title. +- fe4abb8: Updates logic to check for SVG sources when inlining them. +- Updated dependencies + - @backstage/core-components@0.17.0 + - @backstage/core-plugin-api@1.10.5 + - @backstage/integration@1.16.2 + - @backstage/plugin-search-react@1.8.7 + - @backstage/frontend-plugin-api@0.10.0 + - @backstage/plugin-catalog-react@1.16.0 + - @backstage/core-compat-api@0.4.0 + - @backstage/plugin-techdocs-react@1.2.15 + - @backstage/integration-react@1.2.5 + - @backstage/plugin-auth-react@0.1.13 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/theme@0.6.4 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.10.0-next.2 + - @backstage/plugin-catalog-react@1.16.0-next.2 + - @backstage/core-compat-api@0.4.0-next.2 + - @backstage/core-components@0.16.5-next.1 + - @backstage/integration@1.16.2-next.0 + - @backstage/plugin-search-react@1.8.7-next.2 + - @backstage/plugin-techdocs-react@1.2.15-next.2 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-plugin-api@1.10.4 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.5-next.0 + - @backstage/theme@0.6.4 + - @backstage/plugin-auth-react@0.1.13-next.1 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.4-next.1 + +### Patch Changes + +- fffe3c0: Fixed double scrollbar issue that would appear on the Entity TechDocs view page that would stop the page from full scrolling to the top when navigating to a new page +- Updated dependencies + - @backstage/core-components@0.16.5-next.0 + - @backstage/plugin-catalog-react@1.16.0-next.1 + - @backstage/core-compat-api@0.3.7-next.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-plugin-api@1.10.4 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.9.6-next.1 + - @backstage/integration@1.16.1 + - @backstage/integration-react@1.2.4 + - @backstage/theme@0.6.4 + - @backstage/plugin-auth-react@0.1.13-next.0 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.7-next.1 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.15-next.1 + +## 1.12.4-next.0 + +### Patch Changes + +- 065e6b9: Excludes svg styling from sanitization +- b5a8208: Added `TechDocsAddonsBlueprint` extension to allow adding of techdocs addons. +- ed1cb3e: Adds the page name of techdoc to the document's title. +- fe4abb8: Updates logic to check for SVG sources when inlining svgs. +- Updated dependencies + - @backstage/plugin-search-react@1.8.7-next.0 + - @backstage/plugin-catalog-react@1.16.0-next.0 + - @backstage/plugin-techdocs-react@1.2.15-next.0 + - @backstage/frontend-plugin-api@0.9.6-next.0 + - @backstage/core-compat-api@0.3.7-next.0 + - @backstage/integration-react@1.2.4 + +## 1.12.3 + +### Patch Changes + +- eb3d91a: Use the custom error page if provided for displaying errors instead of the default error page + +- 524f0af: Add missing route ref to the `/alpha` entity content extension. + +- f4be934: Changed the base URL in addLinkClickListener from window.location.origin to app.baseUrl for improved path handling. This fixes an issue where Backstage, when running on a subpath, was unable to handle non-Backstage URLs of the same origin correctly. + +- 1f40e6b: Add optional props to `TechDocCustomHome` to allow for more flexibility: + + ```tsx + import { TechDocsCustomHome } from '@backstage/plugin-techdocs'; + //... + + const options = { emptyRowsWhenPaging: false }; + const linkDestination = (entity: Entity): string | undefined => { + return entity.metadata.annotations?.['external-docs']; + }; + const techDocsTabsConfig = [ + { + label: 'Recommended Documentation', + panels: [ + { + title: 'Golden Path', + description: 'Documentation about standards to follow', + panelType: 'DocsCardGrid', + panelProps: { CustomHeader: () => }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('golden-path') ?? false, + }, + { + title: 'Recommended', + description: 'Useful documentation', + panelType: 'InfoCardGrid', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ], + }, + { + label: 'Browse All', + panels: [ + { + description: 'Browse all docs', + filterPredicate: filterEntity, + panelType: 'TechDocsIndexPage', + title: 'All', + panelProps: { PageWrapper: React.Fragment, CustomHeader: React.Fragment, options: options }, + }, + ], + }, + ]; + + const AppRoutes = () => { + + ) => ({children})} + /> + } + /> + ; + }; + ``` + + Add new Grid option called `InfoCardGrid` which is a more customizable card option for the Docs grid. + + ```tsx + entity.metadata['external-docs']} + /> + ``` + + Expose existing `CustomDocsPanel` so that it can be used independently if desired. + + ```tsx + const panels: PanelConfig[] = [ + { + description: '', + filterPredicate: entity => {}, + panelType: 'InfoCardGrid', + title: 'Standards', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + }, + { + description: '', + filterPredicate: entity => {}, + panelType: 'DocsCardGrid', + title: 'Contribute', + }, + ]; + { + panels.map((config, index) => ( + + )); + } + ``` + +- 58ec9e7: Removed older versions of React packages as a preparatory step for upgrading to React 19. This commit does not introduce any functional changes, but removes dependencies on previous React versions, allowing for a cleaner upgrade path in subsequent commits. + +- Updated dependencies + - @backstage/plugin-search-react@1.8.6 + - @backstage/core-components@0.16.4 + - @backstage/plugin-catalog-react@1.15.2 + - @backstage/frontend-plugin-api@0.9.5 + - @backstage/integration-react@1.2.4 + - @backstage/core-compat-api@0.3.6 + - @backstage/core-plugin-api@1.10.4 + - @backstage/plugin-techdocs-react@1.2.14 + - @backstage/plugin-auth-react@0.1.12 + - @backstage/theme@0.6.4 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/integration@1.16.1 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.3-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.9.5-next.3 + - @backstage/core-compat-api@0.3.6-next.3 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-components@0.16.4-next.1 + - @backstage/core-plugin-api@1.10.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration@1.16.1 + - @backstage/integration-react@1.2.4-next.0 + - @backstage/theme@0.6.4-next.0 + - @backstage/plugin-auth-react@0.1.12-next.1 + - @backstage/plugin-catalog-react@1.15.2-next.3 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.6-next.3 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.14-next.1 + +## 1.12.3-next.2 + +### Patch Changes + +- eb3d91a: Use the custom error page if provided for displaying errors instead of the default error page +- Updated dependencies + - @backstage/core-components@0.16.4-next.1 + - @backstage/plugin-catalog-react@1.15.2-next.2 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.3.6-next.2 + - @backstage/core-plugin-api@1.10.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.9.5-next.2 + - @backstage/integration@1.16.1 + - @backstage/integration-react@1.2.4-next.0 + - @backstage/theme@0.6.4-next.0 + - @backstage/plugin-auth-react@0.1.12-next.1 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.6-next.2 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.14-next.1 + +## 1.12.2-next.1 + +### Patch Changes + +- 524f0af: Add missing route ref to the `/alpha` entity content extension. +- 58ec9e7: Removed older versions of React packages as a preparatory step for upgrading to React 19. This commit does not introduce any functional changes, but removes dependencies on previous React versions, allowing for a cleaner upgrade path in subsequent commits. +- Updated dependencies + - @backstage/core-components@0.16.4-next.0 + - @backstage/frontend-plugin-api@0.9.5-next.1 + - @backstage/integration-react@1.2.4-next.0 + - @backstage/core-compat-api@0.3.6-next.1 + - @backstage/core-plugin-api@1.10.4-next.0 + - @backstage/plugin-techdocs-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.15.2-next.1 + - @backstage/plugin-search-react@1.8.6-next.1 + - @backstage/plugin-auth-react@0.1.12-next.0 + - @backstage/theme@0.6.4-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/integration@1.16.1 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.2-next.0 + +### Patch Changes + +- f4be934: Changed the base URL in addLinkClickListener from window.location.origin to app.baseUrl for improved path handling. This fixes an issue where Backstage, when running on a subpath, was unable to handle non-Backstage URLs of the same origin correctly. + +- 1f40e6b: Add optional props to `TechDocCustomHome` to allow for more flexibility: + + ```tsx + import { TechDocsCustomHome } from '@backstage/plugin-techdocs'; + //... + + const options = { emptyRowsWhenPaging: false }; + const linkDestination = (entity: Entity): string | undefined => { + return entity.metadata.annotations?.['external-docs']; + }; + const techDocsTabsConfig = [ + { + label: 'Recommended Documentation', + panels: [ + { + title: 'Golden Path', + description: 'Documentation about standards to follow', + panelType: 'DocsCardGrid', + panelProps: { CustomHeader: () => }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('golden-path') ?? false, + }, + { + title: 'Recommended', + description: 'Useful documentation', + panelType: 'InfoCardGrid', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ], + }, + { + label: 'Browse All', + panels: [ + { + description: 'Browse all docs', + filterPredicate: filterEntity, + panelType: 'TechDocsIndexPage', + title: 'All', + panelProps: { PageWrapper: React.Fragment, CustomHeader: React.Fragment, options: options }, + }, + ], + }, + ]; + + const AppRoutes = () => { + + ) => ({children})} + /> + } + /> + ; + }; + ``` + + Add new Grid option called `InfoCardGrid` which is a more customizable card option for the Docs grid. + + ```tsx + entity.metadata['external-docs']} + /> + ``` + + Expose existing `CustomDocsPanel` so that it can be used independently if desired. + + ```tsx + const panels: PanelConfig[] = [ + { + description: '', + filterPredicate: entity => {}, + panelType: 'InfoCardGrid', + title: 'Standards', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + }, + { + description: '', + filterPredicate: entity => {}, + panelType: 'DocsCardGrid', + title: 'Contribute', + }, + ]; + { + panels.map((config, index) => ( + + )); + } + ``` + +- Updated dependencies + - @backstage/plugin-search-react@1.8.6-next.0 + - @backstage/frontend-plugin-api@0.9.5-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.3.6-next.0 + - @backstage/core-components@0.16.3 + - @backstage/core-plugin-api@1.10.3 + - @backstage/errors@1.2.7 + - @backstage/integration@1.16.1 + - @backstage/integration-react@1.2.3 + - @backstage/theme@0.6.3 + - @backstage/plugin-auth-react@0.1.11 + - @backstage/plugin-catalog-react@1.15.2-next.0 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.13 + +## 1.12.1 + +### Patch Changes + +- 3710b35: Allow passing down `withSearch` prop to `EntityTechdocsContent` component since it was `true` by default, now user can use the `EntityTechdocsContent` component _without_ showing the search field on top of the content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.15.1 + - @backstage/frontend-plugin-api@0.9.4 + - @backstage/core-plugin-api@1.10.3 + - @backstage/core-components@0.16.3 + - @backstage/integration@1.16.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/core-compat-api@0.3.5 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.3 + - @backstage/theme@0.6.3 + - @backstage/plugin-auth-react@0.1.11 + - @backstage/plugin-search-common@1.2.17 + - @backstage/plugin-search-react@1.8.5 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.13 + +## 1.12.1-next.1 + +### Patch Changes + +- 3710b35: Allow passing down `withSearch` prop to `EntityTechdocsContent` component since it was `true` by default, now user can use the `EntityTechdocsContent` component _without_ showing the search field on top of the content. +- Updated dependencies + - @backstage/frontend-plugin-api@0.9.4-next.0 + - @backstage/core-plugin-api@1.10.3-next.0 + - @backstage/core-compat-api@0.3.5-next.0 + - @backstage/plugin-catalog-react@1.15.1-next.1 + - @backstage/plugin-search-react@1.8.5-next.0 + - @backstage/core-components@0.16.3-next.0 + - @backstage/integration-react@1.2.3-next.0 + - @backstage/plugin-auth-react@0.1.11-next.0 + - @backstage/plugin-techdocs-react@1.2.13-next.0 + - @backstage/catalog-model@1.7.3-next.0 + - @backstage/config@1.3.2-next.0 + - @backstage/errors@1.2.7-next.0 + - @backstage/plugin-search-common@1.2.17-next.0 + - @backstage/integration@1.16.1-next.0 + - @backstage/theme@0.6.3 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.15.1-next.0 + - @backstage/integration-react@1.2.2 + - @backstage/core-compat-api@0.3.4 + +## 1.12.0 + +### Minor Changes + +- e153ca6: Add pagination support to TechDocs Index Page and make it the default + +### Patch Changes + +- 7d8777d: Added support for the Search bar in docs residing in the entity page tab, and not only the global "/docs" page. +- Updated dependencies + - @backstage/plugin-catalog-react@1.15.0 + - @backstage/integration@1.16.0 + - @backstage/plugin-search-react@1.8.4 + - @backstage/core-compat-api@0.3.4 + - @backstage/frontend-plugin-api@0.9.3 + - @backstage/theme@0.6.3 + - @backstage/core-components@0.16.2 + - @backstage/errors@1.2.6 + - @backstage/catalog-model@1.7.2 + - @backstage/config@1.3.1 + - @backstage/core-plugin-api@1.10.2 + - @backstage/integration-react@1.2.2 + - @backstage/plugin-auth-react@0.1.10 + - @backstage/plugin-search-common@1.2.16 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.12 + +## 1.11.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.3.4-next.2 + - @backstage/plugin-catalog-react@1.14.3-next.2 + - @backstage/errors@1.2.6-next.0 + - @backstage/catalog-model@1.7.2-next.0 + - @backstage/config@1.3.1-next.0 + - @backstage/core-components@0.16.2-next.2 + - @backstage/core-plugin-api@1.10.2-next.0 + - @backstage/frontend-plugin-api@0.9.3-next.2 + - @backstage/integration@1.16.0-next.1 + - @backstage/integration-react@1.2.2-next.1 + - @backstage/theme@0.6.3-next.0 + - @backstage/plugin-auth-react@0.1.10-next.2 + - @backstage/plugin-search-common@1.2.16-next.0 + - @backstage/plugin-search-react@1.8.4-next.2 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.12-next.2 + +## 1.11.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.14.3-next.1 + - @backstage/core-components@0.16.2-next.1 + - @backstage/catalog-model@1.7.1 + - @backstage/config@1.3.0 + - @backstage/core-compat-api@0.3.4-next.1 + - @backstage/core-plugin-api@1.10.1 + - @backstage/errors@1.2.5 + - @backstage/frontend-plugin-api@0.9.3-next.1 + - @backstage/integration@1.16.0-next.0 + - @backstage/integration-react@1.2.2-next.0 + - @backstage/theme@0.6.3-next.0 + - @backstage/plugin-auth-react@0.1.10-next.1 + - @backstage/plugin-search-common@1.2.15 + - @backstage/plugin-search-react@1.8.4-next.1 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.12-next.1 + +## 1.11.3-next.0 + +### Patch Changes + +- 7d8777d: Added support for the Search bar in docs residing in the entity page tab, and not only the global "/docs" page. +- Updated dependencies + - @backstage/integration@1.16.0-next.0 + - @backstage/plugin-search-react@1.8.4-next.0 + - @backstage/plugin-catalog-react@1.14.3-next.0 + - @backstage/frontend-plugin-api@0.9.3-next.0 + - @backstage/theme@0.6.3-next.0 + - @backstage/catalog-model@1.7.1 + - @backstage/config@1.3.0 + - @backstage/core-compat-api@0.3.4-next.0 + - @backstage/core-components@0.16.2-next.0 + - @backstage/core-plugin-api@1.10.1 + - @backstage/errors@1.2.5 + - @backstage/integration-react@1.2.2-next.0 + - @backstage/plugin-auth-react@0.1.10-next.0 + - @backstage/plugin-search-common@1.2.15 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.12-next.0 + +## 1.11.1 + +### Patch Changes + +- 37a7810: Fixed an issue where `` would re-render infinitely under certain conditions. +- e937ae7: Fix an issue with index page of documentation site being re-rendered. +- 90246a9: Fix techdocs config schema for custom elements sanitizer +- 605bdc0: Avoid page re-rendering when clicking on anchor links in the same documentation page. +- 4f0cb89: Added DomPurify sanitizer configuration for custom elements implementing RFC . + See for how to enable it in the configuration. +- f246178: Removed `canvas` dev dependency. +- 4a2f73a: Fix an issue that caused the current documentation page to be re-rendered when navigating to + another one. +- Updated dependencies + - @backstage/config@1.3.0 + - @backstage/theme@0.6.1 + - @backstage/plugin-catalog-react@1.14.1 + - @backstage/core-components@0.16.0 + - @backstage/plugin-techdocs-react@1.2.10 + - @backstage/catalog-model@1.7.1 + - @backstage/core-compat-api@0.3.2 + - @backstage/core-plugin-api@1.10.1 + - @backstage/errors@1.2.5 + - @backstage/frontend-plugin-api@0.9.1 + - @backstage/integration@1.15.2 + - @backstage/integration-react@1.2.1 + - @backstage/plugin-auth-react@0.1.8 + - @backstage/plugin-search-common@1.2.15 + - @backstage/plugin-search-react@1.8.2 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.11.1-next.3 + +### Patch Changes + +- e937ae7: Fix an issue with index page of documentation site being re-rendered. +- Updated dependencies + - @backstage/core-components@0.16.0-next.2 + - @backstage/plugin-catalog-react@1.14.1-next.3 + - @backstage/core-compat-api@0.3.2-next.2 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.10.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.9.1-next.2 + - @backstage/integration@1.15.1 + - @backstage/integration-react@1.2.0 + - @backstage/theme@0.6.1-next.0 + - @backstage/plugin-auth-react@0.1.8-next.2 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.2-next.2 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.10-next.2 + +## 1.11.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.14.1-next.2 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.3.2-next.1 + - @backstage/core-components@0.16.0-next.1 + - @backstage/core-plugin-api@1.10.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.9.1-next.1 + - @backstage/integration@1.15.1 + - @backstage/integration-react@1.2.0 + - @backstage/theme@0.6.1-next.0 + - @backstage/plugin-auth-react@0.1.8-next.1 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.2-next.1 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.10-next.1 + +## 1.11.1-next.1 + +### Patch Changes + +- 90246a9: Fix techdocs config schema for custom elements sanitizer +- Updated dependencies + - @backstage/theme@0.6.1-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.3.2-next.1 + - @backstage/core-components@0.16.0-next.1 + - @backstage/core-plugin-api@1.10.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.9.1-next.1 + - @backstage/integration@1.15.1 + - @backstage/integration-react@1.2.0 + - @backstage/plugin-auth-react@0.1.8-next.1 + - @backstage/plugin-catalog-react@1.14.1-next.1 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.2-next.1 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.10-next.1 + +## 1.11.1-next.0 + +### Patch Changes + +- 605bdc0: Avoid page re-rendering when clicking on anchor links in the same documentation page. +- 4f0cb89: Added DomPurify sanitizer configuration for custom elements implementing RFC . + See for how to enable it in the configuration. +- f246178: Removed `canvas` dev dependency. +- 4a2f73a: Fix an issue that caused the current documentation page to be re-rendered when navigating to + another one. +- Updated dependencies + - @backstage/core-components@0.16.0-next.0 + - @backstage/plugin-techdocs-react@1.2.10-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.3.2-next.0 + - @backstage/core-plugin-api@1.10.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.9.1-next.0 + - @backstage/integration@1.15.1 + - @backstage/integration-react@1.2.0 + - @backstage/theme@0.6.0 + - @backstage/plugin-auth-react@0.1.8-next.0 + - @backstage/plugin-catalog-react@1.14.1-next.0 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.2-next.0 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.11.0 + +### Minor Changes + +- e77ff3d: Adds support for custom background colors in code blocks and inline code within TechDocs. + +### Patch Changes + +- e969dc7: Move `@types/react` to a peer dependency. +- a77cb40: Make `emptyState` input optional on `entity-content:techdocs` extension so that + the default empty state extension works correctly. +- e918061: Add support for mkdocs material palette conditional hashes. +- 720a2f9: Updated dependency `git-url-parse` to `^15.0.0`. +- e8b4966: Use more of the available space for the navigation sidebar. +- Updated dependencies + - @backstage/core-components@0.15.1 + - @backstage/frontend-plugin-api@0.9.0 + - @backstage/integration-react@1.2.0 + - @backstage/core-compat-api@0.3.1 + - @backstage/core-plugin-api@1.10.0 + - @backstage/plugin-techdocs-react@1.2.9 + - @backstage/plugin-catalog-react@1.14.0 + - @backstage/plugin-search-react@1.8.1 + - @backstage/plugin-auth-react@0.1.7 + - @backstage/theme@0.6.0 + - @backstage/integration@1.15.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.11.0-next.2 + +### Minor Changes + +- e77ff3d: Adds support for custom background colors in code blocks and inline code within TechDocs. + +### Patch Changes + +- e918061: Add support for mkdocs material palette conditional hashes. +- 720a2f9: Updated dependency `git-url-parse` to `^15.0.0`. +- e8b4966: Use more of the available space for the navigation sidebar. +- Updated dependencies + - @backstage/plugin-catalog-react@1.14.0-next.2 + - @backstage/integration@1.15.1-next.1 + - @backstage/theme@0.6.0-next.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.3.1-next.2 + - @backstage/core-components@0.15.1-next.2 + - @backstage/core-plugin-api@1.10.0-next.1 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.9.0-next.2 + - @backstage/integration-react@1.2.0-next.2 + - @backstage/plugin-auth-react@0.1.7-next.2 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.1-next.2 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.9-next.2 + +## 1.10.11-next.1 + +### Patch Changes + +- e969dc7: Move `@types/react` to a peer dependency. +- Updated dependencies + - @backstage/core-components@0.15.1-next.1 + - @backstage/frontend-plugin-api@0.9.0-next.1 + - @backstage/integration-react@1.2.0-next.1 + - @backstage/core-compat-api@0.3.1-next.1 + - @backstage/core-plugin-api@1.10.0-next.1 + - @backstage/plugin-techdocs-react@1.2.9-next.1 + - @backstage/plugin-catalog-react@1.14.0-next.1 + - @backstage/plugin-search-react@1.8.1-next.1 + - @backstage/plugin-auth-react@0.1.7-next.1 + - @backstage/theme@0.5.8-next.0 + - @backstage/integration@1.15.1-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.10.10-next.0 + +### Patch Changes + +- a77cb40: Make `emptyState` input optional on `entity-content:techdocs` extension so that + the default empty state extension works correctly. +- Updated dependencies + - @backstage/frontend-plugin-api@0.9.0-next.0 + - @backstage/core-compat-api@0.3.1-next.0 + - @backstage/core-components@0.15.1-next.0 + - @backstage/core-plugin-api@1.10.0-next.0 + - @backstage/plugin-catalog-react@1.13.1-next.0 + - @backstage/plugin-search-react@1.8.1-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.15.0 + - @backstage/integration-react@1.1.32-next.0 + - @backstage/theme@0.5.7 + - @backstage/plugin-auth-react@0.1.7-next.0 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.9-next.0 + +## 1.10.9 + +### Patch Changes + +- c891b69: Add `FavoriteToggle` in `core-components` to standardise favorite marking + +- fec8b57: Updated exports to use the new type parameters for extensions and extension blueprints. + +- fe94ad8: Fixes left navigation positioning when using mkdocs blog plugin + +- b0206dc: Added support for setting page status with 'new' and 'deprecated' values, allowing visual indication of page status in TechDocs. To use include the following at the top of your markdown file: + + ```markdown + --- + status: new + --- + ``` + +- 836127c: Updated dependency `@testing-library/react` to `^16.0.0`. + +- c7cb4c0: Add `empty-state:techdocs/entity-content` extension to allow overriding the empty state for the entity page techdocs tab. + +- 97db53e: Enhanced the table hover effect with a lighter color and updated the border radius to align with Backstage's theme styling + +- Updated dependencies + - @backstage/core-components@0.15.0 + - @backstage/plugin-catalog-react@1.13.0 + - @backstage/frontend-plugin-api@0.8.0 + - @backstage/plugin-techdocs-react@1.2.8 + - @backstage/core-compat-api@0.3.0 + - @backstage/plugin-search-react@1.8.0 + - @backstage/integration-react@1.1.31 + - @backstage/catalog-model@1.7.0 + - @backstage/integration@1.15.0 + - @backstage/core-plugin-api@1.9.4 + - @backstage/theme@0.5.7 + - @backstage/plugin-auth-react@0.1.6 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.10.9-next.2 + +### Patch Changes + +- c891b69: Add `FavoriteToggle` in `core-components` to standardise favorite marking +- 836127c: Updated dependency `@testing-library/react` to `^16.0.0`. +- 97db53e: Enhanced the table hover effect with a lighter color and updated the border radius to align with Backstage's theme styling +- Updated dependencies + - @backstage/core-components@0.14.11-next.1 + - @backstage/plugin-catalog-react@1.13.0-next.2 + - @backstage/integration-react@1.1.31-next.0 + - @backstage/plugin-search-react@1.8.0-next.2 + - @backstage/integration@1.15.0-next.0 + - @backstage/core-compat-api@0.3.0-next.2 + - @backstage/core-plugin-api@1.9.4-next.0 + - @backstage/frontend-plugin-api@0.8.0-next.2 + - @backstage/theme@0.5.7-next.0 + - @backstage/plugin-auth-react@0.1.6-next.1 + - @backstage/plugin-techdocs-react@1.2.8-next.2 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.10.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.8.0-next.1 + - @backstage/core-compat-api@0.3.0-next.1 + - @backstage/core-components@0.14.11-next.0 + - @backstage/plugin-catalog-react@1.12.4-next.1 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0 + - @backstage/integration-react@1.1.30 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-search-react@1.8.0-next.1 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-techdocs-react@1.2.8-next.1 + +## 1.10.9-next.0 + +### Patch Changes + +- fec8b57: Updated exports to use the new type parameters for extensions and extension blueprints. +- Updated dependencies + - @backstage/frontend-plugin-api@0.8.0-next.0 + - @backstage/plugin-techdocs-react@1.2.8-next.0 + - @backstage/core-compat-api@0.2.9-next.0 + - @backstage/plugin-catalog-react@1.12.4-next.0 + - @backstage/plugin-search-react@1.8.0-next.0 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0 + - @backstage/integration-react@1.1.30 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5 + - @backstage/plugin-search-common@1.2.14 + - @backstage/plugin-techdocs-common@0.1.0 + +## 1.10.8 + +### Patch Changes + +- 69bd940: Use annotation constants from new techdocs-common package. +- c7603e8: Deprecate the old pattern of `create*Extension`, and replace it with the equivalent Blueprint implementation instead +- 27794d1: Allow for more granular control of TechDocsReaderPage styling. Theme overrides can now be provided to TechDocs without affecting the theme in other areas of Backstage. +- 4490d73: Refactor TechDocs' mkdocs-redirects support. +- 8543e72: TechDocs redirect feature now includes a notification to the user before they are redirected. +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0 + - @backstage/plugin-catalog-react@1.12.3 + - @backstage/plugin-search-react@1.7.14 + - @backstage/core-components@0.14.10 + - @backstage/core-compat-api@0.2.8 + - @backstage/plugin-search-common@1.2.14 + - @backstage/integration@1.14.0 + - @backstage/plugin-techdocs-common@0.1.0 + - @backstage/plugin-auth-react@0.1.5 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7 + +## 1.10.8-next.3 + +### Patch Changes + +- 27794d1: Allow for more granular control of TechDocsReaderPage styling. Theme overrides can now be provided to TechDocs without affecting the theme in other areas of Backstage. +- 8543e72: TechDocs redirect feature now includes a notification to the user before they are redirected. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 1.10.8-next.1 + +### Patch Changes + +- 69bd940: Use annotation constants from new techdocs-common package. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 1.10.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 1.10.7 + +### Patch Changes + +- 8fc2622: Fixed an issue that was causing techdocs pages unnecessarily re-render on navigate. +- 6fa652c: Improve default sorting of docs table +- 605b691: Allow for searching TechDocs by entity title +- 60caa92: Fix double scrollbar bug in reader +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + +## 1.10.7-next.2 + +### Patch Changes + +- 6fa652c: Improve default sorting of docs table +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## 1.10.7-next.1 + +### Patch Changes + +- 60caa92: Fix double scrollbar bug in reader +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## 1.10.7-next.0 + +### Patch Changes + +- 8ac9ce5: Fixed a bug with the TechDocsReaderPageProvider not re-rendering when setShadowDom is called, meaning that the useShadowDom hooks were inconsistent. This issue caused the TextSize addon changes not to reapply during navigation. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/plugin-search-common@1.2.12 + +## 1.10.6 + +### Patch Changes + +- 654af4a: mkdocs-material have updated their CSS variable template, and a few are unset in Backstage. This patch adds the missing variables to ensure coverage. +- cbebad1: Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. +- 96cd13e: `TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- e40bd9a: Fixed bug in `CopyToClipboardButton` component where positioning of the "Copy to clipboard" button in techdocs code snippets was broken in some cases. +- d44a20a: Added additional plugin metadata to `package.json`. +- 1256d88: Fixed an issue preventing the `TechDocsSearchBar` component from opening when clicking on the arrow icon. +- Updated dependencies + - @backstage/core-components@0.14.8 + - @backstage/core-compat-api@0.2.6 + - @backstage/integration@1.12.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.5 + - @backstage/plugin-catalog-react@1.12.1 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-search-react@1.7.12 + - @backstage/plugin-auth-react@0.1.3 + - @backstage/integration-react@1.1.28 + - @backstage/frontend-plugin-api@0.6.6 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 1.10.6-next.2 + +### Patch Changes + +- d44a20a: Added additional plugin metadata to `package.json`. +- Updated dependencies + - @backstage/core-components@0.14.8-next.2 + - @backstage/integration@1.12.0-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.2 + - @backstage/plugin-catalog-react@1.12.1-next.2 + - @backstage/plugin-search-common@1.2.12-next.0 + - @backstage/plugin-search-react@1.7.12-next.2 + - @backstage/plugin-auth-react@0.1.3-next.2 + - @backstage/integration-react@1.1.28-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.2 + - @backstage/core-compat-api@0.2.6-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + +## 1.10.6-next.1 + +### Patch Changes + +- cbebad1: Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.6-next.0 + +### Patch Changes + +- 654af4a: mkdocs-material have updated their CSS variable template, and a few are unset in Backstage. This patch adds the missing variables to ensure coverage. +- 96cd13e: `TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- e40bd9a: Fixed bug in CopyToClipboardButton component where positioning of the "Copy to clipboard" button in techdocs code snippets was broken in some cases +- 1256d88: Fix weird opening behaviour of the component. +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.5 + +### Patch Changes + +- d2cc139: Update path in Readme for Plugin Techdocs to show the correct setup information. +- 5863cf7: The `techdocs.builder` config is now optional and it will default to `local`. +- Updated dependencies + - @backstage/core-compat-api@0.2.5 + - @backstage/core-components@0.14.7 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-react@0.1.2 + - @backstage/plugin-catalog-react@1.12.0 + - @backstage/theme@0.5.4 + - @backstage/integration@1.11.0 + - @backstage/frontend-plugin-api@0.6.5 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-search-react@1.7.11 + - @backstage/plugin-techdocs-react@1.2.4 + +## 1.10.5-next.2 + +### Patch Changes + +- 5863cf7: The `techdocs.builder` config is now optional and it will default to `local`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.0-next.2 + - @backstage/core-components@0.14.7-next.2 + - @backstage/integration@1.11.0-next.0 + - @backstage/core-compat-api@0.2.5-next.1 + - @backstage/frontend-plugin-api@0.6.5-next.1 + - @backstage/plugin-search-react@1.7.11-next.1 + - @backstage/integration-react@1.1.27-next.0 + +## 1.10.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.6-next.1 + - @backstage/plugin-catalog-react@1.11.4-next.1 + - @backstage/frontend-plugin-api@0.6.5-next.1 + - @backstage/integration-react@1.1.26 + - @backstage/plugin-auth-react@0.1.2-next.1 + - @backstage/plugin-search-react@1.7.11-next.1 + - @backstage/plugin-techdocs-react@1.2.4-next.1 + - @backstage/core-compat-api@0.2.5-next.1 + +## 1.10.5-next.0 + +### Patch Changes + +- d2cc139: Update path in Readme for Plugin Techdocs to show the correct setup information. +- Updated dependencies + - @backstage/core-compat-api@0.2.5-next.0 + - @backstage/catalog-model@1.5.0-next.0 + - @backstage/plugin-auth-react@0.1.1-next.0 + - @backstage/theme@0.5.4-next.0 + - @backstage/core-components@0.14.5-next.0 + - @backstage/plugin-catalog-react@1.11.4-next.0 + - @backstage/plugin-techdocs-react@1.2.4-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.5-next.0 + - @backstage/integration@1.10.0 + - @backstage/integration-react@1.1.26 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-search-react@1.7.11-next.0 + +## 1.10.4 + +### Patch Changes + +- abfbcfc: Updated dependency `@testing-library/react` to `^15.0.0`. +- cb1e3b0: Updated dependency `@testing-library/dom` to `^10.0.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.3 + - @backstage/plugin-search-react@1.7.10 + - @backstage/plugin-auth-react@0.1.0 + - @backstage/plugin-catalog-react@1.11.3 + - @backstage/core-compat-api@0.2.4 + - @backstage/core-components@0.14.4 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.4 + - @backstage/theme@0.5.3 + - @backstage/integration-react@1.1.26 + - @backstage/integration@1.10.0 + - @backstage/catalog-model@1.4.5 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.0-next.1 + - @backstage/frontend-plugin-api@0.6.4-next.1 + - @backstage/core-compat-api@0.2.4-next.1 + - @backstage/catalog-model@1.4.5 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.4-next.0 + - @backstage/core-plugin-api@1.9.1 + - @backstage/errors@1.2.4 + - @backstage/integration@1.10.0-next.0 + - @backstage/integration-react@1.1.26-next.0 + - @backstage/theme@0.5.2 + - @backstage/plugin-catalog-react@1.11.3-next.1 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-search-react@1.7.10-next.1 + - @backstage/plugin-techdocs-react@1.2.3-next.0 + +## 1.10.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.10.0-next.0 + - @backstage/core-components@0.14.4-next.0 + - @backstage/catalog-model@1.4.5 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.4-next.0 + - @backstage/core-plugin-api@1.9.1 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.4-next.0 + - @backstage/integration-react@1.1.26-next.0 + - @backstage/theme@0.5.2 + - @backstage/plugin-auth-react@0.0.4-next.0 + - @backstage/plugin-catalog-react@1.11.3-next.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-search-react@1.7.10-next.0 + - @backstage/plugin-techdocs-react@1.2.3-next.0 + +## 1.10.3 + +### Patch Changes + +- e8f026a: Use ESM exports of react-use library +- Updated dependencies + - @backstage/core-components@0.14.3 + - @backstage/plugin-techdocs-react@1.2.2 + - @backstage/plugin-catalog-react@1.11.2 + - @backstage/plugin-search-react@1.7.9 + - @backstage/frontend-plugin-api@0.6.3 + - @backstage/integration-react@1.1.25 + - @backstage/plugin-auth-react@0.0.3 + - @backstage/core-compat-api@0.2.3 + - @backstage/core-plugin-api@1.9.1 + - @backstage/catalog-model@1.4.5 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.9.1 + - @backstage/theme@0.5.2 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.2 + +### Patch Changes + +- e8f026a: Use ESM exports of react-use library +- Updated dependencies + - @backstage/core-components@0.14.2 + - @backstage/plugin-techdocs-react@1.2.1 + - @backstage/plugin-catalog-react@1.11.1 + - @backstage/plugin-search-react@1.7.8 + - @backstage/frontend-plugin-api@0.6.2 + - @backstage/integration-react@1.1.25 + - @backstage/plugin-auth-react@0.0.2 + - @backstage/core-compat-api@0.2.2 + - @backstage/core-plugin-api@1.9.1 + - @backstage/catalog-model@1.4.5 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.9.1 + - @backstage/theme@0.5.2 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.1 + +### Patch Changes + +- 7c2d022: Fixed bug in TechDocs sidebar render that prevented scrollbar from being displayed +- 3f14e9f: Implement a client cookie refresh mechanism. +- 62bcaf8: Use the new generic refresh user cookie provider. +- 28f27f0: Added ESLint rule `no-top-level-material-ui-4-imports` to aid with the migration to Material UI v5. +- Updated dependencies + - @backstage/integration@1.9.1 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-react@0.0.1 + - @backstage/theme@0.5.2 + - @backstage/integration-react@1.1.25 + - @backstage/plugin-techdocs-react@1.2.0 + - @backstage/plugin-catalog-react@1.11.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/catalog-model@1.4.5 + - @backstage/core-compat-api@0.2.1 + - @backstage/core-plugin-api@1.9.1 + - @backstage/frontend-plugin-api@0.6.1 + - @backstage/plugin-search-react@1.7.7 + +## 1.10.1-next.2 + +### Patch Changes + +- 7c2d022: Fixed bug in TechDocs sidebar render that prevented scrollbar from being displayed +- 3f14e9f: Implement a client cookie refresh mechanism. +- Updated dependencies + - @backstage/integration@1.9.1-next.2 + - @backstage/plugin-techdocs-react@1.2.0-next.2 + - @backstage/core-components@0.14.1-next.2 + - @backstage/plugin-catalog-react@1.11.0-next.2 + - @backstage/integration-react@1.1.25-next.2 + - @backstage/frontend-plugin-api@0.6.1-next.2 + - @backstage/plugin-search-react@1.7.7-next.2 + - @backstage/core-compat-api@0.2.1-next.2 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.2.0-next.1 + - @backstage/core-plugin-api@1.9.1-next.1 + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/plugin-search-common@1.2.11-next.1 + +## 1.10.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0-next.1 + - @backstage/core-components@0.14.1-next.1 + - @backstage/plugin-catalog-react@1.10.1-next.1 + - @backstage/core-plugin-api@1.9.1-next.1 + - @backstage/integration@1.9.1-next.1 + - @backstage/integration-react@1.1.25-next.1 + - @backstage/plugin-techdocs-react@1.1.17-next.1 + - @backstage/frontend-plugin-api@0.6.1-next.1 + - @backstage/plugin-search-react@1.7.7-next.1 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-compat-api@0.2.1-next.1 + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/plugin-search-common@1.2.11-next.1 + +## 1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## 1.10.0 + +### Minor Changes + +- af4d147: Updated the styling for `` tags to avoid word break. + +### Patch Changes + +- 912ca7b: Use `convertLegacyRouteRefs` to define routes in `/alpha` export plugin. +- 8fe56a8: Widen `@types/react` dependency range to include version 18. +- 3631fb4: Updated dependency `dompurify` to `^3.0.0`. + Updated dependency `@types/dompurify` to `^3.0.0`. +- 1cae748: Updated dependency `git-url-parse` to `^14.0.0`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.0 + - @backstage/core-compat-api@0.2.0 + - @backstage/plugin-catalog-react@1.10.0 + - @backstage/core-components@0.14.0 + - @backstage/plugin-techdocs-react@1.1.16 + - @backstage/catalog-model@1.4.4 + - @backstage/theme@0.5.1 + - @backstage/integration@1.9.0 + - @backstage/core-plugin-api@1.9.0 + - @backstage/plugin-search-react@1.7.6 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/integration-react@1.1.24 + - @backstage/plugin-search-common@1.2.10 + +## 1.10.0-next.3 + +### Patch Changes + +- 3631fb4: Updated dependency `dompurify` to `^3.0.0`. + Updated dependency `@types/dompurify` to `^3.0.0`. +- 1cae748: Updated dependency `git-url-parse` to `^14.0.0`. +- Updated dependencies + - @backstage/theme@0.5.1-next.1 + - @backstage/integration@1.9.0-next.1 + - @backstage/core-components@0.14.0-next.2 + - @backstage/plugin-catalog-react@1.10.0-next.3 + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/config@1.1.1 + - @backstage/core-compat-api@0.2.0-next.3 + - @backstage/core-plugin-api@1.9.0-next.1 + - @backstage/errors@1.2.3 + - @backstage/frontend-plugin-api@0.6.0-next.3 + - @backstage/integration-react@1.1.24-next.2 + - @backstage/plugin-search-common@1.2.10 + - @backstage/plugin-search-react@1.7.6-next.3 + - @backstage/plugin-techdocs-react@1.1.16-next.2 + +## 1.10.0-next.2 + +### Patch Changes + +- 8fe56a8: Widen `@types/react` dependency range to include version 18. +- Updated dependencies + - @backstage/core-components@0.14.0-next.1 + - @backstage/plugin-techdocs-react@1.1.16-next.1 + - @backstage/core-plugin-api@1.9.0-next.1 + - @backstage/frontend-plugin-api@0.6.0-next.2 + - @backstage/plugin-catalog-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.6-next.2 + - @backstage/theme@0.5.1-next.0 + - @backstage/integration-react@1.1.24-next.1 + - @backstage/core-compat-api@0.2.0-next.2 + - @backstage/config@1.1.1 + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/errors@1.2.3 + - @backstage/integration@1.9.0-next.0 + - @backstage/plugin-search-common@1.2.10 + +## 1.10.0-next.1 + +### Minor Changes + +- af4d147: Updated the styling for `` tags to avoid word break. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.0-next.1 + - @backstage/core-compat-api@0.2.0-next.1 + - @backstage/core-components@0.14.0-next.0 + - @backstage/catalog-model@1.4.4-next.0 + - @backstage/core-plugin-api@1.8.3-next.0 + - @backstage/integration@1.9.0-next.0 + - @backstage/plugin-catalog-react@1.9.4-next.1 + - @backstage/plugin-search-react@1.7.6-next.1 + - @backstage/integration-react@1.1.24-next.0 + - @backstage/plugin-techdocs-react@1.1.16-next.0 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-common@1.2.10 + +## 1.9.4-next.0 + +### Patch Changes + +- 912ca7b: Use `convertLegacyRouteRefs` to define routes in `/alpha` export plugin. +- Updated dependencies + - @backstage/core-compat-api@0.1.2-next.0 + - @backstage/plugin-catalog-react@1.9.4-next.0 + - @backstage/frontend-plugin-api@0.5.1-next.0 + - @backstage/core-components@0.13.10 + - @backstage/plugin-search-react@1.7.6-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/core-plugin-api@1.8.2 + - @backstage/errors@1.2.3 + - @backstage/integration@1.8.0 + - @backstage/integration-react@1.1.23 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-common@1.2.10 + - @backstage/plugin-techdocs-react@1.1.15 + +## 1.9.3 + +### Patch Changes + +- 4016f21: Remove some unused dependencies +- Updated dependencies + - @backstage/core-compat-api@0.1.1 + - @backstage/frontend-plugin-api@0.5.0 + - @backstage/core-components@0.13.10 + - @backstage/core-plugin-api@1.8.2 + - @backstage/plugin-techdocs-react@1.1.15 + - @backstage/plugin-catalog-react@1.9.3 + - @backstage/plugin-search-react@1.7.5 + - @backstage/integration-react@1.1.23 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/integration@1.8.0 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-common@1.2.10 + +## 1.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.1.1-next.2 + - @backstage/frontend-plugin-api@0.4.1-next.2 + - @backstage/plugin-catalog-react@1.9.3-next.2 + - @backstage/plugin-search-react@1.7.5-next.2 + - @backstage/integration-react@1.1.23-next.0 + +## 1.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.8.2-next.0 + - @backstage/core-components@0.13.10-next.1 + - @backstage/core-compat-api@0.1.1-next.1 + - @backstage/frontend-plugin-api@0.4.1-next.1 + - @backstage/integration-react@1.1.23-next.0 + - @backstage/plugin-catalog-react@1.9.3-next.1 + - @backstage/plugin-search-react@1.7.5-next.1 + - @backstage/plugin-techdocs-react@1.1.15-next.1 + - @backstage/integration@1.8.0 + - @backstage/config@1.1.1 + - @backstage/catalog-model@1.4.3 + - @backstage/errors@1.2.3 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-common@1.2.9 + +## 1.9.3-next.0 + +### Patch Changes + +- 4016f21: Remove some unused dependencies +- Updated dependencies + - @backstage/core-components@0.13.10-next.0 + - @backstage/frontend-plugin-api@0.4.1-next.0 + - @backstage/plugin-techdocs-react@1.1.15-next.0 + - @backstage/plugin-catalog-react@1.9.3-next.0 + - @backstage/integration-react@1.1.22 + - @backstage/plugin-search-react@1.7.5-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/core-compat-api@0.1.1-next.0 + - @backstage/core-plugin-api@1.8.1 + - @backstage/errors@1.2.3 + - @backstage/integration@1.8.0 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-common@1.2.9 + +## 1.9.2 + +### Patch Changes + +- 03d0b6d: The `convertLegacyRouteRef` utility used by the alpha exports is now imported from `@backstage/core-compat-api`. +- a1227cc: Wrap `/alpha` export extension elements in backwards compatibility wrapper. +- 5814122: Updated `/alpha` exports to fit new naming patterns. +- 36c94b8: Refactor of the alpha exports due to API change in how extension IDs are constructed. +- Updated dependencies + - @backstage/core-compat-api@0.1.0 + - @backstage/core-plugin-api@1.8.1 + - @backstage/frontend-plugin-api@0.4.0 + - @backstage/plugin-catalog-react@1.9.2 + - @backstage/core-components@0.13.9 + - @backstage/theme@0.5.0 + - @backstage/plugin-search-react@1.7.4 + - @backstage/integration@1.8.0 + - @backstage/integration-react@1.1.22 + - @backstage/plugin-techdocs-react@1.1.14 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/plugin-search-common@1.2.9 + +## 1.9.2-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.9-next.3 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/core-compat-api@0.1.0-next.3 + - @backstage/core-plugin-api@1.8.1-next.1 + - @backstage/errors@1.2.3 + - @backstage/frontend-plugin-api@0.4.0-next.3 + - @backstage/integration@1.8.0-next.1 + - @backstage/integration-react@1.1.22-next.1 + - @backstage/theme@0.5.0-next.1 + - @backstage/plugin-catalog-react@1.9.2-next.3 + - @backstage/plugin-search-common@1.2.8 + - @backstage/plugin-search-react@1.7.4-next.3 + - @backstage/plugin-techdocs-react@1.1.14-next.3 + +## 1.9.2-next.3 + +### Patch Changes + +- a1227cc: Wrap `/alpha` export extension elements in backwards compatibility wrapper. +- 36c94b8: Refactor of the alpha exports due to API change in how extension IDs are constructed. +- Updated dependencies + - @backstage/frontend-plugin-api@0.4.0-next.2 + - @backstage/theme@0.5.0-next.1 + - @backstage/core-compat-api@0.1.0-next.2 + - @backstage/plugin-catalog-react@1.9.2-next.2 + - @backstage/plugin-search-react@1.7.4-next.2 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/core-components@0.13.9-next.2 + - @backstage/core-plugin-api@1.8.1-next.1 + - @backstage/errors@1.2.3 + - @backstage/integration@1.8.0-next.1 + - @backstage/integration-react@1.1.22-next.1 + - @backstage/plugin-search-common@1.2.8 + - @backstage/plugin-techdocs-react@1.1.14-next.2 + +## 1.9.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.4.0-next.1 + - @backstage/core-components@0.13.9-next.1 + - @backstage/core-plugin-api@1.8.1-next.1 + - @backstage/plugin-catalog-react@1.9.2-next.1 + - @backstage/plugin-search-react@1.7.4-next.1 + - @backstage/integration@1.8.0-next.1 + - @backstage/core-compat-api@0.0.1-next.1 + - @backstage/integration-react@1.1.22-next.1 + - @backstage/plugin-techdocs-react@1.1.14-next.1 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/theme@0.5.0-next.0 + - @backstage/plugin-search-common@1.2.8 + +## 1.9.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.0.1-next.0 + +## 1.9.2-next.0 + +### Patch Changes + +- 03d0b6dcdc: The `convertLegacyRouteRef` utility used by the alpha exports is now imported from `@backstage/core-compat-api`. +- Updated dependencies + - @backstage/core-compat-api@0.0.2-next.0 + - @backstage/core-plugin-api@1.8.1-next.0 + - @backstage/plugin-catalog-react@1.9.2-next.0 + - @backstage/core-components@0.13.9-next.0 + - @backstage/plugin-search-react@1.7.4-next.0 + - @backstage/integration@1.8.0-next.0 + - @backstage/theme@0.5.0-next.0 + - @backstage/frontend-plugin-api@0.3.1-next.0 + - @backstage/integration-react@1.1.22-next.0 + - @backstage/plugin-techdocs-react@1.1.14-next.0 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/plugin-search-common@1.2.8 + +## 1.9.0 + +### Minor Changes + +- 17f93d5589: A new analytics event `not-found` will be published when a user visits a documentation site that does not exist + +### Patch Changes + +- 4728b3960d: Fixed navigation bug that caused users to not be scrolled to the top of a new page. Fixed navigation bug where using backwards and forwards browser navigation did not scroll users to the correct place on the TechDoc page. +- a3add7a682: Export alpha routes and nav item extension, only available for applications that uses the new Frontend system. +- 71c97e7d73: The \`spec.lifecycle' field in entities will now always be rendered as a string. +- 68fc9dc60e: Updated alpha exports according to routing changes in `@backstage/frontend-plugin-api`. +- 6c2b872153: Add official support for React 18. +- 0bf6ebda88: Added entity page content for the new plugin exported via `/alpha`. +- 67cc85bb14: Switched the conditional `react-dom/client` import to use `import(...)` rather than `require(...)`. +- 4aa43f62aa: Updated dependency `cross-fetch` to `^4.0.0`. +- 38cda52746: Added support for React 18. The new `createRoot` API from `react-dom/client` will now be used if present. +- fdb5e23602: Import `MissingAnnotationEmptyState` from `@backstage/plugin-catalog-react` to remove the cyclical dependency +- Updated dependencies + - @backstage/plugin-catalog-react@1.9.0 + - @backstage/core-components@0.13.8 + - @backstage/frontend-plugin-api@0.3.0 + - @backstage/integration@1.7.2 + - @backstage/integration-react@1.1.21 + - @backstage/core-plugin-api@1.8.0 + - @backstage/plugin-techdocs-react@1.1.13 + - @backstage/plugin-search-react@1.7.2 + - @backstage/theme@0.4.4 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/plugin-search-common@1.2.8 + +## 1.9.0-next.2 + +### Minor Changes + +- [#20851](https://github.com/backstage/backstage/pull/20851) [`17f93d5589`](https://github.com/backstage/backstage/commit/17f93d5589812df3dea53d956212e184b080fbac) Thanks [@agentbellnorm](https://github.com/agentbellnorm)! - A new analytics event `not-found` will be published when a user visits a documentation site that does not exist + +### Patch Changes + +- [#20842](https://github.com/backstage/backstage/pull/20842) [`fdb5e23602`](https://github.com/backstage/backstage/commit/fdb5e2360299c5faa30f4d4236fc548b94d37446) Thanks [@benjdlambert](https://github.com/benjdlambert)! - Import `MissingAnnotationEmptyState` from `@backstage/plugin-catalog-react` to remove the cyclical dependency + +- Updated dependencies + - @backstage/core-components@0.13.8-next.2 + - @backstage/frontend-plugin-api@0.3.0-next.2 + - @backstage/plugin-catalog-react@1.9.0-next.2 + - @backstage/integration-react@1.1.21-next.1 + - @backstage/plugin-search-react@1.7.2-next.2 + - @backstage/plugin-techdocs-react@1.1.13-next.2 + +## 1.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.2-next.0 + - @backstage/frontend-plugin-api@0.3.0-next.1 + - @backstage/plugin-catalog-react@1.9.0-next.1 + - @backstage/plugin-search-react@1.7.2-next.1 + - @backstage/integration-react@1.1.21-next.1 + - @backstage/core-components@0.13.8-next.1 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/core-plugin-api@1.8.0-next.0 + - @backstage/errors@1.2.3 + - @backstage/theme@0.4.4-next.0 + - @backstage/plugin-search-common@1.2.7 + - @backstage/plugin-techdocs-react@1.1.13-next.1 + +## 1.8.1-next.0 + +### Patch Changes + +- 4728b3960d: Fixed navigation bug that caused users to not be scrolled to the top of a new page. Fixed navigation bug where using backwards and forwards browser navigation did not scroll users to the correct place on the TechDoc page. +- a3add7a682: Export alpha routes and nav item extension, only available for applications that uses the new Frontend system. +- 71c97e7d73: The \`spec.lifecycle' field in entities will now always be rendered as a string. +- 68fc9dc60e: Updated alpha exports according to routing changes in `@backstage/frontend-plugin-api`. +- 6c2b872153: Add official support for React 18. +- 0bf6ebda88: Added entity page content for the new plugin exported via `/alpha`. +- 67cc85bb14: Switched the conditional `react-dom/client` import to use `import(...)` rather than `require(...)`. +- 38cda52746: Added support for React 18. The new `createRoot` API from `react-dom/client` will now be used if present. +- Updated dependencies + - @backstage/core-components@0.13.7-next.0 + - @backstage/frontend-plugin-api@0.3.0-next.0 + - @backstage/plugin-catalog-react@1.9.0-next.0 + - @backstage/integration-react@1.1.21-next.0 + - @backstage/core-plugin-api@1.8.0-next.0 + - @backstage/plugin-techdocs-react@1.1.13-next.0 + - @backstage/plugin-search-react@1.7.2-next.0 + - @backstage/theme@0.4.4-next.0 + - @backstage/integration@1.7.1 + - @backstage/catalog-model@1.4.3 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/plugin-search-common@1.2.7 + +## 1.8.0 + +### Minor Changes + +- 27740caa2d: Added experimental support for declarative integration via the `/alpha` subpath. + +### Patch Changes + +- 4918f65ab2: Create an experimental `TechDocsSearchResultItemExtension` for declarative integration with Backstage; it can be accessed via the `/alpha` import. +- 3605370af6: Improved `DocsTable` to display pagination controls dynamically, appearing only when needed. +- 0296f272b4: The \`spec.lifecycle' field in entities will now always be rendered as a string. +- 9a1fce352e: Updated dependency `@testing-library/jest-dom` to `^6.0.0`. +- f95af4e540: Updated dependency `@testing-library/dom` to `^9.0.0`. +- 9468a67b92: Added support for React 18. The new `createRoot` API from `react-dom/client` will now be used if present. +- df449a7a31: Add kind column by default to TechDocsTable +- Updated dependencies + - @backstage/integration@1.7.1 + - @backstage/plugin-catalog-react@1.8.5 + - @backstage/frontend-plugin-api@0.2.0 + - @backstage/core-plugin-api@1.7.0 + - @backstage/core-components@0.13.6 + - @backstage/integration-react@1.1.20 + - @backstage/catalog-model@1.4.3 + - @backstage/errors@1.2.3 + - @backstage/plugin-search-react@1.7.1 + - @backstage/plugin-techdocs-react@1.1.12 + - @backstage/theme@0.4.3 + - @backstage/config@1.1.1 + - @backstage/plugin-search-common@1.2.7 + +## 1.7.1-next.2 + +### Patch Changes + +- 3605370af6: Improved `DocsTable` to display pagination controls dynamically, appearing only when needed. +- Updated dependencies + - @backstage/frontend-plugin-api@0.2.0-next.2 + - @backstage/integration-react@1.1.20-next.2 + - @backstage/core-components@0.13.6-next.2 + - @backstage/core-plugin-api@1.7.0-next.1 + - @backstage/catalog-model@1.4.3-next.0 + - @backstage/plugin-catalog-react@1.8.5-next.2 + - @backstage/integration@1.7.1-next.1 + - @backstage/errors@1.2.3-next.0 + - @backstage/plugin-search-react@1.7.1-next.2 + - @backstage/theme@0.4.3-next.0 + - @backstage/config@1.1.1-next.0 + - @backstage/plugin-search-common@1.2.7-next.0 + - @backstage/plugin-techdocs-react@1.1.12-next.2 + +## 1.7.1-next.1 + +### Patch Changes + +- 4918f65ab2: Create an experimental `TechDocsSearchResultItemExtension` for declarative integration with Backstage; it can be accessed via the `/alpha` import. +- df449a7a31: Add kind column by default to TechDocsTable +- Updated dependencies + - @backstage/frontend-plugin-api@0.1.1-next.1 + - @backstage/core-components@0.13.6-next.1 + - @backstage/plugin-search-react@1.7.1-next.1 + - @backstage/integration-react@1.1.20-next.1 + - @backstage/plugin-catalog-react@1.8.5-next.1 + - @backstage/plugin-techdocs-react@1.1.12-next.1 + - @backstage/core-plugin-api@1.7.0-next.0 + - @backstage/config@1.1.0 + - @backstage/catalog-model@1.4.2 + - @backstage/errors@1.2.2 + - @backstage/integration@1.7.1-next.0 + - @backstage/theme@0.4.2 + - @backstage/plugin-search-common@1.2.6 + +## 1.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.1-next.0 + - @backstage/plugin-catalog-react@1.8.5-next.0 + - @backstage/core-plugin-api@1.7.0-next.0 + - @backstage/core-components@0.13.6-next.0 + - @backstage/integration-react@1.1.20-next.0 + - @backstage/config@1.1.0 + - @backstage/plugin-search-react@1.7.1-next.0 + - @backstage/plugin-techdocs-react@1.1.12-next.0 + - @backstage/catalog-model@1.4.2 + - @backstage/errors@1.2.2 + - @backstage/theme@0.4.2 + - @backstage/plugin-search-common@1.2.6 + +## 1.7.0 + +### Minor Changes + +- e44f45ac4515: This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. + +### Patch Changes + +- 88c9525a36f3: Fixed bug in styles that caused next and previous links in footer to overlap page content. +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19 + - @backstage/plugin-catalog-react@1.8.4 + - @backstage/core-components@0.13.5 + - @backstage/config@1.1.0 + - @backstage/catalog-model@1.4.2 + - @backstage/core-plugin-api@1.6.0 + - @backstage/errors@1.2.2 + - @backstage/integration@1.7.0 + - @backstage/plugin-search-common@1.2.6 + - @backstage/plugin-search-react@1.7.0 + - @backstage/plugin-techdocs-react@1.1.10 + - @backstage/theme@0.4.2 + +## 1.7.0-next.3 + +### Patch Changes + +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.2 + - @backstage/config@1.1.0-next.2 + - @backstage/core-components@0.13.5-next.3 + - @backstage/core-plugin-api@1.6.0-next.3 + - @backstage/errors@1.2.2-next.0 + - @backstage/integration@1.7.0-next.3 + - @backstage/integration-react@1.1.19-next.3 + - @backstage/plugin-catalog-react@1.8.4-next.3 + - @backstage/plugin-search-common@1.2.6-next.2 + - @backstage/plugin-search-react@1.7.0-next.3 + - @backstage/plugin-techdocs-react@1.1.10-next.3 + - @backstage/theme@0.4.2-next.0 + +## 1.7.0-next.2 + +### Minor Changes + +- e44f45ac4515: This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.6.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.6.8-next.0 + +### Patch Changes + +- 88c9525a36f3: Fixed bug in styles that caused next and previous links in footer to overlap page content. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## 1.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-react@1.1.9 + +## 1.6.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## 1.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16-next.1 + - @backstage/integration@1.5.1 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/core-components@0.13.4-next.0 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.1-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.4-next.0 + - @backstage/plugin-techdocs-react@1.1.9-next.0 + +## 1.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4-next.0 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-catalog-react@1.8.1-next.0 + - @backstage/plugin-search-react@1.6.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration@1.5.1 + - @backstage/integration-react@1.1.16-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-react@1.1.9-next.0 + +## 1.6.5 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.4.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-react@1.8.0 + - @backstage/core-components@0.13.3 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/integration@1.5.1 + - @backstage/integration-react@1.1.15 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.3 + - @backstage/plugin-techdocs-react@1.1.8 + +## 1.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.0-next.2 + - @backstage/theme@0.4.1-next.1 + - @backstage/core-plugin-api@1.5.3-next.1 + - @backstage/core-components@0.13.3-next.2 + - @backstage/catalog-model@1.4.1-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1-next.0 + - @backstage/integration@1.5.1-next.0 + - @backstage/integration-react@1.1.15-next.2 + - @backstage/plugin-search-common@1.2.5-next.0 + - @backstage/plugin-search-react@1.6.3-next.2 + - @backstage/plugin-techdocs-react@1.1.8-next.2 + +## 1.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.4.1-next.0 + - @backstage/core-components@0.13.3-next.1 + - @backstage/core-plugin-api@1.5.3-next.0 + - @backstage/integration-react@1.1.15-next.1 + - @backstage/plugin-catalog-react@1.7.1-next.1 + - @backstage/plugin-search-react@1.6.3-next.1 + - @backstage/plugin-techdocs-react@1.1.8-next.1 + - @backstage/config@1.0.8 + +## 1.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1-next.0 + - @backstage/core-components@0.13.3-next.0 + - @backstage/catalog-model@1.4.1-next.0 + - @backstage/config@1.0.8 + - @backstage/core-plugin-api@1.5.2 + - @backstage/integration@1.5.1-next.0 + - @backstage/integration-react@1.1.15-next.0 + - @backstage/theme@0.4.0 + - @backstage/plugin-catalog-react@1.7.1-next.0 + - @backstage/plugin-search-common@1.2.5-next.0 + - @backstage/plugin-search-react@1.6.3-next.0 + - @backstage/plugin-techdocs-react@1.1.8-next.0 + +## 1.6.4 + +### Patch Changes + +- 2f660eb573cc: Fix SearchBar styles & update StoryBook stories for custom styles for `notchedOutline` class. +- 956d09e8ea68: Change deprecated local references to import from shared `plugin-techdocs-react` plugin +- e33beb1f2a8e: Make the documentation pages printable (also handy for exporting to PDF) +- Updated dependencies + - @backstage/core-plugin-api@1.5.2 + - @backstage/plugin-search-react@1.6.2 + - @backstage/core-components@0.13.2 + - @backstage/theme@0.4.0 + - @backstage/integration@1.5.0 + - @backstage/plugin-catalog-react@1.7.0 + - @backstage/catalog-model@1.4.0 + - @backstage/errors@1.2.0 + - @backstage/plugin-techdocs-react@1.1.7 + - @backstage/integration-react@1.1.14 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.4 + +## 1.6.4-next.3 + +### Patch Changes + +- e33beb1f2a8e: Make the documentation pages printable (also handy for exporting to PDF) +- Updated dependencies + - @backstage/plugin-search-react@1.6.2-next.3 + - @backstage/core-components@0.13.2-next.3 + - @backstage/catalog-model@1.4.0-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.2-next.0 + - @backstage/errors@1.2.0-next.0 + - @backstage/integration@1.5.0-next.0 + - @backstage/integration-react@1.1.14-next.3 + - @backstage/theme@0.4.0-next.1 + - @backstage/plugin-catalog-react@1.7.0-next.3 + - @backstage/plugin-search-common@1.2.4-next.0 + - @backstage/plugin-techdocs-react@1.1.7-next.3 + +## 1.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.4.0-next.1 + - @backstage/plugin-catalog-react@1.7.0-next.2 + - @backstage/core-components@0.13.2-next.2 + - @backstage/integration-react@1.1.14-next.2 + - @backstage/plugin-search-react@1.6.1-next.2 + - @backstage/plugin-techdocs-react@1.1.7-next.2 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.2-next.0 + +## 1.6.3-next.1 + +### Patch Changes + +- 2f660eb573cc: Fix SearchBar styles & update StoryBook stories for custom styles for `notchedOutline` class. +- Updated dependencies + - @backstage/integration@1.5.0-next.0 + - @backstage/errors@1.2.0-next.0 + - @backstage/plugin-search-react@1.6.1-next.1 + - @backstage/core-components@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.7.0-next.1 + - @backstage/catalog-model@1.4.0-next.0 + - @backstage/core-plugin-api@1.5.2-next.0 + - @backstage/integration-react@1.1.14-next.1 + - @backstage/plugin-techdocs-react@1.1.7-next.1 + - @backstage/config@1.0.7 + - @backstage/theme@0.4.0-next.0 + - @backstage/plugin-search-common@1.2.4-next.0 + +## 1.6.3-next.0 + +### Patch Changes + +- 956d09e8ea68: Change deprecated local references to import from shared `plugin-techdocs-react` plugin +- Updated dependencies + - @backstage/plugin-catalog-react@1.7.0-next.0 + - @backstage/theme@0.4.0-next.0 + - @backstage/plugin-techdocs-react@1.1.7-next.0 + - @backstage/integration@1.4.5 + - @backstage/config@1.0.7 + - @backstage/core-components@0.13.2-next.0 + - @backstage/core-plugin-api@1.5.1 + - @backstage/integration-react@1.1.14-next.0 + - @backstage/plugin-search-react@1.6.1-next.0 + - @backstage/catalog-model@1.3.0 + - @backstage/errors@1.1.5 + - @backstage/plugin-search-common@1.2.3 + +## 1.6.2 + +### Patch Changes + +- 863beb49498: Re-add the possibility to have trailing slashes in Techdocs navigation. +- Updated dependencies + - @backstage/theme@0.3.0 + - @backstage/plugin-catalog-react@1.6.0 + - @backstage/integration@1.4.5 + - @backstage/plugin-search-react@1.6.0 + - @backstage/core-components@0.13.1 + - @backstage/integration-react@1.1.13 + - @backstage/plugin-techdocs-react@1.1.6 + - @backstage/catalog-model@1.3.0 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + - @backstage/errors@1.1.5 + - @backstage/plugin-search-common@1.2.3 + +## 1.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.3.0-next.0 + - @backstage/core-components@0.13.1-next.1 + - @backstage/plugin-search-react@1.6.0-next.2 + - @backstage/integration-react@1.1.13-next.2 + - @backstage/plugin-catalog-react@1.6.0-next.2 + - @backstage/plugin-techdocs-react@1.1.6-next.1 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1 + +## 1.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.1-next.0 + - @backstage/core-plugin-api@1.5.1 + - @backstage/plugin-catalog-react@1.6.0-next.1 + - @backstage/plugin-search-react@1.6.0-next.1 + - @backstage/integration-react@1.1.13-next.1 + - @backstage/plugin-techdocs-react@1.1.6-next.0 + - @backstage/config@1.0.7 + +## 1.6.2-next.0 + +### Patch Changes + +- 863beb49498: Re-add the possibility to have trailing slashes in Techdocs navigation. +- Updated dependencies + - @backstage/plugin-catalog-react@1.6.0-next.0 + - @backstage/integration@1.4.5-next.0 + - @backstage/plugin-search-react@1.6.0-next.0 + - @backstage/integration-react@1.1.13-next.0 + - @backstage/core-components@0.13.0 + - @backstage/core-plugin-api@1.5.1 + - @backstage/catalog-model@1.3.0 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/theme@0.2.19 + - @backstage/plugin-search-common@1.2.3 + - @backstage/plugin-techdocs-react@1.1.5 + +## 1.6.1 + +### Patch Changes + +- 6c809d1a41c: Minor visual tweaks to adapt to changes in mkdocs-material v9 + +- b2e182cdfa4: Fixes a UI bug in search result item which rendered the item text with incorrect font size and color + +- 847a1eee3da: Change anchor links color in Techdocs content + + With the color (mkdocs supplied) used for anchor links the background and foreground colors do not have a sufficient contrast ratio. Using the link color from theme palette. + +- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs) + +- 2e493480626: Fix a bug in sub-path navigation due to double addition of a sub-path if one was set up in `app.baseUrl`. + +- e0c6e8b9c3c: Update peer dependencies + +- Updated dependencies + - @backstage/core-components@0.13.0 + - @backstage/plugin-catalog-react@1.5.0 + - @backstage/plugin-search-react@1.5.2 + - @backstage/plugin-techdocs-react@1.1.5 + - @backstage/integration-react@1.1.12 + - @backstage/theme@0.2.19 + - @backstage/core-plugin-api@1.5.1 + - @backstage/catalog-model@1.3.0 + - @backstage/integration@1.4.4 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/plugin-search-common@1.2.3 + +## 1.6.1-next.3 + +### Patch Changes + +- 2e493480626: Fix a bug in sub-path navigation due to double addition of a sub-path if one was set up in `app.baseUrl`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.5.0-next.3 + - @backstage/catalog-model@1.3.0-next.0 + - @backstage/core-components@0.13.0-next.3 + - @backstage/config@1.0.7 + - @backstage/core-plugin-api@1.5.1-next.1 + - @backstage/errors@1.1.5 + - @backstage/integration@1.4.4-next.0 + - @backstage/integration-react@1.1.12-next.3 + - @backstage/theme@0.2.19-next.0 + - @backstage/plugin-search-common@1.2.3-next.0 + - @backstage/plugin-search-react@1.5.2-next.3 + - @backstage/plugin-techdocs-react@1.1.5-next.3 + +## 1.6.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.6-next.2 + - @backstage/plugin-catalog-react@1.4.1-next.2 + - @backstage/core-plugin-api@1.5.1-next.1 + - @backstage/catalog-model@1.2.1 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/integration@1.4.4-next.0 + - @backstage/integration-react@1.1.12-next.2 + - @backstage/theme@0.2.19-next.0 + - @backstage/plugin-search-common@1.2.3-next.0 + - @backstage/plugin-search-react@1.5.2-next.2 + - @backstage/plugin-techdocs-react@1.1.5-next.2 + +## 1.6.1-next.1 + +### Patch Changes + +- 6c809d1a41c: Minor visual tweaks to adapt to changes in mkdocs-material v9 + +- 847a1eee3da: Change anchor links color in Techdocs content + + With the color (mkdocs supplied) used for anchor links the background and foreground colors do not have a sufficient contrast ratio. Using the link color from theme palette. + +- e0c6e8b9c3c: Update peer dependencies + +- Updated dependencies + - @backstage/core-components@0.12.6-next.1 + - @backstage/integration-react@1.1.12-next.1 + - @backstage/core-plugin-api@1.5.1-next.0 + - @backstage/plugin-techdocs-react@1.1.5-next.1 + - @backstage/plugin-catalog-react@1.4.1-next.1 + - @backstage/integration@1.4.4-next.0 + - @backstage/plugin-search-react@1.5.2-next.1 + - @backstage/theme@0.2.19-next.0 + - @backstage/catalog-model@1.2.1 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/plugin-search-common@1.2.3-next.0 + +## 1.6.1-next.0 + +### Patch Changes + +- b2e182cdfa4: Fixes a UI bug in search result item which rendered the item text with incorrect font size and color +- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs) +- Updated dependencies + - @backstage/core-components@0.12.6-next.0 + - @backstage/plugin-search-react@1.5.2-next.0 + - @backstage/plugin-techdocs-react@1.1.5-next.0 + - @backstage/plugin-catalog-react@1.4.1-next.0 + - @backstage/integration-react@1.1.12-next.0 + - @backstage/core-plugin-api@1.5.0 + - @backstage/config@1.0.7 + - @backstage/integration@1.4.3 + - @backstage/catalog-model@1.2.1 + - @backstage/errors@1.1.5 + - @backstage/theme@0.2.18 + - @backstage/plugin-search-common@1.2.2 + +## 1.6.0 + +### Minor Changes + +- 3f75b7607ca: Add ability to pass icon as function to have ability to customize it by search item + +### Patch Changes + +- 65454876fb2: Minor API report tweaks +- 54a1e133b56: Fix bug that caused next and previous links not to work with certain versions of mkdocs-material +- f320c299c67: The HTML tag attributes in the documentation content inserted to shadow DOM is preserved to improve accessibility +- cb8ec97cdeb: Change black & white colors to be theme aware +- c10384a9235: Switch to using `LinkButton` instead of the deprecated `Button` +- 8adfda60ae1: Updated dependency `jss` to `~10.10.0`. +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 238cf657c09: Copy to clipboard now works in a not secure context. +- Updated dependencies + - @backstage/core-components@0.12.5 + - @backstage/plugin-techdocs-react@1.1.4 + - @backstage/plugin-catalog-react@1.4.0 + - @backstage/plugin-search-react@1.5.1 + - @backstage/errors@1.1.5 + - @backstage/core-plugin-api@1.5.0 + - @backstage/catalog-model@1.2.1 + - @backstage/integration-react@1.1.11 + - @backstage/integration@1.4.3 + - @backstage/config@1.0.7 + - @backstage/theme@0.2.18 + - @backstage/plugin-search-common@1.2.2 + +## 1.6.0-next.2 + +### Patch Changes + +- 65454876fb2: Minor API report tweaks +- Updated dependencies + - @backstage/core-components@0.12.5-next.2 + - @backstage/plugin-techdocs-react@1.1.4-next.2 + - @backstage/plugin-catalog-react@1.4.0-next.2 + - @backstage/plugin-search-react@1.5.1-next.2 + - @backstage/core-plugin-api@1.5.0-next.2 + - @backstage/integration-react@1.1.11-next.2 + - @backstage/config@1.0.7-next.0 + - @backstage/integration@1.4.3-next.0 + +## 1.6.0-next.1 + +### Patch Changes + +- 54a1e133b56: Fix bug that caused next and previous links not to work with certain versions of mkdocs-material +- cb8ec97cdeb: Change black & white colors to be theme aware +- c10384a9235: Switch to using `LinkButton` instead of the deprecated `Button` +- 8adfda60ae1: Updated dependency `jss` to `~10.10.0`. +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 238cf657c09: Copy to clipboard now works in a not secure context. +- Updated dependencies + - @backstage/core-components@0.12.5-next.1 + - @backstage/errors@1.1.5-next.0 + - @backstage/plugin-techdocs-react@1.1.4-next.1 + - @backstage/core-plugin-api@1.4.1-next.1 + - @backstage/integration-react@1.1.11-next.1 + - @backstage/integration@1.4.3-next.0 + - @backstage/config@1.0.7-next.0 + - @backstage/theme@0.2.18-next.0 + - @backstage/plugin-catalog-react@1.4.0-next.1 + - @backstage/catalog-model@1.2.1-next.1 + - @backstage/plugin-search-common@1.2.2-next.0 + - @backstage/plugin-search-react@1.5.1-next.1 + +## 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 `
  • ` elements. + +- 4660b63947: Create a TechDocs `` 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 = () => { + + // other plugin routes + }> + + + } + > + + + + + + ; + }; + ``` + +- 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 `
  • ` 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 + +- a74dd61534: Fix sizing of build log component to render all lines +- 80ce4e8c29: Small updates to some components to ensure theme typography properties are inherited correctly. +- 7115c7389b: Updated dependency `jss` to `~10.9.0`. +- Updated dependencies + - @backstage/catalog-model@1.1.5 + - @backstage/plugin-catalog-react@1.2.4 + - @backstage/core-components@0.12.3 + - @backstage/plugin-search-react@1.4.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-techdocs-react@1.1.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.9 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1 + +## 1.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.4.0-next.2 + - @backstage/core-plugin-api@1.3.0-next.1 + - @backstage/plugin-catalog-react@1.2.4-next.2 + - @backstage/plugin-techdocs-react@1.1.2-next.2 + - @backstage/catalog-model@1.1.5-next.1 + - @backstage/config@1.0.6-next.0 + - @backstage/core-components@0.12.3-next.2 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2-next.0 + - @backstage/integration-react@1.1.9-next.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.1-next.0 + +## 1.4.3-next.1 + +### Patch Changes + +- a74dd61534: Fix sizing of build log component to render all lines +- Updated dependencies + - @backstage/config@1.0.6-next.0 + - @backstage/catalog-model@1.1.5-next.1 + - @backstage/core-components@0.12.3-next.1 + - @backstage/core-plugin-api@1.2.1-next.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2-next.0 + - @backstage/integration-react@1.1.9-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.4-next.1 + - @backstage/plugin-search-common@1.2.1-next.0 + - @backstage/plugin-search-react@1.3.2-next.1 + - @backstage/plugin-techdocs-react@1.1.2-next.1 + +## 1.4.3-next.0 + +### Patch Changes + +- 7115c7389b: Updated dependency `jss` to `~10.9.0`. +- Updated dependencies + - @backstage/catalog-model@1.1.5-next.0 + - @backstage/plugin-catalog-react@1.2.4-next.0 + - @backstage/core-components@0.12.3-next.0 + - @backstage/plugin-techdocs-react@1.1.2-next.0 + - @backstage/config@1.0.5 + - @backstage/core-plugin-api@1.2.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.1 + - @backstage/integration-react@1.1.9-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.0 + - @backstage/plugin-search-react@1.3.2-next.0 + +## 1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.2 + - @backstage/integration-react@1.1.8 + - @backstage/plugin-catalog-react@1.2.3 + - @backstage/plugin-search-react@1.3.1 + - @backstage/plugin-techdocs-react@1.1.1 + +## 1.4.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`. +- a19cffbeed: Update search links to only have header as linkable text +- 5d3058355d: Add `react/forbid-elements` linter rule for button, suggest Material UI `Button` +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 786f1b1419: Support older versions of react-router +- Updated dependencies + - @backstage/plugin-techdocs-react@1.1.0 + - @backstage/core-plugin-api@1.2.0 + - @backstage/plugin-search-react@1.3.0 + - @backstage/core-components@0.12.1 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-react@1.2.2 + - @backstage/integration-react@1.1.7 + - @backstage/integration@1.4.1 + - @backstage/plugin-search-common@1.2.0 + - @backstage/catalog-model@1.1.4 + - @backstage/config@1.0.5 + - @backstage/theme@0.2.16 + +## 1.4.1-next.4 + +### Patch Changes + +- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.4 + - @backstage/plugin-catalog-react@1.2.2-next.4 + - @backstage/plugin-search-react@1.3.0-next.4 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/core-plugin-api@1.2.0-next.2 + - @backstage/errors@1.1.4-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/integration-react@1.1.7-next.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.2.0-next.3 + - @backstage/plugin-techdocs-react@1.0.7-next.4 + +## 1.4.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.3 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/core-plugin-api@1.2.0-next.2 + - @backstage/errors@1.1.4-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/integration-react@1.1.7-next.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.2-next.3 + - @backstage/plugin-search-common@1.2.0-next.2 + - @backstage/plugin-search-react@1.3.0-next.3 + - @backstage/plugin-techdocs-react@1.0.7-next.3 + +## 1.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.2.0-next.2 + - @backstage/plugin-search-react@1.3.0-next.2 + - @backstage/core-components@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.2.2-next.2 + - @backstage/plugin-search-common@1.2.0-next.2 + - @backstage/integration-react@1.1.7-next.2 + - @backstage/plugin-techdocs-react@1.0.7-next.2 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## 1.4.1-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.4.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 1.4.0 + +### Minor Changes + +- 5691baea69: Add ability to configure filters when using EntityListDocsGrid + + The following example will render two sections of cards grid: + + - One section for documentations tagged as `recommended` + - One section for documentations tagged as `runbook` + + ```js + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + { + title: "RunBooks Documentation", + filterPredicate: entity => + entity?.metadata?.tags?.includes('runbook') ?? false, + } + ]}} /> + ``` + +- 63705e73d9: Hide document description if not provided + +- 847fc588a6: Updated TechDocs header to include label for source code icon and updated label to reflect Kind name + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- cbe11d1e23: Tweak README +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- 3a1a999b7b: Include query parameters when navigating to relative links in documents +- bd2aab4726: An analytics event matching the semantics of the `click` action is now captured when users click links within a TechDocs document. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## 1.4.0-next.2 + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## 1.4.0-next.1 + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app + +## 1.4.0-next.0 + +### Minor Changes + +- 5691baea69: Add ability to configure filters when using EntityListDocsGrid + + The following example will render two sections of cards grid: + + - One section for documentations tagged as `recommended` + - One section for documentations tagged as `runbook` + + ```js + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + { + title: "RunBooks Documentation", + filterPredicate: entity => + entity?.metadata?.tags?.includes('runbook') ?? false, + } + ]}} /> + ``` + +### Patch Changes + +- cbe11d1e23: Tweak README +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- 3a1a999b7b: Include query parameters when navigating to relative links in documents +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1-next.0 + - @backstage/core-components@0.12.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/integration-react@1.1.6-next.0 + - @backstage/plugin-search-react@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.0.6-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1-next.0 + +## 1.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.2 + - @backstage/plugin-catalog-react@1.2.0 + - @backstage/core-components@0.11.2 + - @backstage/plugin-search-react@1.2.0 + - @backstage/plugin-search-common@1.1.0 + - @backstage/plugin-techdocs-react@1.0.5 + - @backstage/integration-react@1.1.5 + - @backstage/core-plugin-api@1.0.7 + - @backstage/config@1.0.3 + - @backstage/errors@1.1.2 + - @backstage/integration@1.3.2 + - @backstage/theme@0.2.16 + +## 1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.0-next.2 + - @backstage/plugin-search-common@1.1.0-next.2 + - @backstage/catalog-model@1.1.2-next.2 + - @backstage/config@1.0.3-next.2 + - @backstage/core-components@0.11.2-next.2 + - @backstage/core-plugin-api@1.0.7-next.2 + - @backstage/errors@1.1.2-next.2 + - @backstage/integration@1.3.2-next.2 + - @backstage/integration-react@1.1.5-next.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-react@1.2.0-next.2 + - @backstage/plugin-techdocs-react@1.0.5-next.2 + +## 1.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.0-next.1 + - @backstage/plugin-search-react@1.2.0-next.1 + - @backstage/plugin-search-common@1.1.0-next.1 + - @backstage/core-components@0.11.2-next.1 + - @backstage/core-plugin-api@1.0.7-next.1 + - @backstage/catalog-model@1.1.2-next.1 + - @backstage/config@1.0.3-next.1 + - @backstage/errors@1.1.2-next.1 + - @backstage/integration@1.3.2-next.1 + - @backstage/integration-react@1.1.5-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.0.5-next.1 + +## 1.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.2-next.0 + - @backstage/core-components@0.11.2-next.0 + - @backstage/plugin-catalog-react@1.1.5-next.0 + - @backstage/plugin-techdocs-react@1.0.5-next.0 + - @backstage/integration-react@1.1.5-next.0 + - @backstage/plugin-search-react@1.1.1-next.0 + - @backstage/config@1.0.3-next.0 + - @backstage/core-plugin-api@1.0.7-next.0 + - @backstage/errors@1.1.2-next.0 + - @backstage/integration@1.3.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.0.2-next.0 + +## 1.3.2 + +### Patch Changes + +- 817f3196f6: Updated React Router dependencies to be peer dependencies. +- eadf56bbbf: Bump `git-url-parse` version to `^13.0.0` +- 3f739be9d9: Minor API signatures cleanup +- 763fb81e82: Internal refactor to use more type safe code when dealing with route parameters. +- 7d47def9c4: Removed dependency on `@types/jest`. +- 817f3196f6: Updated the `TechDocsReaderPage` to be compatible with React Router v6 stable. +- 7a95c705fa: Fixed a bug where addons wouldn't render on sub pages when using React Route v6 stable. +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. +- ca8d5a6eae: Use the new `SearchAutocomplete` component in the `TechDocsSearch` component to maintain consistency across search experiences and avoid code duplication. +- 829f14a9b0: Always update the title and sub-title when the location changes on a `TechDocs` reader page. +- e97d616f08: Fixed a bug where scrolling for anchors where the id starts with number didn't work for the current page. +- ef9ab322de: Minor API signatures cleanup +- Updated dependencies + - @backstage/core-components@0.11.1 + - @backstage/core-plugin-api@1.0.6 + - @backstage/plugin-catalog-react@1.1.4 + - @backstage/plugin-search-react@1.1.0 + - @backstage/plugin-techdocs-react@1.0.4 + - @backstage/integration@1.3.1 + - @backstage/catalog-model@1.1.1 + - @backstage/config@1.0.2 + - @backstage/errors@1.1.1 + - @backstage/integration-react@1.1.4 + - @backstage/plugin-search-common@1.0.1 + +## 1.3.2-next.3 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.4-next.2 + - @backstage/catalog-model@1.1.1-next.0 + - @backstage/config@1.0.2-next.0 + - @backstage/core-components@0.11.1-next.3 + - @backstage/core-plugin-api@1.0.6-next.3 + - @backstage/errors@1.1.1-next.0 + - @backstage/integration@1.3.1-next.2 + - @backstage/integration-react@1.1.4-next.2 + - @backstage/plugin-techdocs-react@1.0.4-next.2 + +## 1.3.2-next.2 + +### Patch Changes + +- eadf56bbbf: Bump `git-url-parse` version to `^13.0.0` +- 7a95c705fa: Fixed a bug where addons wouldn't render on sub pages when using React Route v6 stable. +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. +- ca8d5a6eae: Use the new `SearchAutocomplete` component in the `TechDocsSearch` component to maintain consistency across search experiences and avoid code duplication. +- e97d616f08: Fixed a bug where scrolling for anchors where the id starts with number didn't work for the current page. +- Updated dependencies + - @backstage/integration@1.3.1-next.1 + - @backstage/core-components@0.11.1-next.2 + - @backstage/core-plugin-api@1.0.6-next.2 + - @backstage/integration-react@1.1.4-next.1 + - @backstage/plugin-search-react@1.1.0-next.2 + +## 1.3.2-next.1 + +### Patch Changes + +- 817f3196f6: Updated React Router dependencies to be peer dependencies. +- 763fb81e82: Internal refactor to use more type safe code when dealing with route parameters. +- 817f3196f6: Updated the `TechDocsReaderPage` to be compatible with React Router v6 stable. +- Updated dependencies + - @backstage/core-components@0.11.1-next.1 + - @backstage/core-plugin-api@1.0.6-next.1 + - @backstage/plugin-catalog-react@1.1.4-next.1 + - @backstage/plugin-search-react@1.0.2-next.1 + - @backstage/plugin-techdocs-react@1.0.4-next.1 + +## 1.3.2-next.0 + +### Patch Changes + +- 3f739be9d9: Minor API signatures cleanup +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. +- 829f14a9b0: Always update the title and sub-title when the location changes on a `TechDocs` reader page. +- ef9ab322de: Minor API signatures cleanup +- Updated dependencies + - @backstage/core-plugin-api@1.0.6-next.0 + - @backstage/core-components@0.11.1-next.0 + - @backstage/integration-react@1.1.4-next.0 + - @backstage/integration@1.3.1-next.0 + - @backstage/plugin-catalog-react@1.1.4-next.0 + - @backstage/plugin-search-react@1.0.2-next.0 + - @backstage/plugin-techdocs-react@1.0.4-next.0 + - @backstage/plugin-search-common@1.0.1-next.0 + +## 1.3.1 + +### Patch Changes + +- e924d2d013: Added back reduction in size, this fixes the extremely large TeachDocs headings +- b86ed4d990: Add highlight to active navigation item and navigation parents. +- 7a98c73dc8: Fixed techdocs sidebar layout bug for medium devices. +- 8acb22205c: Scroll techdocs navigation into focus and expand any nested navigation items. +- Updated dependencies + - @backstage/integration@1.3.0 + - @backstage/core-components@0.11.0 + - @backstage/core-plugin-api@1.0.5 + - @backstage/plugin-catalog-react@1.1.3 + - @backstage/plugin-techdocs-react@1.0.3 + - @backstage/integration-react@1.1.3 + - @backstage/plugin-search-react@1.0.1 + +## 1.3.1-next.2 + +### Patch Changes + +- 8acb22205c: Scroll techdocs navigation into focus and expand any nested navigation items. +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.3-next.2 + - @backstage/core-components@0.11.0-next.2 + - @backstage/integration-react@1.1.3-next.1 + - @backstage/plugin-search-react@1.0.1-next.1 + - @backstage/plugin-techdocs-react@1.0.3-next.2 + +## 1.3.1-next.1 + +### Patch Changes + +- b86ed4d990: Add highlight to active navigation item and navigation parents. +- Updated dependencies + - @backstage/core-components@0.10.1-next.1 + - @backstage/integration@1.3.0-next.1 + - @backstage/plugin-techdocs-react@1.0.3-next.1 + - @backstage/plugin-catalog-react@1.1.3-next.1 + +## 1.3.1-next.0 + +### Patch Changes + +- 7a98c73dc8: Fixed techdocs sidebar layout bug for medium devices. +- Updated dependencies + - @backstage/integration@1.3.0-next.0 + - @backstage/core-plugin-api@1.0.5-next.0 + - @backstage/integration-react@1.1.3-next.0 + - @backstage/plugin-catalog-react@1.1.3-next.0 + - @backstage/core-components@0.10.1-next.0 + - @backstage/plugin-search-react@1.0.1-next.0 + - @backstage/plugin-techdocs-react@1.0.3-next.0 + +## 1.3.0 + +### Minor Changes + +- ebf3eb1641: Use the same initial filter `owned` for the `TechDocsIndexPage` as for the `CatalogPage`. + + If you prefer to keep the previous behavior, you can change the default for the initial filter + to `all` (or `starred` if you rather prefer that). + + + + In general, with this change you will be able to set props at `TechDocsIndexPage`. + +### Patch Changes + +- a70869e775: Updated dependency `msw` to `^0.43.0`. + +- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. + +- e2d7b76f43: Upgrade git-url-parse to 12.0.0. + + Motivation for upgrade is transitively upgrading parse-url which is vulnerable + to several CVEs detected by Snyk. + + - SNYK-JS-PARSEURL-2935944 + - SNYK-JS-PARSEURL-2935947 + - SNYK-JS-PARSEURL-2936249 + +- 3cbebf710e: Reorder browser tab title in Techdocs pages to have the site name first. + +- 726577958f: Remove the 60% factor from the font size calculation of headers to use the exact size defined in BackstageTheme. + +- 7739141ab2: Fix: When docs are shown in an entity page under the docs tab the sidebars start overlapping with the header and tabs in the page when you scroll the documentation content. + +- Updated dependencies + - @backstage/core-components@0.10.0 + - @backstage/catalog-model@1.1.0 + - @backstage/plugin-techdocs-react@1.0.2 + - @backstage/plugin-search-react@1.0.0 + - @backstage/plugin-search-common@1.0.0 + - @backstage/core-plugin-api@1.0.4 + - @backstage/integration@1.2.2 + - @backstage/integration-react@1.1.2 + - @backstage/plugin-catalog-react@1.1.2 + - @backstage/theme@0.2.16 + - @backstage/errors@1.1.0 + +## 1.2.1-next.3 + +### Patch Changes + +- a70869e775: Updated dependency `msw` to `^0.43.0`. +- Updated dependencies + - @backstage/core-plugin-api@1.0.4-next.0 + - @backstage/core-components@0.10.0-next.3 + - @backstage/integration-react@1.1.2-next.3 + - @backstage/integration@1.2.2-next.3 + - @backstage/catalog-model@1.1.0-next.3 + - @backstage/plugin-catalog-react@1.1.2-next.3 + - @backstage/plugin-search-react@0.2.2-next.3 + - @backstage/plugin-techdocs-react@1.0.2-next.2 + +## 1.2.1-next.2 + +### Patch Changes + +- e2d7b76f43: Upgrade git-url-parse to 12.0.0. + + Motivation for upgrade is transitively upgrading parse-url which is vulnerable + to several CVEs detected by Snyk. + + - SNYK-JS-PARSEURL-2935944 + - SNYK-JS-PARSEURL-2935947 + - SNYK-JS-PARSEURL-2936249 + +- 7739141ab2: Fix: When docs are shown in an entity page under the docs tab the sidebars start overlapping with the header and tabs in the page when you scroll the documentation content. + +- Updated dependencies + - @backstage/core-components@0.10.0-next.2 + - @backstage/catalog-model@1.1.0-next.2 + - @backstage/plugin-search-react@0.2.2-next.2 + - @backstage/theme@0.2.16-next.1 + - @backstage/integration@1.2.2-next.2 + - @backstage/plugin-catalog-react@1.1.2-next.2 + - @backstage/integration-react@1.1.2-next.2 + - @backstage/plugin-techdocs-react@1.0.2-next.1 + +## 1.2.1-next.1 + +### Patch Changes + +- 726577958f: Remove the 60% factor from the font size calculation of headers to use the exact size defined in BackstageTheme. +- Updated dependencies + - @backstage/core-components@0.9.6-next.1 + - @backstage/catalog-model@1.1.0-next.1 + - @backstage/errors@1.1.0-next.0 + - @backstage/theme@0.2.16-next.0 + - @backstage/integration@1.2.2-next.1 + - @backstage/integration-react@1.1.2-next.1 + - @backstage/plugin-catalog-react@1.1.2-next.1 + - @backstage/plugin-search-common@0.3.6-next.0 + - @backstage/plugin-search-react@0.2.2-next.1 + +## 1.2.1-next.0 + +### Patch Changes + +- 3cbebf710e: Reorder browser tab title in Techdocs pages to have the site name first. +- Updated dependencies + - @backstage/catalog-model@1.1.0-next.0 + - @backstage/core-components@0.9.6-next.0 + - @backstage/plugin-techdocs-react@1.0.2-next.0 + - @backstage/integration@1.2.2-next.0 + - @backstage/plugin-catalog-react@1.1.2-next.0 + - @backstage/integration-react@1.1.2-next.0 + - @backstage/plugin-search-react@0.2.2-next.0 + +## 1.2.0 + +### Minor Changes + +- fe7614ea54: Add an optional icon to the Catalog and TechDocs search results + +### Patch Changes + +- d047d81295: Use entity title as label in `TechDocsReaderPageHeader` if available + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. + +- bff65e6958: Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()` from `@backstage/core-components`. + +- 915700f64f: In order to simplify analytics on top of the search experience in Backstage, the provided `<*ResultListItem />` component now captures a `discover` analytics event instead of a `click` event. This event includes the result rank as its `value` and, like a click, the URL/path clicked to as its `to` attribute. + +- 881fbd7e8d: Fix `EntityTechdocsContent` component to use objects instead of `` elements, otherwise "outlet" will be null on sub-pages and add-ons won't render. + +- 17c059dfd0: Restructures reader style transformations to improve code readability: + + - Extracts the style rules to separate files; + - Creates a hook that processes each rule; + - And creates another hook that returns a transformer responsible for injecting them into the head tag of a given element. + +- 3b45ad701f: Packages a set of tweaks to the TechDocs addons rendering process: + + - Prevents displaying sidebars until page styles are loaded and the sidebar position is updated; + - Prevents new sidebar locations from being created every time the reader page is rendered if these locations already exist; + - Centers the styles loaded event to avoid having multiple locations setting the opacity style in Shadow Dom causing the screen to flash multiple times. + +- 9b94ade898: Use entity title in `TechDocsSearch` placeholder if available. + +- 816f7475ec: Convert `sanitizeDOM` transformer to hook as part of code readability improvements in dom file. + +- 50ff56a80f: Change the `EntityDocsPage` path to be more specific and also add integration tests for `sub-routes` on this page. + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.1 + - @backstage/plugin-search-common@0.3.5 + - @backstage/plugin-search-react@0.2.1 + - @backstage/core-components@0.9.5 + - @backstage/integration@1.2.1 + - @backstage/core-plugin-api@1.0.3 + - @backstage/integration-react@1.1.1 + - @backstage/catalog-model@1.0.3 + - @backstage/plugin-techdocs-react@1.0.1 + +## 1.1.2-next.1 + +### Patch Changes + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. +- bff65e6958: Updated sidebar-related logic to use `` + `useSidebarPinState()` and/or `` + `useSidebarOpenState()` from `@backstage/core-components`. +- Updated dependencies + - @backstage/core-components@0.9.5-next.1 + - @backstage/core-plugin-api@1.0.3-next.0 + - @backstage/integration-react@1.1.1-next.1 + - @backstage/integration@1.2.1-next.1 + - @backstage/catalog-model@1.0.3-next.0 + - @backstage/plugin-catalog-react@1.1.1-next.1 + - @backstage/plugin-search-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.0.1-next.1 + - @backstage/plugin-search-common@0.3.5-next.0 + +## 1.1.2-next.0 + +### Patch Changes + +- 881fbd7e8d: Fix `EntityTechdocsContent` component to use objects instead of `` elements, otherwise "outlet" will be null on sub-pages and add-ons won't render. + +- 17c059dfd0: Restructures reader style transformations to improve code readability: + + - Extracts the style rules to separate files; + - Creates a hook that processes each rule; + - And creates another hook that returns a transformer responsible for injecting them into the head tag of a given element. + +- 3b45ad701f: Packages a set of tweaks to the TechDocs addons rendering process: + + - Prevents displaying sidebars until page styles are loaded and the sidebar position is updated; + - Prevents new sidebar locations from being created every time the reader page is rendered if these locations already exist; + - Centers the styles loaded event to avoid having multiple locations setting the opacity style in Shadow Dom causing the screen to flash multiple times. + +- 816f7475ec: Convert `sanitizeDOM` transformer to hook as part of code readability improvements in dom file. + +- 50ff56a80f: Change the `EntityDocsPage` path to be more specific and also add integration tests for `sub-routes` on this page. + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.1-next.0 + - @backstage/core-components@0.9.5-next.0 + - @backstage/integration@1.2.1-next.0 + - @backstage/plugin-techdocs-react@1.0.1-next.0 + - @backstage/integration-react@1.1.1-next.0 + +## 1.1.1 + +### Patch Changes + +- 52419be116: Create a menu in the sub header of documentation pages, it is responsible for rendering TechDocs addons that allow users to customize their reading experience. +- a307a14be0: Removed dependency on `@backstage/core-app-api`. +- bed0d64ce9: Fixed bugs that prevented a 404 error from being shown when it should have been. +- 2efee91251: Add a `sub-route` path on the EntityDocs page to fix the blank screen error when navigating using sidebar links. +- 2dcb2c9678: Loading SVGs correctly with `bota` with extended characters +- 52fddad92d: The `TechDocsStorageApi` and its associated ref are now exported by `@backstage/plugin-techdocs-react`. The API interface, ref, and types are now deprecated in `@backstage/plugin-techdocs` and will be removed in a future release. +- 0ad901569f: Hidden exports related to experimental TechDocs reader functionality have been removed and can no longer be imported. In the unlikely event you were using these exports, you can now take advantage of the officially supported and generally available TechDocs Addon framework instead. +- 3a74e203a8: Updated search result components to support rendering content with highlighted matched terms +- Updated dependencies + - @backstage/core-components@0.9.4 + - @backstage/integration@1.2.0 + - @backstage/core-plugin-api@1.0.2 + - @backstage/plugin-catalog-react@1.1.0 + - @backstage/integration-react@1.1.0 + - @backstage/plugin-techdocs-react@1.0.0 + - @backstage/config@1.0.1 + - @backstage/plugin-search-react@0.2.0 + - @backstage/plugin-search-common@0.3.4 + - @backstage/catalog-model@1.0.2 + +## 1.1.1-next.3 + +### Patch Changes + +- cc8ddd0979: revert dependency `event-source-polyfill` to `1.0.25` +- Updated dependencies + - @backstage/core-components@0.9.4-next.2 + +## 1.1.1-next.2 + +### Patch Changes + +- 52419be116: Create a menu in the sub header of documentation pages, it is responsible for rendering TechDocs addons that allow users to customize their reading experience. +- 1af133f779: Updated dependency `event-source-polyfill` to `1.0.26`. +- 2dcb2c9678: Loading SVGs correctly with `bota` with extended characters +- 3a74e203a8: Updated search result components to support rendering content with highlighted matched terms +- Updated dependencies + - @backstage/core-components@0.9.4-next.1 + - @backstage/plugin-techdocs-react@0.1.1-next.2 + - @backstage/config@1.0.1-next.0 + - @backstage/plugin-search-react@0.2.0-next.2 + - @backstage/plugin-search-common@0.3.4-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.2 + - @backstage/catalog-model@1.0.2-next.0 + - @backstage/core-plugin-api@1.0.2-next.1 + - @backstage/integration@1.2.0-next.1 + - @backstage/integration-react@1.1.0-next.2 + +## 1.1.1-next.1 + +### Patch Changes + +- 52fddad92d: The `TechDocsStorageApi` and its associated ref are now exported by `@backstage/plugin-techdocs-react`. The API interface, ref, and types are now deprecated in `@backstage/plugin-techdocs` and will be removed in a future release. +- Updated dependencies + - @backstage/core-components@0.9.4-next.0 + - @backstage/core-plugin-api@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.1 + - @backstage/plugin-search-react@0.2.0-next.1 + - @backstage/plugin-techdocs-react@0.1.1-next.1 + - @backstage/integration-react@1.1.0-next.1 + +## 1.1.1-next.0 + +### Patch Changes + +- a307a14be0: Removed dependency on `@backstage/core-app-api`. +- bed0d64ce9: Fixed bugs that prevented a 404 error from being shown when it should have been. +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + +## 1.1.0 + +### Minor Changes + +- ace749b785: TechDocs supports a new, experimental method of customization: addons! + + To customize the standalone TechDocs reader page experience, update your `/packages/app/src/App.tsx` in the following way: + + ```diff + import { TechDocsIndexPage, TechDocsReaderPage } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + } /> + } + > + + + + + + + + + // ... + ``` + + To customize the TechDocs reader experience on the Catalog entity page, update your `packages/app/src/components/catalog/EntityPage.tsx` in the following way: + + ```diff + import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + + + {overviewContent} + + + + - + + + + + + + + + + + + + + // ... + ``` + + If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary! + +### Patch Changes + +- ab230a433f: imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search` + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- 230ad0826f: Bump to using `@types/node` v16 + +- f0fb9153b7: Fix broken query selectors on techdocs + +- 9975ff9852: Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. + +- 3ba256c389: Fixed a bug preventing custom TechDocs reader page implementations from rendering without being double-wrapped in the `` component. + +- fe53fe97d7: Fix permalink scrolling for anchors where the id starts with a number. + +- 0152c0de22: Some documentation layout tweaks: + + - drawer toggle margins + - code block margins + - sidebar drawer width + - inner content width + - footer link width + - sidebar table of contents scroll + +- 3ba256c389: Fixed a bug that caused addons in the `Subheader` location to break the default TechDocs reader page layout. + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-techdocs-react@0.1.0 + - @backstage/integration-react@1.0.1 + +## 1.1.0-next.3 + +### Minor Changes + +- ace749b785: TechDocs supports a new, experimental method of customization: addons! + + To customize the standalone TechDocs reader page experience, update your `/packages/app/src/App.tsx` in the following way: + + ```diff + import { TechDocsIndexPage, TechDocsReaderPage } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + } /> + } + > + + + + + + + + + // ... + ``` + + To customize the TechDocs reader experience on the Catalog entity page, update your `packages/app/src/components/catalog/EntityPage.tsx` in the following way: + + ```diff + import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + + + {overviewContent} + + + + - + + + + + + + + + + + + + + // ... + ``` + + If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary! + +### Patch Changes + +- ab230a433f: imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search` +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-search-react@0.1.0-next.0 + - @backstage/integration@1.1.0-next.2 + - @backstage/plugin-techdocs-react@0.1.0-next.0 + +## 1.0.1-next.2 + +### Patch Changes + +- f0fb9153b7: Fix broken query selectors on techdocs +- 9975ff9852: Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. +- Updated dependencies + - @backstage/core-components@0.9.3-next.1 + - @backstage/plugin-catalog-react@1.0.1-next.2 + - @backstage/catalog-model@1.0.1-next.1 + +## 1.0.1 + +### Patch Changes + +- Pin the `event-source-polyfill` dependency to version 1.0.25 + +## 1.0.1-next.1 + +### Patch Changes + +- 0152c0de22: Some documentation layout tweaks: + + - drawer toggle margins + - code block margins + - sidebar drawer width + - inner content width + - footer link width + - sidebar table of contents scroll + +- Updated dependencies + - @backstage/integration@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.0.1-next.1 + - @backstage/integration-react@1.0.1-next.1 + +## 1.0.1-next.0 + +### Patch Changes + +- fe53fe97d7: Fix permalink scrolling for anchors where the id starts with a number. +- Updated dependencies + - @backstage/catalog-model@1.0.1-next.0 + - @backstage/plugin-search@0.7.5-next.0 + - @backstage/integration@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.0 + - @backstage/core-components@0.9.3-next.0 + - @backstage/integration-react@1.0.1-next.0 + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Minor Changes + +- 700d93ff41: Removed deprecated exports, including: + + - deprecated `DocsResultListItem` is now deleted and fully replaced with `TechDocsSearchResultListItem` + - deprecated `TechDocsPage` is now deleted and fully replaced with `TechDocsReaderPage` + - deprecated `TechDocsPageHeader` is now deleted and fully replaced with `TechDocsReaderPageHeader` + - deprecated `TechDocsPageHeaderProps` is now deleted and fully replaced with `TechDocsReaderPageHeaderProps` + - deprecated `TechDocsPageRenderFunction` is now deleted and fully replaced with `TechDocsReaderPageRenderFunction` + - deprecated config `techdocs.requestUrl` is now deleted and fully replaced with the discoveryApi + +### Patch Changes + +- a422d7ce5e: chore(deps): bump `@testing-library/react` from 11.2.6 to 12.1.3 +- c689d7a94c: Switched to using `CatalogFilterLayout` from `@backstage/plugin-catalog-react`. +- f24ef7864e: Minor typo fixes +- 06af9e8d17: Long sidebars will no longer overflow the footer and will properly show a scrollbar when needed. +- Updated dependencies + - @backstage/core-components@0.9.2 + - @backstage/core-plugin-api@1.0.0 + - @backstage/integration-react@1.0.0 + - @backstage/plugin-catalog-react@1.0.0 + - @backstage/plugin-search@0.7.4 + - @backstage/catalog-model@1.0.0 + - @backstage/integration@1.0.0 + - @backstage/config@1.0.0 + - @backstage/errors@1.0.0 + +## 0.15.1 + +### Patch Changes + +- 7a1dbe6ce9: The panels of `TechDocsCustomHome` now use the `useEntityOwnership` hook to resolve ownership when the `'ownedByUser'` filter predicate is used. +- Updated dependencies + - @backstage/plugin-catalog@0.10.0 + - @backstage/plugin-catalog-react@0.9.0 + - @backstage/core-components@0.9.1 + - @backstage/catalog-model@0.13.0 + - @backstage/plugin-search@0.7.3 + - @backstage/integration-react@0.1.25 + +## 0.15.1-next.0 + +### Patch Changes + +- 7a1dbe6ce9: The panels of `TechDocsCustomHome` now use the `useEntityOwnership` hook to resolve ownership when the `'ownedByUser'` filter predicate is used. +- Updated dependencies + - @backstage/plugin-catalog@0.10.0-next.0 + - @backstage/plugin-catalog-react@0.9.0-next.0 + - @backstage/core-components@0.9.1-next.0 + - @backstage/catalog-model@0.13.0-next.0 + - @backstage/plugin-search@0.7.3-next.0 + - @backstage/integration-react@0.1.25-next.0 + +## 0.15.0 + +### Minor Changes + +- ee3d6c6f10: **BREAKING:** + Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. + + Use as following: + + ```tsx + DocsTable.columns.createNameColumn(); + DocsTable.columns.createOwnerColumn(); + DocsTable.columns.createTypeColumn(); + + DocsTable.actions.createCopyDocsUrlAction(); + DocsTable.actions.createStarEntityAction(); + ``` + + - Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. + + - `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. + + - `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- c5fda066b1: Collapse techdocs sidebar on small devices +- f590d1681b: Removed usage of deprecated favorite utility methods. +- 5b0f9a75fa: Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). +- 0c3ba547a6: Show feedback when copying code snippet to clipboard. +- 0ca964ee0e: Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + +## 0.14.0 + +### Minor Changes + +- 2262fe19c9: **BREAKING**: Removed support for passing in an explicit `entity` prop to entity page extensions, which has been deprecated for a long time. This is only a breaking change at the TypeScript level, as this property was already ignored. +- 4faae902eb: Adjust the Tech Docs page theme as a side effect of the `mkdocs-material` theme update. + + If you use the `spofify/techdocs` image to build your documentation, make sure you use version `spotify/techdocs:v0.3.7`. + + **Breaking**: The `PyMdown` extensions have also been updated and some syntax may have changed, so it is recommended that you check the extension's documentation if something stops working. + For example, the syntax of tags below was deprecated in `PyMdown` extensions `v.7.0` and in `v.8.0.0` it has been removed. This means that the old syntax specified below no longer works. + + ````markdown + ```markdown tab="tab" + This is some markdown + ``` + + ```markdown tab="tab 2" + This is some markdown in tab 2 + ``` + ```` + +### Patch Changes + +- 3bbb4d98c6: Changed to use from createApp +- ed09ad8093: Updated usage of the `LocationSpec` type from `@backstage/catalog-model`, which is deprecated. +- b776ce5aab: Replaced use of deprecated `useEntityListProvider` hook with `useEntityList`. +- d4f67fa728: Removed import of deprecated hook. +- 45e1706328: Continuation of [#9569](https://github.com/backstage/backstage/pull/9569), fix Tech Docs Reader search position to be the same width as content. +- 919cf2f836: Minor updates to match the new `targetRef` field of relations, and to stop consuming the `target` field +- Updated dependencies + - @backstage/plugin-catalog@0.9.0 + - @backstage/core-components@0.8.10 + - @backstage/plugin-catalog-react@0.7.0 + - @backstage/catalog-model@0.11.0 + - @backstage/core-plugin-api@0.7.0 + - @backstage/integration@0.7.5 + - @backstage/plugin-search@0.7.1 + - @backstage/integration-react@0.1.23 + +## 0.13.4 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 6553985cd4: Match text size of admonitions to main content text size. +- 9df7b43e1a: Improve overall appearance of highlighted code in docs. +- Updated dependencies + - @backstage/core-components@0.8.9 + - @backstage/core-plugin-api@0.6.1 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/integration-react@0.1.22 + - @backstage/plugin-catalog@0.8.0 + - @backstage/plugin-catalog-react@0.6.15 + - @backstage/plugin-search@0.7.0 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/theme@0.2.15 + +## 0.13.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-search@0.6.2 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/plugin-catalog@0.7.12 + - @backstage/integration-react@0.1.21 + +## 0.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-search@0.6.2-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/integration-react@0.1.21-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + +## 0.13.2 + +### Patch Changes + +- 742434a6ba: Fixed a bug where links to files within a TechDocs site that use the `download` attribute would result in a 404 in cases where the TechDocs backend and Backstage frontend application are on the same host. +- 359c31e31d: Added support for documentation using the raw `` tag to point to relative resources like audio or video files. +- 18317a08db: Fixed a bug where copy-to-clipboard buttons were appended to unintended elements. +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/integration-react@0.1.20 + - @backstage/plugin-catalog@0.7.11 + - @backstage/plugin-search@0.6.1 + +## 0.13.2-next.1 + +### Patch Changes + +- 742434a6ba: Fixed a bug where links to files within a TechDocs site that use the `download` attribute would result in a 404 in cases where the TechDocs backend and Backstage frontend application are on the same host. +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/plugin-catalog@0.7.11-next.1 + +## 0.13.2-next.0 + +### Patch Changes + +- 359c31e31d: Added support for documentation using the raw `` tag to point to relative resources like audio or video files. +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + - @backstage/plugin-search@0.6.1-next.0 + +## 0.13.1 + +### Patch Changes + +- bdc53553eb: chore(deps): bump `react-text-truncate` from 0.16.0 to 0.17.0 +- a64f99f734: Code snippets now include a "copy to clipboard" button. +- Updated dependencies + - @backstage/core-components@0.8.6 + - @backstage/plugin-search@0.6.0 + - @backstage/plugin-catalog@0.7.10 + +## 0.13.0 + +### Minor Changes + +- aecfe4f403: Make `TechDocsClient` and `TechDocsStorageClient` use the `FetchApi`. You now + need to pass in an instance of that API when constructing the client, if you + create a custom instance in your app. + + If you are replacing the factory: + + ```diff + +import { fetchApiRef } from '@backstage/core-plugin-api'; + + createApiFactory({ + api: techdocsStorageApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + + fetchApi: fetchApiRef, + }, + factory: ({ + configApi, + discoveryApi, + identityApi, + + fetchApi, + }) => + new TechDocsStorageClient({ + configApi, + discoveryApi, + identityApi, + + fetchApi, + }), + }), + createApiFactory({ + api: techdocsApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + - identityApi: identityApiRef, + + fetchApi: fetchApiRef, + }, + factory: ({ + configApi, + discoveryApi, + - identityApi, + + fetchApi, + }) => + new TechDocsClient({ + configApi, + discoveryApi, + - identityApi, + + fetchApi, + }), + }), + ``` + + If instantiating directly: + + ```diff + +import { fetchApiRef } from '@backstage/core-plugin-api'; + + +const fetchApi = useApi(fetchApiRef); + const storageClient = new TechDocsStorageClient({ + configApi, + discoveryApi, + identityApi, + + fetchApi, + }); + const techdocsClient = new TechDocsClient({ + configApi, + discoveryApi, + - identityApi, + + fetchApi, + }), + ``` + +### Patch Changes + +- 51fbedc445: Migrated usage of deprecated `IdentityApi` methods. +- 29710c91c2: use lighter color for block quotes and horizontal rulers +- Updated dependencies + - @backstage/core-components@0.8.5 + - @backstage/integration@0.7.2 + - @backstage/plugin-search@0.5.6 + - @backstage/core-plugin-api@0.6.0 + - @backstage/plugin-catalog@0.7.9 + - @backstage/plugin-catalog-react@0.6.12 + - @backstage/config@0.1.13 + - @backstage/catalog-model@0.9.10 + - @backstage/integration-react@0.1.19 + +## 0.12.15-next.0 + +### Patch Changes + +- 51fbedc445: Migrated usage of deprecated `IdentityApi` methods. +- 29710c91c2: use lighter color for block quotes and horizontal rulers +- Updated dependencies + - @backstage/core-components@0.8.5-next.0 + - @backstage/core-plugin-api@0.6.0-next.0 + - @backstage/plugin-catalog@0.7.9-next.0 + - @backstage/config@0.1.13-next.0 + - @backstage/plugin-catalog-react@0.6.12-next.0 + - @backstage/plugin-search@0.5.6-next.0 + - @backstage/catalog-model@0.9.10-next.0 + - @backstage/integration-react@0.1.19-next.0 + - @backstage/integration@0.7.2-next.0 + +## 0.12.14 + +### Patch Changes + +- 5333451def: Cleaned up API exports +- 1628ca3f49: Fix an issue where the TechDocs sidebar is hidden when the Backstage sidebar is pinned at smaller screen sizes +- Updated dependencies + - @backstage/config@0.1.12 + - @backstage/integration@0.7.1 + - @backstage/core-components@0.8.4 + - @backstage/core-plugin-api@0.5.0 + - @backstage/plugin-catalog-react@0.6.11 + - @backstage/errors@0.2.0 + - @backstage/catalog-model@0.9.9 + - @backstage/integration-react@0.1.18 + - @backstage/plugin-catalog@0.7.8 + - @backstage/plugin-search@0.5.5 + +## 0.12.13 + +### Patch Changes + +- fe9de6c25b: Adds support for opening internal Techdocs links in a new tab with CTRL+Click or CMD+Click +- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead. +- e0271456d8: Updated Techdocs footer navigation to dynamically resize to the width of the dom, resolving an issue where a pinned sidebar causes navigation to go off of the screen +- Updated dependencies + - @backstage/plugin-search@0.5.4 + - @backstage/core-plugin-api@0.4.1 + - @backstage/plugin-catalog-react@0.6.10 + - @backstage/core-components@0.8.3 + - @backstage/plugin-catalog@0.7.7 + +## 0.12.12 + +### Patch Changes + +- aa8f764a3e: Add the techdocs.sanitizer.allowedIframeHosts config. + This config allows all iframes which have the host of the attribute src in the 'allowedIframehosts' list to be displayed in the documentation. +- Updated dependencies + - @backstage/plugin-search@0.5.3 + - @backstage/plugin-catalog@0.7.6 + - @backstage/plugin-catalog-react@0.6.9 + - @backstage/integration@0.7.0 + - @backstage/integration-react@0.1.17 + +## 0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search@0.5.2 + - @backstage/core-plugin-api@0.4.0 + - @backstage/plugin-catalog-react@0.6.8 + - @backstage/core-components@0.8.2 + - @backstage/plugin-catalog@0.7.5 + - @backstage/integration-react@0.1.16 + +## 0.12.10 + +### Patch Changes + +- e7cce2b603: Fix issue where assets weren't being fetched from the correct URL path for doc URLs without trailing slashes +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + +## 0.12.9 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- d90dad84b0: Switch to using `LogViewer` component from `@backstage/core-components` to display build logs. +- 3421826ca8: The problem of lowercase entity triplets which causes docs to not load on entity page is fixed. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog@0.7.4 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/plugin-search@0.5.1 + +## 0.12.8 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + - @backstage/plugin-search@0.5.0 + +## 0.12.7 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + + More information can be found here: + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + +## 0.12.6 + +### Patch Changes + +- a125278b81: Refactor out the deprecated path and icon from RouteRefs +- c1858c4cf9: Fixed entity triplet case handling for certain locales. +- f7703981a9: Use a better checkbox rendering in a task list. +- e266687580: Updates reader component used to display techdocs documentation. A previous change made this component not usable out of a page which don't have entityRef in url parameters. Reader component EntityRef parameter is now used instead of url parameters. Techdocs documentation component can now be used in our custom pages. +- Updated dependencies + - @backstage/plugin-catalog@0.7.3 + - @backstage/catalog-model@0.9.7 + - @backstage/plugin-catalog-react@0.6.4 + - @backstage/plugin-search@0.4.18 + - @backstage/core-components@0.7.4 + - @backstage/core-plugin-api@0.2.0 + - @backstage/integration-react@0.1.14 + +## 0.12.5 + +### Patch Changes + +- fe5738fe1c: Lazy load `LazyLog` as it is rarely used. +- 53c9ad7e04: Update font weight for headings in TechDocs +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/plugin-search@0.4.17 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.12.4 + +### Patch Changes + +- a9a8c6f7c5: Reader will now scroll to the top of the page when navigating between pages +- 106a5dc3ad: Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/integration-react@0.1.13 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/plugin-search@0.4.16 + - @backstage/core-plugin-api@0.1.12 + +## 0.12.3 + +### Patch Changes + +- ba5b75ed2f: Add `` as an alternative to `` that + shows a grid of card instead of table. + + Extend `` to display the entity title of the entity instead of the + name if available. + +- 177401b571: Display entity title (if defined) in titles of TechDocs search results + +- cdf8ca6111: Only replace the shadow dom if the content is changed to avoid a flickering UI. + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + - @backstage/plugin-catalog@0.7.2 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + +## 0.12.2 + +### Patch Changes + +- 76fef740fe: Refactored `` component internals to support future extensibility. +- Updated dependencies + - @backstage/plugin-catalog-react@0.6.0 + - @backstage/plugin-catalog@0.7.1 + - @backstage/integration@0.6.8 + - @backstage/core-components@0.7.0 + - @backstage/theme@0.2.11 + - @backstage/plugin-search@0.4.15 + - @backstage/integration-react@0.1.12 + +## 0.12.1 + +### Patch Changes + +- 81a41ec249: Added a `name` key to all extensions in order to improve Analytics API metadata. +- Updated dependencies + - @backstage/core-components@0.6.1 + - @backstage/core-plugin-api@0.1.10 + - @backstage/plugin-catalog@0.7.0 + - @backstage/plugin-catalog-react@0.5.2 + - @backstage/catalog-model@0.9.4 + - @backstage/integration@0.6.7 + - @backstage/plugin-search@0.4.14 + +## 0.12.0 + +### Minor Changes + +- 82bb0842a3: Adds support for being able to customize and compose your TechDocs reader page in the App. + + You can likely upgrade to this version without issue. If, however, you have + imported the `` component in your custom code, the name of a property + has changed. You will need to make the following change anywhere you use it: + + ```diff + - + + + ``` + +### Patch Changes + +- 79ebee7a6b: Add "data-testid" for e2e tests and fix techdocs entity not found error. +- 3df2e8532b: Fixed the URL for the "Click to copy documentation link to clipboard" action +- 0a8bec0877: Added a check for the TechDocs annotation on the entity +- Updated dependencies + - @backstage/integration@0.6.6 + - @backstage/core-plugin-api@0.1.9 + - @backstage/core-components@0.6.0 + - @backstage/integration-react@0.1.11 + - @backstage/plugin-catalog@0.6.17 + - @backstage/plugin-catalog-react@0.5.1 + - @backstage/plugin-search@0.4.13 + +## 0.11.3 + +### Patch Changes + +- be13dfe61a: Make techdocs context search bar width adjust on smaller screens. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/plugin-catalog@0.6.16 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + - @backstage/integration-react@0.1.10 + - @backstage/plugin-search@0.4.12 + +## 0.11.2 + +### Patch Changes + +- 1d346ba903: Modify TechDocsCollator to be aware of new TechDocs URL pattern. Modify tech docs in context search to use correct casing when creating initial filter. +- 9f1362dcc1: Upgrade `@material-ui/lab` to `4.0.0-alpha.57`. +- 96fef17a18: Upgrade git-parse-url to v11.6.0 +- Updated dependencies + - @backstage/core-components@0.4.2 + - @backstage/integration@0.6.4 + - @backstage/integration-react@0.1.9 + - @backstage/plugin-catalog@0.6.15 + - @backstage/plugin-catalog-react@0.4.6 + - @backstage/plugin-search@0.4.11 + - @backstage/core-plugin-api@0.1.8 + +## 0.11.1 + +### Patch Changes + +- 30ed662a3: Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. + + Adding extra information to indexed tech docs documents for search. + +- 434dfc5d4: Display [metadata.title](https://backstage.io/docs/features/software-catalog/descriptor-format#title-optional) for components on the TechDocs homepage, if defined; otherwise fall back to `metadata.name` as displayed before. + +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/integration@0.6.3 + - @backstage/core-components@0.4.0 + - @backstage/plugin-catalog@0.6.14 + - @backstage/plugin-search@0.4.9 + - @backstage/catalog-model@0.9.1 + - @backstage/integration-react@0.1.8 + +## 0.11.0 + +### Minor Changes + +- c772d9a84: TechDocs sites can now be accessed using paths containing entity triplets of + any case (e.g. `/docs/namespace/KIND/name` or `/docs/namespace/kind/name`). + + If you do not use an external storage provider for serving TechDocs, this is a + transparent change and no action is required from you. + + If you _do_ use an external storage provider for serving TechDocs (one of\* GCS, + AWS S3, or Azure Blob Storage), you must run a migration command against your + storage provider before updating. + + [A migration guide is available here](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta). + + - (\*) We're seeking help from the community to bring OpenStack Swift support + [to feature parity](https://github.com/backstage/backstage/issues/6763) with the above. + +- 787bc0826: The TechDocs plugin has completed the migration to the Composability API. In + order to update to this version, please ensure you've made all necessary + changes to your `App.tsx` file as outlined in the [create-app changelog][cacl]. + + [cacl]: https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md + +### Patch Changes + +- 90c68a2ca: Fix Techdocs feedback icon link for GitHub URLs +- Updated dependencies + - @backstage/plugin-catalog@0.6.13 + - @backstage/plugin-catalog-react@0.4.4 + - @backstage/core-components@0.3.3 + - @backstage/integration@0.6.2 + - @backstage/config@0.1.8 + +## 0.10.4 + +### Patch Changes + +- a440d3b38: Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags. + + You can migrate to the new UI view by making the following changes in your `App.tsx`: + + ```diff + - } /> + + }> + + + + + + } + + /> + ``` + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. + +- 8a3e46591: Switch `EventSource` implementation with header support from a Node.js API-based one to an XHR-based one. + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog@0.6.11 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + +## 0.10.3 + +### Patch Changes + +- 260c053b9: Fix All Material UI Warnings +- db58cf06c: Avoid sanitize safe links in the header of document pages. +- 1d65bd490: Fix Techdocs feedback icon link for GitLab URLs with subgroup(s) in path +- Updated dependencies + - @backstage/core-components@0.3.0 + - @backstage/config@0.1.6 + - @backstage/core-plugin-api@0.1.5 + - @backstage/integration@0.5.9 + - @backstage/integration-react@0.1.6 + - @backstage/plugin-catalog-react@0.4.1 + +## 0.10.2 + +### Patch Changes + +- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. + - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` + - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. + - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` + - `onChangePage` has been renamed to `onPageChange` + - Migration guide is here: +- 11c370af2: Optimize load times by only fetching entities with the `backstage.io/techdocs-ref` annotation +- 2b1ac002d: TechDocs now uses a "safe by default" sanitization library, rather than relying on its own, hard-coded list of allowable tags and attributes. +- Updated dependencies + - @backstage/core-components@0.2.0 + - @backstage/plugin-catalog-react@0.4.0 + - @backstage/core-plugin-api@0.1.4 + - @backstage/integration-react@0.1.5 + - @backstage/theme@0.2.9 + +## 0.10.1 + +### Patch Changes + +- 9266b80ab: Add search list item to display tech docs search results + +- 03bf17e9b: Improve the responsiveness of the EntityPage UI. With this the Header component should scale with the screen size & wrapping should not cause overflowing/blocking of links. Additionally enforce the Pages using the Grid Layout to use it across all screen sizes & to wrap as intended. + + To benefit from the improved responsive layout, the `EntityPage` in existing Backstage applications should be updated to set the `xs` column size on each grid item in the page, as this does not default. For example: + + ```diff + - + + + ``` + +- 378cc6a54: Only update the `path` when the content is updated. + If content and path are updated independently, the frontend rendering is triggered twice on each navigation: Once for the `path` change (with the old content) and once for the new content. + This might result in a flickering rendering that is caused by the async frontend preprocessing, and the fact that replacing the shadow dom content is expensive. + +- 214e7c52d: Refactor the techdocs transformers to return `Promise`s and await all transformations. + +- e35b13afa: Handle error responses in `getTechDocsMetadata` and `getEntityMetadata` such that `` doesn't throw errors. + +- Updated dependencies + - @backstage/core-components@0.1.6 + - @backstage/plugin-catalog-react@0.3.1 + +## 0.10.0 + +### Minor Changes + +- 94a54dd47: Added a `migrateDocsCase()` method to TechDocs publishers, along with + implementations for AWS, Azure, and GCS. + + This change is in support of a future update to TechDocs that will allow for + case-insensitive entity triplet URL access to documentation pages which will + require a migration of existing documentation objects in external storage + solutions. + + See [#4367](https://github.com/backstage/backstage/issues/4367) for details. + +### Patch Changes + +- 537c37b0f: Fix displaying owned documents list by fetching associated entity relations +- 136a91974: Show a "Refresh" button to if the content is stale. + This removes the need to do a full page-reload to display more recent TechDocs content. +- f1200f44c: Rewrite the `/sync/:namespace/:kind/:name` endpoint to support an event-stream as response. + This change allows the sync process to take longer than a normal HTTP timeout. + The stream also emits log events, so the caller can follow the build process in the frontend. +- 3af126cdd: Provide a Drawer component to follow a running build. + This can be used to debug the rendering and get build logs in case an error occurs. +- 2a4a3b32d: Techdocs: fix sidebars not adjusting position automatically +- Updated dependencies + - @backstage/plugin-catalog-react@0.3.0 + +## 0.9.9 + +### Patch Changes + +- 0172d3424: Fixed bug preventing scroll bar from showing up on code blocks in a TechDocs site. +- Updated dependencies + - @backstage/integration@0.5.8 + - @backstage/core-components@0.1.5 + - @backstage/catalog-model@0.9.0 + - @backstage/plugin-catalog-react@0.2.6 + +## 0.9.8 + +### Patch Changes + +- 99a2873c7: Include cookies when making fetch requests for SVG from techdocs plugin +- a444c7431: Filter fetched entity fields to optimize loading techdocs list +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.5 + - @backstage/core-components@0.1.4 + - @backstage/integration@0.5.7 + +## 0.9.7 + +### Patch Changes + +- aefd54da6: Fix the overlapping between the sidebar and the tabs navigation when enabled in mkdocs (features: navigation.tabs) +- 48c9fcd33: Migrated to use the new `@backstage/core-*` packages rather than `@backstage/core`. +- 1dfec7a2a: Refactor the implicit logic from `` into an explicit state machine. This resolves some state synchronization issues when content is refreshed or rebuilt in the backend. +- Updated dependencies + - @backstage/core-plugin-api@0.1.3 + - @backstage/catalog-model@0.8.4 + - @backstage/integration-react@0.1.4 + - @backstage/plugin-catalog-react@0.2.4 + +## 0.9.6 + +### Patch Changes + +- 938aee2fb: Fix the link to the documentation page when no owned documents are displayed +- 2e1fbe203: Do not add trailing slash for .html pages during doc links rewriting +- 9b57fda8b: Fixes a bug that could prevent some externally hosted images (like icons or + build badges) from rendering within TechDocs documentation. +- 667656c8b: Adding support for user owned document filter for TechDocs custom Homepage +- Updated dependencies + - @backstage/plugin-catalog-react@0.2.3 + - @backstage/catalog-model@0.8.3 + - @backstage/core@0.7.13 + +## 0.9.5 + +### Patch Changes + +- aad98c544: Fixes multiple XSS and sanitization bypass vulnerabilities in TechDocs. +- Updated dependencies [e7c5e4b30] +- Updated dependencies [ebe802bc4] +- Updated dependencies [49d7ec169] +- Updated dependencies [1cf1d351f] +- Updated dependencies [deaba2e13] +- Updated dependencies [8e919a6f8] + - @backstage/theme@0.2.8 + - @backstage/catalog-model@0.8.1 + - @backstage/integration@0.5.5 + - @backstage/core@0.7.12 + - @backstage/plugin-catalog-react@0.2.1 + +## 0.9.4 + +### Patch Changes + +- 4ea9df9d3: Set admonition font size to 1rem in TechDocs to align with the rest of the document's font sizes. + Fixes #5448 and #5541. +- bf805b467: Fixes #5529, a bug that prevented TechDocs from rendering pages containing malformed links. +- 203ce6f6f: TechDocs now respects the `download` attribute on anchor tags in generated + markup, allowing documentation authors to bundle downloadable files with their + documentation. +- Updated dependencies [0fd4ea443] +- Updated dependencies [add62a455] +- Updated dependencies [cc592248b] +- Updated dependencies [17c497b81] +- Updated dependencies [704875e26] + - @backstage/integration@0.5.4 + - @backstage/catalog-model@0.8.0 + - @backstage/core@0.7.11 + - @backstage/plugin-catalog-react@0.2.0 + +## 0.9.3 + +### Patch Changes + +- 65e6c4541: Remove circular dependencies +- a62cfe068: Bug fix on sidebar position when Tab-Bar is enabled +- 35e091604: Handle URLs with a `#hash` correctly when rewriting link URLs. +- Updated dependencies [f7f7783a3] +- Updated dependencies [65e6c4541] +- Updated dependencies [68fdbf014] +- Updated dependencies [5da6a561d] + - @backstage/catalog-model@0.7.10 + - @backstage/core@0.7.10 + - @backstage/integration@0.5.3 + +## 0.9.2 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + +## 0.9.1 + +### Patch Changes + +- 2e05277e0: Fix navigation in a page using the table of contents. +- 4075c6367: Make git config optional for techdocs feedback links +- Updated dependencies [38ca05168] +- Updated dependencies [f65adcde7] +- Updated dependencies [81c54d1f2] +- Updated dependencies [80888659b] +- Updated dependencies [7b8272fb7] +- Updated dependencies [d8b81fd28] + - @backstage/integration@0.5.2 + - @backstage/core@0.7.8 + - @backstage/plugin-catalog-react@0.1.5 + - @backstage/theme@0.2.7 + - @backstage/catalog-model@0.7.8 + - @backstage/config@0.1.5 + +## 0.9.0 + +### Minor Changes + +- 21fddf452: Make `techdocsStorageApiRef` and `techdocsApiRef` use interfaces instead of the + actual implementation classes. + + This renames the classes `TechDocsApi` to `TechDocsClient` and `TechDocsStorageApi` + to `TechDocsStorageClient` and renames the interfaces `TechDocs` to `TechDocsApi` + and `TechDocsStorage` to `TechDocsStorageApi` to comply the pattern elsewhere in + the project. This also fixes the types returned by some methods on those + interfaces. + +### Patch Changes + +- 6fbd7beca: Use `EntityRefLink` in header and use relations to reference the owner of the + document. +- 15cbe6815: Fix TechDocs landing page table wrong copied link +- 39bdaa004: Add customization and exportable components for TechDocs landing page +- cb8c848a3: Disable color transitions on links to avoid issues in dark mode. +- 17915e29b: Rework state management to avoid rendering multiple while navigating between pages. +- Updated dependencies [9afcac5af] +- Updated dependencies [e0c9ed759] +- Updated dependencies [6eaecbd81] + - @backstage/core@0.7.7 + +## 0.8.0 + +### Minor Changes + +- ac6025f63: Add feedback link icon in Techdocs Reader that directs to GitLab or GitHub repo issue page with pre-filled title and source link. + For link to appear, requires `repo_url` and `edit_uri` to be filled in mkdocs.yml, as per . An `edit_uri` will need to be specified for self-hosted GitLab/GitHub instances with a different host name. + To identify issue URL format as GitHub or GitLab, the host name of source in `repo_url` is checked if it contains `gitlab` or `github`. Alternately this is determined by matching to `host` values from `integrations` in app-config.yaml. + +### Patch Changes + +- e292e393f: Add a test id to the shadow root element of the Reader to access it easily in e2e tests +- Updated dependencies [94da20976] +- Updated dependencies [d8cc7e67a] +- Updated dependencies [99fbef232] +- Updated dependencies [ab07d77f6] +- Updated dependencies [931b21a12] +- Updated dependencies [937ed39ce] +- Updated dependencies [9a9e7a42f] +- Updated dependencies [50ce875a0] + - @backstage/core@0.7.6 + - @backstage/theme@0.2.6 + +## 0.7.2 + +### Patch Changes + +- fef852ecd: Reworked the TechDocs plugin to support using the configured company name instead of + 'Backstage' in the page title. +- 18f7345a6: Add borders to TechDocs tables and increase font size. Fixes #5264 and #5276. +- Updated dependencies [bb5055aee] +- Updated dependencies [d0d1c2f7b] +- Updated dependencies [5d0740563] +- Updated dependencies [5cafcf452] +- Updated dependencies [86a95ba67] +- Updated dependencies [e27cb6c45] + - @backstage/catalog-model@0.7.7 + - @backstage/core@0.7.5 + +## 0.7.1 + +### Patch Changes + +- bebd1c4fe: Remove the `@backstage/techdocs-common` dependency to not pull in backend config schemas in the frontend. +- Updated dependencies [9f48b548c] +- Updated dependencies [8488a1a96] + - @backstage/plugin-catalog-react@0.1.4 + - @backstage/catalog-model@0.7.5 + +## 0.7.0 + +### Minor Changes + +- aaeb7ecf3: When newer documentation available but not built, show older documentation while async building newer + TechDocs backend: /sync endpoint added to support above, returns immediate success if docs don't need a build, returns delayed success after build if needed + TechDocs backend: /docs endpoint removed as frontend can directly request to techdocs.storageUrl or /static/docs +- 3139f83af: Add sticky sidebars and footer navigation links to TechDocs Reader + +### Patch Changes + +- ea9d977e7: Introduce workaround for admonition icons of MkDocs. +- 2aab54319: TechDocs: links at sidebar and bottom reset scroll position to top +- Updated dependencies [01ccef4c7] +- Updated dependencies [fcc3ada24] +- Updated dependencies [4618774ff] +- Updated dependencies [df59930b3] + - @backstage/plugin-catalog-react@0.1.3 + - @backstage/core@0.7.3 + - @backstage/theme@0.2.5 + +## 0.6.2 + +### Patch Changes + +- 83bfc98a3: On TechDocs page header, change the breadcrumbs link to be static and point to TechDocs homepage. +- e7baa0d2e: Separate techdocs-backend and frontend config schema declarations +- c8b54c370: Extended TechDocs HomePage with owned documents +- Updated dependencies [0434853a5] +- Updated dependencies [8686eb38c] +- Updated dependencies [9ca0e4009] +- Updated dependencies [34ff49b0f] +- Updated dependencies [8686eb38c] +- Updated dependencies [424742dc1] +- Updated dependencies [4e0b5055a] + - @backstage/config@0.1.4 + - @backstage/core@0.7.2 + - @backstage/plugin-catalog-react@0.1.2 + - @backstage/techdocs-common@0.4.5 + - @backstage/test-utils@0.1.9 + +## 0.6.1 + +### Patch Changes + +- aa095e469: OpenStack Swift publisher added for tech-docs. +- 2089de76b: Make use of the new core `ItemCardGrid` and `ItemCardHeader` instead of the deprecated `ItemCard`. +- 868e4cdf2: - Adds a link to the owner entity + - Corrects the link to the component which includes the namespace +- ca4a904f6: Add an optional configuration option for setting the url endpoint for AWS S3 publisher: `techdocs.publisher.awsS3.endpoint` +- Updated dependencies [d7245b733] +- Updated dependencies [0b42fff22] +- Updated dependencies [0b42fff22] +- Updated dependencies [2ef5bc7ea] +- Updated dependencies [ff4d666ab] +- Updated dependencies [aa095e469] +- Updated dependencies [2089de76b] +- Updated dependencies [dc1fc92c8] +- Updated dependencies [bc46435f5] +- Updated dependencies [a501128db] +- Updated dependencies [ca4a904f6] + - @backstage/techdocs-common@0.4.4 + - @backstage/catalog-model@0.7.4 + - @backstage/core@0.7.1 + - @backstage/theme@0.2.4 + +## 0.6.0 + +### Minor Changes + +- 813c6a4f2: Add authorization header on techdocs api requests. Breaking change as clients now needs the Identity API. + +### Patch Changes + +- Updated dependencies [12d8f27a6] +- Updated dependencies [f43192207] +- Updated dependencies [40c0fdbaa] +- Updated dependencies [2a271d89e] +- Updated dependencies [bece09057] +- Updated dependencies [169f48deb] +- Updated dependencies [8a1566719] +- Updated dependencies [9d455f69a] +- Updated dependencies [4c049a1a1] +- Updated dependencies [02816ecd7] +- Updated dependencies [61299519f] + - @backstage/catalog-model@0.7.3 + - @backstage/techdocs-common@0.4.3 + - @backstage/core@0.7.0 + - @backstage/plugin-catalog-react@0.1.1 + +## 0.5.8 + +### Patch Changes + +- f37992797: Got rid of some `attr` and cleaned up a bit in the TechDocs config schema. +- 2499f6cde: Add support for assuming role in AWS integrations +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [dc12852c9] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2499f6cde] +- Updated dependencies [a1f5e6545] +- Updated dependencies [1e4ddd71d] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/test-utils@0.1.8 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + - @backstage/techdocs-common@0.4.2 + - @backstage/config@0.1.3 + +## 0.5.7 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [fb28da212] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [26e143e60] +- Updated dependencies [c6655413d] +- Updated dependencies [44414239f] +- Updated dependencies [b0a41c707] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/techdocs-common@0.4.1 + - @backstage/plugin-catalog-react@0.0.4 + +## 0.5.6 + +### Patch Changes + +- f5e564cd6: Improve display of error messages +- 41af18227: Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. +- 8f3443427: Enhance API calls to support trapping 500 errors from techdocs-backend +- Updated dependencies [77ad0003a] +- Updated dependencies [b51ee6ece] +- Updated dependencies [19d354c78] +- Updated dependencies [08142b256] +- Updated dependencies [08142b256] +- Updated dependencies [b51ee6ece] + - @backstage/techdocs-common@0.4.0 + - @backstage/test-utils@0.1.7 + - @backstage/plugin-catalog-react@0.0.3 + - @backstage/core@0.6.1 + +## 0.5.5 + +### Patch Changes + +- 5fa3bdb55: Add `href` in addition to `onClick` to `ItemCard`. Ensure that the height of a + `ItemCard` with and without tags is equal. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [c777df180] +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [f0320190d] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/techdocs-common@0.3.7 + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + +## 0.5.4 + +### Patch Changes + +- a5e27d5c1: Create type for TechDocsMetadata (#3716) + + This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend). + +- Updated dependencies [def2307f3] + +- Updated dependencies [efd6ef753] + +- Updated dependencies [593632f07] + +- Updated dependencies [33846acfc] + +- Updated dependencies [a187b8ad0] + +- Updated dependencies [f04db53d7] + +- Updated dependencies [53c9c51f2] + +- Updated dependencies [a5e27d5c1] + +- Updated dependencies [a93f42213] + - @backstage/catalog-model@0.7.0 + - @backstage/core@0.5.0 + - @backstage/plugin-catalog@0.2.12 + - @backstage/techdocs-common@0.3.5 + +## 0.5.3 + +### Patch Changes + +- dbe4450c3: Google Cloud authentication in TechDocs has been improved. + + 1. `techdocs.publisher.googleGcs.credentials` is now optional. If it is missing, `GOOGLE_APPLICATION_CREDENTIALS` + environment variable (and some other methods) will be used to authenticate. + Read more here + + 2. `techdocs.publisher.googleGcs.projectId` is no longer used. You can remove it from your `app-config.yaml`. + +- a6f9dca0d: Remove dependency on `@backstage/core-api`. No plugin should ever depend on that package; it's an internal concern whose important bits are re-exported by `@backstage/core` which is the public facing dependency to use. + +- b3b9445df: AWS S3 authentication in TechDocs has been improved. + + 1. `techdocs.publisher.awsS3.bucketName` is now the only required config. `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are optional. + + 2. If `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are missing, the AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` will be used. There are more better ways of setting up AWS authentication. Read the guide at + +- e5d12f705: Use `history.pushState` for hash link navigation. + +- Updated dependencies [68ad5af51] + +- Updated dependencies [f3b064e1c] + +- Updated dependencies [371f67ecd] + +- Updated dependencies [f1e74777a] + +- Updated dependencies [dbe4450c3] + +- Updated dependencies [c00488983] + +- Updated dependencies [265a7ab30] + +- Updated dependencies [5826d0973] + +- Updated dependencies [b3b9445df] + +- Updated dependencies [abbee6fff] + +- Updated dependencies [147fadcb9] + - @backstage/techdocs-common@0.3.3 + - @backstage/catalog-model@0.6.1 + - @backstage/plugin-catalog@0.2.11 + - @backstage/core@0.4.4 + +## 0.5.2 + +### Patch Changes + +- 359f9d2d8: Added configuration schema for the commonly used properties of techdocs and techdocs-backend plugins +- Updated dependencies [a08c32ced] +- Updated dependencies [7e0b8cac5] +- Updated dependencies [8804e8981] +- Updated dependencies [87c0c53c2] +- Updated dependencies [86c3c652a] +- Updated dependencies [27f2af935] + - @backstage/core-api@0.2.8 + - @backstage/core@0.4.3 + - @backstage/plugin-catalog@0.2.9 + - @backstage/techdocs-common@0.3.1 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [d681db2b5] +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] +- Updated dependencies [1dc445e89] +- Updated dependencies [a8573e53b] + - @backstage/core-api@0.2.7 + - @backstage/core@0.4.2 + - @backstage/test-utils@0.1.6 + - @backstage/plugin-catalog@0.2.8 + - @backstage/techdocs-common@0.3.0 + +## 0.5.0 + +### Minor Changes + +- dae4f3983: _Breaking changes_ + + 1. Added option to use Google Cloud Storage as a choice to store the static generated files for TechDocs. + It can be configured using `techdocs.publisher.type` option in `app-config.yaml`. + Step-by-step guide to configure GCS is available here + Set `techdocs.publisher.type` to `'local'` if you want to continue using local filesystem to store TechDocs files. + + 2. `techdocs.builder` is now required and can be set to `'local'` or `'external'`. (Set it to `'local'` for now, since CI/CD build + workflow for TechDocs will be available soon (in few weeks)). + 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. + If builder is set to `'external'`, `techdocs-backend` will only fetch the docs and will NOT try to generate and publish. In this case of `'external'`, + we assume that docs are being built in the CI/CD pipeline of the repository. + TechDocs will not assume a default value for `techdocs.builder`. It is better to explicitly define it in the `app-config.yaml`. + + 3. When configuring TechDocs in your backend, there is a difference in how a new publisher is created. + + --- const publisher = new LocalPublish(logger, discovery); + +++ const publisher = Publisher.fromConfig(config, logger, discovery); + + Based on the config `techdocs.publisher.type`, the publisher could be either Local publisher or Google Cloud Storage publisher. + + 4. `techdocs.storageUrl` is now a required config. Should be `http://localhost:7000/api/techdocs/static/docs` in most setups. + + 5. Parts of `@backstage/plugin-techdocs-backend` have been moved to a new package `@backstage/techdocs-common` to generate docs. Also to publish docs + to-and-fro between TechDocs and a storage (either local or external). However, a Backstage app does NOT need to import the `techdocs-common` package - + app should only import `@backstage/plugin-techdocs` and `@backstage/plugin-techdocs-backend`. + + _Patch changes_ + + 1. See all of TechDocs config options and its documentation + + 2. Logic about serving static files and metadata retrieval have been abstracted away from the router in `techdocs-backend` to the instance of publisher. + + 3. Removed Material UI Spinner from TechDocs header. Spinners cause unnecessary UX distraction. + Case 1 (when docs are built and are to be served): Spinners appear for a split second before the name of site shows up. This unnecessarily distracts eyes because spinners increase the size of the Header. A dot (.) would do fine. Definitely more can be done. + Case 2 (when docs are being generated): There is already a linear progress bar (which is recommended in Storybook). + +### Patch Changes + +- Updated dependencies [c911061b7] +- Updated dependencies [dae4f3983] +- Updated dependencies [8ef71ed32] +- Updated dependencies [0e6298f7e] +- Updated dependencies [7dd2ef7d1] +- Updated dependencies [ac3560b42] + - @backstage/catalog-model@0.6.0 + - @backstage/techdocs-common@0.2.0 + - @backstage/core@0.4.1 + - @backstage/core-api@0.2.6 + - @backstage/plugin-catalog@0.2.7 + +## 0.4.0 + +### Minor Changes + +- 87a33d2fe: Removed modifyCss transformer and moved the css to injectCss transformer + Fixed issue where some internal doc links would cause a reload of the page + +### Patch Changes + +- Updated dependencies [b6557c098] +- Updated dependencies [2527628e1] +- Updated dependencies [6011b7d3e] +- Updated dependencies [e1f4e24ef] +- Updated dependencies [1c69d4716] +- Updated dependencies [d8d5a17da] +- Updated dependencies [83b6e0c1f] +- Updated dependencies [1665ae8bb] +- Updated dependencies [04f26f88d] +- Updated dependencies [ff243ce96] + - @backstage/core-api@0.2.5 + - @backstage/core@0.4.0 + - @backstage/plugin-catalog@0.2.6 + - @backstage/test-utils@0.1.5 + - @backstage/catalog-model@0.5.0 + - @backstage/theme@0.2.2 + +## 0.3.1 + +### Patch Changes + +- da2ad65cb: Use type EntityName from catalog-model for entities +- Updated dependencies [b4488ddb0] +- Updated dependencies [08835a61d] +- Updated dependencies [a9fd599f7] +- Updated dependencies [bcc211a08] +- Updated dependencies [ebf37bbae] + - @backstage/core-api@0.2.4 + - @backstage/catalog-model@0.4.0 + - @backstage/plugin-catalog@0.2.5 + - @backstage/test-utils@0.1.4 + +## 0.3.0 + +### Minor Changes + +- 4b53294a6: - Use techdocs annotation to add repo_url if missing in mkdocs.yml. Having repo_url creates a Edit button on techdocs pages. + - techdocs-backend: API endpoint `/metadata/mkdocs/*` renamed to `/metadata/techdocs/*` + +### Patch Changes + +- Updated dependencies [6f70ed7a9] +- Updated dependencies [ab94c9542] +- Updated dependencies [2daf18e80] +- Updated dependencies [069cda35f] +- Updated dependencies [700a212b4] + - @backstage/plugin-catalog@0.2.4 + - @backstage/catalog-model@0.3.1 + - @backstage/core-api@0.2.3 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + +## 0.2.2 + +### Patch Changes + +- 1722cb53c: Added configuration schema +- Updated dependencies [1722cb53c] +- Updated dependencies [8b7737d0b] + - @backstage/core@0.3.1 + - @backstage/plugin-catalog@0.2.2 + - @backstage/test-utils@0.1.3 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [c5bab94ab] +- Updated dependencies [7b37d65fd] +- Updated dependencies [4aca74e08] +- Updated dependencies [e8f69ba93] +- Updated dependencies [0c0798f08] +- Updated dependencies [0c0798f08] +- Updated dependencies [199237d2f] +- Updated dependencies [6627b626f] +- Updated dependencies [4577e377b] +- Updated dependencies [2d0bd1be7] + - @backstage/core-api@0.2.1 + - @backstage/core@0.3.0 + - @backstage/theme@0.2.1 + - @backstage/plugin-catalog@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 28edd7d29: Create backend plugin through CLI +- 8351ad79b: Add a message if techdocs takes long time to load + + Fixes #2416. + + The UI after the change should look like this: + + ![techdocs-progress-bar](https://user-images.githubusercontent.com/33940798/94189286-296ac980-fec8-11ea-9051-1b3db938d12f.gif) + +### Patch Changes + +- 782f3b354: add test case for Progress component +- 57b54c8ed: While techdocs fetches site name and metadata for the component, the page title was displayed as '[object Object] | Backstage'. This has now been fixed to display the component ID if site name is not present or being fetched. +- Updated dependencies [28edd7d29] +- Updated dependencies [819a70229] +- Updated dependencies [3a4236570] +- Updated dependencies [ae5983387] +- Updated dependencies [0d4459c08] +- Updated dependencies [cbbd271c4] +- Updated dependencies [482b6313d] +- Updated dependencies [e0be86b6f] +- Updated dependencies [f70a52868] +- Updated dependencies [12b5fe940] +- Updated dependencies [368fd8243] +- Updated dependencies [1c60f716e] +- Updated dependencies [144c66d50] +- Updated dependencies [a768a07fb] +- Updated dependencies [b79017fd3] +- Updated dependencies [6d97d2d6f] +- Updated dependencies [5adfc005e] +- Updated dependencies [f0aa01bcc] +- Updated dependencies [0aecfded0] +- Updated dependencies [93a3fa3ae] +- Updated dependencies [782f3b354] +- Updated dependencies [8b9c8196f] +- Updated dependencies [2713f28f4] +- Updated dependencies [406015b0d] +- Updated dependencies [82759d3e4] +- Updated dependencies [60d40892c] +- Updated dependencies [ac8d5d5c7] +- Updated dependencies [2ebcfac8d] +- Updated dependencies [fa56f4615] +- Updated dependencies [ebca83d48] +- Updated dependencies [aca79334f] +- Updated dependencies [c0d5242a0] +- Updated dependencies [b3d57961c] +- Updated dependencies [0b956f21b] +- Updated dependencies [26e69ab1a] +- Updated dependencies [97c2cb19b] +- Updated dependencies [3beb5c9fc] +- Updated dependencies [cbab5bbf8] +- Updated dependencies [754e31db5] +- Updated dependencies [1611c6dbc] + - @backstage/plugin-catalog@0.2.0 + - @backstage/core-api@0.2.0 + - @backstage/core@0.2.0 + - @backstage/catalog-model@0.2.0 + - @backstage/theme@0.2.0 + - @backstage/test-utils@0.1.2 + +## @backstage/ui@0.9.0-next.1 + +### Minor Changes + +- 5c614ff: **BREAKING**: Migrated Checkbox component from Base UI to React Aria Components. + + API changes required: + + - `checked` → `isSelected` + - `defaultChecked` → `defaultSelected` + - `disabled` → `isDisabled` + - `required` → `isRequired` + - `label` prop removed - use `children` instead + - CSS: `bui-CheckboxLabel` class removed + - Data attribute: `data-checked` → `data-selected` + - Use without label is no longer supported + + Migration examples: + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accept terms + + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + Option + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accessible label + + ``` + +- b78fc45: **BREAKING**: Changed className prop behavior to augment default styles instead of being ignored or overriding them. + + Affected components: + + - Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator + - Switch + - Skeleton + - FieldLabel + - Header, HeaderToolbar + - HeaderPage + - Tabs, TabList, Tab, TabPanel + + If you were passing custom className values to any of these components that relied on the previous behavior, you may need to adjust your styles to account for the default classes now being applied alongside your custom classes. + +### Patch Changes + +- ff9f0c3: Enable tree-shaking of imports other than `*.css`. +- 1ef3ca4: Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. + +## @backstage/plugin-notifications-backend-module-email@0.3.16-next.0 + +# @backstage/plugin-notifications-backend-module-email + +## 0.3.15-next.0 + +### Patch Changes + +- 22a5362: Updated `AWS SES` client to version 2 to support `nodemailer` version 7. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.21-next.0 + - @backstage/config@1.3.6-next.0 + - @backstage/catalog-model@1.7.6-next.0 + - @backstage/integration-aws-node@0.1.19-next.0 + - @backstage/backend-plugin-api@1.4.5-next.0 + - @backstage/catalog-client@1.12.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.19.2-next.0 + - @backstage/plugin-notifications-common@0.1.2-next.0 + +## 0.3.14 + +### Patch Changes + +- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 +- f5e0963: Removed unused dependencies +- Updated dependencies + - @backstage/config@1.3.5 + - @backstage/backend-plugin-api@1.4.4 + - @backstage/integration-aws-node@0.1.18 + - @backstage/plugin-catalog-node@1.19.1 + - @backstage/plugin-notifications-common@0.1.1 + - @backstage/plugin-notifications-node@0.2.20 + +## 0.3.14-next.1 + +### Patch Changes + +- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 +- f5e0963: Removed unused dependencies + +## 0.3.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.4-next.0 + - @backstage/integration-aws-node@0.1.18-next.0 + - @backstage/backend-plugin-api@1.4.4-next.0 + - @backstage/plugin-notifications-common@0.1.1-next.0 + - @backstage/plugin-catalog-node@1.19.1-next.0 + - @backstage/plugin-notifications-node@0.2.20-next.0 + - @backstage/catalog-client@1.12.0 + +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3-next.0 + - @backstage/plugin-catalog-node@1.18.1-next.0 + - @backstage/plugin-notifications-node@0.2.19-next.0 + +## 0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.11.0 + - @backstage/plugin-catalog-node@1.18.0 + - @backstage/plugin-notifications-common@0.1.0 + - @backstage/plugin-notifications-node@0.2.18 + - @backstage/backend-plugin-api@1.4.2 + +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.11.0-next.0 + - @backstage/plugin-catalog-node@1.18.0-next.0 + - @backstage/plugin-notifications-node@0.2.18-next.0 + - @backstage/backend-plugin-api@1.4.2-next.0 + - @backstage/catalog-model@1.7.5 + - @backstage/config@1.3.3 + - @backstage/integration-aws-node@0.1.17 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.10 + +## 0.3.11 + +### Patch Changes + +- f92c9fc: Add optional config for `ses` mail options with `sourceArn`, `fromArn`, `configurationSetName` +- Updated dependencies + - @backstage/config@1.3.3 + - @backstage/catalog-model@1.7.5 + - @backstage/catalog-client@1.10.2 + - @backstage/backend-plugin-api@1.4.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-catalog-node@1.17.2 + - @backstage/plugin-notifications-common@0.0.10 + - @backstage/plugin-notifications-node@0.2.17 + +## 0.3.11-next.1 + +### Patch Changes + +- f92c9fc: Add optional config for `ses` mail options with `sourceArn`, `fromArn`, `configurationSetName` + +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.3-next.0 + - @backstage/catalog-model@1.7.5-next.0 + - @backstage/catalog-client@1.10.2-next.0 + - @backstage/integration-aws-node@0.1.17-next.0 + - @backstage/backend-plugin-api@1.4.1-next.0 + - @backstage/plugin-notifications-common@0.0.10-next.0 + - @backstage/plugin-catalog-node@1.17.2-next.0 + - @backstage/plugin-notifications-node@0.2.17-next.0 + +## 0.3.10 + +### Patch Changes + +- 8a150bf: Internal changes to switch to the non-alpha `catalogServiceRef` +- Updated dependencies + - @backstage/catalog-client@1.10.1 + - @backstage/plugin-notifications-common@0.0.9 + - @backstage/plugin-catalog-node@1.17.1 + - @backstage/backend-plugin-api@1.4.0 + - @backstage/plugin-notifications-node@0.2.16 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.16 + - @backstage/types@1.2.1 + +## 0.3.10-next.2 + +### Patch Changes + +- 8a150bf: Internal changes to switch to the non-alpha `catalogServiceRef` +- Updated dependencies + - @backstage/backend-plugin-api@1.4.0-next.1 + - @backstage/catalog-client@1.10.1-next.0 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.16 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.17.1-next.1 + - @backstage/plugin-notifications-common@0.0.9-next.0 + - @backstage/plugin-notifications-node@0.2.16-next.1 + +## 0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.10.1-next.0 + - @backstage/plugin-notifications-common@0.0.9-next.0 + - @backstage/plugin-catalog-node@1.17.1-next.1 + - @backstage/plugin-notifications-node@0.2.16-next.1 + - @backstage/backend-plugin-api@1.4.0-next.1 + - @backstage/catalog-model@1.7.4 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.16 + - @backstage/types@1.2.1 + +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.0-next.0 + - @backstage/plugin-catalog-node@1.17.1-next.0 + - @backstage/plugin-notifications-node@0.2.16-next.0 + +## 0.3.9 + +### Patch Changes + +- aa3a63a: Enable the ability to configure the endpoint for the SES connection used in the notifications email module. This enables the configuration of alternate endpoints as required, for example for local testing or alternative stacks. +- Updated dependencies + - @backstage/catalog-model@1.7.4 + - @backstage/plugin-catalog-node@1.17.0 + - @backstage/backend-plugin-api@1.3.1 + - @backstage/integration-aws-node@0.1.16 + - @backstage/catalog-client@1.10.0 + - @backstage/config@1.3.2 + - @backstage/plugin-notifications-node@0.2.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + +## 0.3.9-next.3 + +### Patch Changes + +- aa3a63a: Enable the ability to configure the endpoint for the SES connection used in the notifications email module. This enables the configuration of alternate endpoints as required, for example for local testing or alternative stacks. +- Updated dependencies + - @backstage/backend-plugin-api@1.3.1-next.2 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.16-next.0 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.17.0-next.2 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.15-next.2 + +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-aws-node@0.1.16-next.0 + - @backstage/config@1.3.2 + - @backstage/plugin-notifications-node@0.2.15-next.1 + - @backstage/backend-plugin-api@1.3.1-next.1 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.17.0-next.1 + - @backstage/plugin-notifications-common@0.0.8 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.17.0-next.1 + - @backstage/backend-plugin-api@1.3.1-next.1 + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.15-next.1 + +## 0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.10.0-next.0 + - @backstage/plugin-catalog-node@1.17.0-next.0 + - @backstage/backend-plugin-api@1.3.1-next.0 + - @backstage/plugin-notifications-node@0.2.15-next.0 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.16.3 + - @backstage/backend-plugin-api@1.3.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.14 + +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.16.3-next.0 + - @backstage/backend-plugin-api@1.2.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.13 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-aws-node@0.1.15 + - @backstage/backend-plugin-api@1.2.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.16.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.13 + +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.2.1-next.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.16.1-next.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.13-next.2 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.2.1-next.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.16.1-next.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.13-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.2.1-next.0 + - @backstage/plugin-catalog-node@1.16.1-next.0 + - @backstage/plugin-notifications-node@0.2.13-next.0 + +## 0.3.6 + +### Patch Changes + +- 6259aa9: Add transport for Azure Communication Service +- Updated dependencies + - @backstage/backend-plugin-api@1.2.0 + - @backstage/plugin-catalog-node@1.16.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.12 + +## 0.3.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.16.0-next.3 + - @backstage/backend-plugin-api@1.2.0-next.2 + - @backstage/plugin-notifications-node@0.2.12-next.2 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + +## 0.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.2.0-next.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-catalog-node@1.16.0-next.2 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.12-next.1 + +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.16.0-next.1 + - @backstage/backend-plugin-api@1.2.0-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.12-next.0 + +## 0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.15.2-next.0 + - @backstage/backend-plugin-api@1.2.0-next.0 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/types@1.2.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.12-next.0 + +## 0.3.5 + +### Patch Changes + +- bed5f35: Added more examples of the plugin configuration +- Updated dependencies + - @backstage/types@1.2.1 + - @backstage/backend-plugin-api@1.1.1 + - @backstage/catalog-client@1.9.1 + - @backstage/catalog-model@1.7.3 + - @backstage/config@1.3.2 + - @backstage/integration-aws-node@0.1.15 + - @backstage/plugin-catalog-node@1.15.1 + - @backstage/plugin-notifications-common@0.0.8 + - @backstage/plugin-notifications-node@0.2.11 + +## 0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1-next.0 + - @backstage/backend-plugin-api@1.1.1-next.1 + - @backstage/catalog-model@1.7.3-next.0 + - @backstage/config@1.3.2-next.0 + - @backstage/plugin-catalog-node@1.15.1-next.1 + - @backstage/integration-aws-node@0.1.15-next.0 + - @backstage/plugin-notifications-node@0.2.11-next.1 + - @backstage/catalog-client@1.9.1-next.0 + - @backstage/plugin-notifications-common@0.0.8-next.0 + +## 0.3.5-next.0 + +### Patch Changes + +- bed5f35: Added more examples of the plugin configuration +- Updated dependencies + - @backstage/backend-plugin-api@1.1.1-next.0 + - @backstage/catalog-client@1.9.0 + - @backstage/catalog-model@1.7.2 + - @backstage/config@1.3.1 + - @backstage/integration-aws-node@0.1.14 + - @backstage/types@1.2.0 + - @backstage/plugin-catalog-node@1.15.1-next.0 + - @backstage/plugin-notifications-common@0.0.7 + - @backstage/plugin-notifications-node@0.2.11-next.0 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.1.0 + - @backstage/plugin-catalog-node@1.15.0 + - @backstage/catalog-client@1.9.0 + - @backstage/plugin-notifications-node@0.2.10 + - @backstage/catalog-model@1.7.2 + - @backstage/config@1.3.1 + - @backstage/integration-aws-node@0.1.14 + - @backstage/types@1.2.0 + - @backstage/plugin-notifications-common@0.0.7 + +## 0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.1.0-next.2 + - @backstage/plugin-catalog-node@1.15.0-next.2 + - @backstage/plugin-notifications-node@0.2.10-next.2 + - @backstage/catalog-client@1.9.0-next.2 + - @backstage/catalog-model@1.7.2-next.0 + - @backstage/config@1.3.1-next.0 + - @backstage/integration-aws-node@0.1.14-next.0 + - @backstage/types@1.2.0 + - @backstage/plugin-notifications-common@0.0.7-next.0 + +## 0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.15.0-next.1 + - @backstage/catalog-client@1.9.0-next.1 + - @backstage/plugin-notifications-node@0.2.10-next.1 + - @backstage/backend-plugin-api@1.1.0-next.1 + - @backstage/catalog-model@1.7.1 + - @backstage/config@1.3.0 + - @backstage/integration-aws-node@0.1.13 + - @backstage/types@1.2.0 + - @backstage/plugin-notifications-common@0.0.6 + +## 0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.0.3-next.0 + - @backstage/catalog-client@1.8.1-next.0 + - @backstage/catalog-model@1.7.1 + - @backstage/config@1.3.0 + - @backstage/integration-aws-node@0.1.13 + - @backstage/types@1.2.0 + - @backstage/plugin-catalog-node@1.14.1-next.0 + - @backstage/plugin-notifications-common@0.0.6 + - @backstage/plugin-notifications-node@0.2.10-next.0 + +## 0.3.3 + +### Patch Changes + +- d52d7f9: Support ISO and ms string forms of durations in config too +- 5d74716: Remove unused backend-common dependency +- Updated dependencies + - @backstage/catalog-client@1.8.0 + - @backstage/config@1.3.0 + - @backstage/types@1.2.0 + - @backstage/integration-aws-node@0.1.13 + - @backstage/plugin-catalog-node@1.14.0 + - @backstage/backend-plugin-api@1.0.2 + - @backstage/plugin-notifications-common@0.0.6 + - @backstage/plugin-notifications-node@0.2.9 + - @backstage/catalog-model@1.7.1 + +## 0.3.3-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-aws-node@0.1.13-next.0 + - @backstage/backend-plugin-api@1.0.2-next.2 + - @backstage/catalog-client@1.8.0-next.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.14.0-next.2 + - @backstage/plugin-notifications-common@0.0.6-next.0 + - @backstage/plugin-notifications-node@0.2.9-next.3 + +## 0.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.8.0-next.1 + - @backstage/plugin-catalog-node@1.14.0-next.2 + - @backstage/plugin-notifications-node@0.2.9-next.2 + - @backstage/backend-plugin-api@1.0.2-next.2 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.6-next.0 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.0.2-next.1 + - @backstage/catalog-client@1.8.0-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.14.0-next.1 + - @backstage/plugin-notifications-common@0.0.6-next.0 + - @backstage/plugin-notifications-node@0.2.9-next.1 + +## 0.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.14.0-next.0 + - @backstage/plugin-notifications-common@0.0.6-next.0 + - @backstage/catalog-client@1.8.0-next.0 + - @backstage/backend-plugin-api@1.0.2-next.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-node@0.2.9-next.0 + +## 0.3.1 + +### Patch Changes + +- 094eaa3: Remove references to in-repo backend-common +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.7 + - @backstage/plugin-catalog-node@1.13.1 + - @backstage/catalog-client@1.7.1 + - @backstage/backend-plugin-api@1.0.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.13.1-next.1 + - @backstage/catalog-client@1.7.1-next.0 + - @backstage/backend-plugin-api@1.0.1-next.1 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.7-next.1 + +## 0.3.1-next.0 + +### Patch Changes + +- 094eaa3: Remove references to in-repo backend-common +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.7-next.0 + - @backstage/backend-plugin-api@1.0.1-next.0 + - @backstage/catalog-client@1.7.0 + - @backstage/catalog-model@1.7.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.13.1-next.0 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.3.0 + +### Minor Changes + +- d425fc4: **BREAKING**: The return values from `createBackendPlugin`, `createBackendModule`, and `createServiceFactory` are now simply `BackendFeature` and `ServiceFactory`, instead of the previously deprecated form of a function that returns them. For this reason, `createServiceFactory` also no longer accepts the callback form where you provide direct options to the service. This also affects all `coreServices.*` service refs. + + This may in particular affect tests; if you were effectively doing `createBackendModule({...})()` (note the parentheses), you can now remove those extra parentheses at the end. You may encounter cases of this in your `packages/backend/src/index.ts` too, where you add plugins, modules, and services. If you were using `createServiceFactory` with a function as its argument for the purpose of passing in options, this pattern has been deprecated for a while and is no longer supported. You may want to explore the new multiton patterns to achieve your goals, or moving settings to app-config. + + As part of this change, the `IdentityFactoryOptions` type was removed, and can no longer be used to tweak that service. The identity service was also deprecated some time ago, and you will want to [migrate to the new auth system](https://backstage.io/docs/tutorials/auth-service-migration) if you still rely on it. + +### Patch Changes + +- 5edd344: Refactor to use injected catalog client in the new backend system +- Updated dependencies + - @backstage/backend-common@0.25.0 + - @backstage/backend-plugin-api@1.0.0 + - @backstage/catalog-model@1.7.0 + - @backstage/catalog-client@1.7.0 + - @backstage/plugin-catalog-node@1.13.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.6 + +## 0.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.25.0-next.2 + - @backstage/backend-plugin-api@1.0.0-next.2 + - @backstage/catalog-client@1.7.0-next.1 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.7-next.2 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.6-next.2 + +## 0.3.0-next.1 + +### Patch Changes + +- 5edd344: Refactor to use injected catalog client in the new backend system +- Updated dependencies + - @backstage/backend-common@0.25.0-next.1 + - @backstage/catalog-client@1.6.7-next.0 + - @backstage/backend-plugin-api@0.9.0-next.1 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.7-next.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.6-next.1 + +## 0.3.0-next.0 + +### Minor Changes + +- d425fc4: **BREAKING**: The return values from `createBackendPlugin`, `createBackendModule`, and `createServiceFactory` are now simply `BackendFeature` and `ServiceFactory`, instead of the previously deprecated form of a function that returns them. For this reason, `createServiceFactory` also no longer accepts the callback form where you provide direct options to the service. This also affects all `coreServices.*` service refs. + + This may in particular affect tests; if you were effectively doing `createBackendModule({...})()` (note the parentheses), you can now remove those extra parentheses at the end. You may encounter cases of this in your `packages/backend/src/index.ts` too, where you add plugins, modules, and services. If you were using `createServiceFactory` with a function as its argument for the purpose of passing in options, this pattern has been deprecated for a while and is no longer supported. You may want to explore the new multiton patterns to achieve your goals, or moving settings to app-config. + + As part of this change, the `IdentityFactoryOptions` type was removed, and can no longer be used to tweak that service. The identity service was also deprecated some time ago, and you will want to [migrate to the new auth system](https://backstage.io/docs/tutorials/auth-service-migration) if you still rely on it. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.9.0-next.0 + - @backstage/backend-common@0.25.0-next.0 + - @backstage/plugin-notifications-node@0.2.6-next.0 + - @backstage/catalog-client@1.6.6 + - @backstage/catalog-model@1.6.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.2.0 + +### Minor Changes + +- def53a7: **BREAKING** Following `NotificationTemplateRenderer` methods now return a Promise and **must** be awaited: `getSubject`, `getText` and `getHtml`. + + Required changes and example usage: + + ```diff + import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email'; + import { Notification } from '@backstage/plugin-notifications-common'; + +import { getNotificationSubject, getNotificationTextContent, getNotificationHtmlContent } from 'my-notification-processing-library` + export const notificationsModuleEmailDecorator = createBackendModule({ + pluginId: 'notifications', + moduleId: 'email.templates', + register(reg) { + reg.registerInit({ + deps: { + emailTemplates: notificationsEmailTemplateExtensionPoint, + }, + async init({ emailTemplates }) { + emailTemplates.setTemplateRenderer({ + - getSubject(notification) { + + async getSubject(notification) { + - return `New notification from ${notification.source}`; + + const subject = await getNotificationSubject(notification); + + return `New notification from ${subject}`; + }, + - getText(notification) { + + async getText(notification) { + - return notification.content; + + const text = await getNotificationTextContent(notification); + + return text; + }, + - getHtml(notification) { + + async getHtml(notification) { + - return `

    ${notification.content}

    `; + + const html = await getNotificationHtmlContent(notification); + + return html; + }, + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- d55b8e3: Avoid sending broadcast emails as a fallback in case the entity-typed notification user can not be resolved. +- cdb630d: Add support for stream transport for debugging purposes +- 83faf24: Notification email processor supports allowing or denying specific email addresses from receiving notifications +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0 + - @backstage/backend-common@0.24.0 + - @backstage/plugin-notifications-node@0.2.4 + - @backstage/catalog-model@1.6.0 + - @backstage/catalog-client@1.6.6 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.2.0-next.3 + +### Patch Changes + +- 83faf24: Notification email processor supports allowing or denying specific email addresses from receiving notifications +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + +## 0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.2.0-next.1 + +### Minor Changes + +- def53a7: **BREAKING** Following `NotificationTemplateRenderer` methods now return a Promise and **must** be awaited: `getSubject`, `getText` and `getHtml`. + + Required changes and example usage: + + ```diff + import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email'; + import { Notification } from '@backstage/plugin-notifications-common'; + +import { getNotificationSubject, getNotificationTextContent, getNotificationHtmlContent } from 'my-notification-processing-library` + export const notificationsModuleEmailDecorator = createBackendModule({ + pluginId: 'notifications', + moduleId: 'email.templates', + register(reg) { + reg.registerInit({ + deps: { + emailTemplates: notificationsEmailTemplateExtensionPoint, + }, + async init({ emailTemplates }) { + emailTemplates.setTemplateRenderer({ + - getSubject(notification) { + + async getSubject(notification) { + - return `New notification from ${notification.source}`; + + const subject = await getNotificationSubject(notification); + + return `New notification from ${subject}`; + }, + - getText(notification) { + + async getText(notification) { + - return notification.content; + + const text = await getNotificationTextContent(notification); + + return text; + }, + - getHtml(notification) { + + async getHtml(notification) { + - return `

    ${notification.content}

    `; + + const html = await getNotificationHtmlContent(notification); + + return html; + }, + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + +## 0.1.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + +## 0.1.0 + +### Minor Changes + +- 07a789b: add notification filters + +### Patch Changes + +- 78a0b08: Internal refactor to handle `BackendFeature` contract change. +- d44a20a: Added additional plugin metadata to `package.json`. +- Updated dependencies + - @backstage/backend-common@0.23.0 + - @backstage/backend-plugin-api@0.6.19 + - @backstage/plugin-notifications-node@0.2.0 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.1.0-next.3 + +### Patch Changes + +- d44a20a: Added additional plugin metadata to `package.json`. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.3 + - @backstage/plugin-notifications-common@0.0.4-next.0 + - @backstage/plugin-notifications-node@0.2.0-next.3 + - @backstage/backend-common@0.23.0-next.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-notifications-node@0.2.0-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + +## 0.1.0-next.1 + +### Minor Changes + +- 07a789b: add notification filters + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-notifications-node@0.2.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## 0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-notifications-node@0.1.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + +## 0.0.1 + +### Patch Changes + +- d541ff6: Fixed email processor `esm` issue and config reading +- e538b10: Support relative links in notifications sent via email +- dbf2696: Allow sending notifications by email with the new notifications module +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/backend-common@0.22.0 + - @backstage/plugin-notifications-node@0.1.4 + - @backstage/backend-plugin-api@0.6.18 + - @backstage/catalog-client@1.6.5 + +## 0.0.1-next.1 + +### Patch Changes + +- d541ff6: Fixed email processor `esm` issue and config reading +- e538b10: Support relative links in notifications sent via email +- Updated dependencies + - @backstage/backend-common@0.22.0-next.2 + +## 0.0.1-next.0 + +### Patch Changes + +- dbf2696: Allow sending notifications by email with the new notifications module +- Updated dependencies + - @backstage/plugin-notifications-node@0.1.4-next.1 + - @backstage/backend-common@0.22.0-next.1 + - @backstage/backend-plugin-api@0.6.18-next.1 + +## @backstage/create-app@0.7.6-next.1 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + +## @backstage/plugin-mui-to-bui@0.2.1-next.1 + +### Patch Changes + +- 5c614ff: Updated BUI checkbox preview example to align with new component API. +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + +## example-app@0.2.115-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + - @backstage/plugin-mui-to-bui@0.2.1-next.1 + +## example-app-next@0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + +## techdocs-cli-embedded-app@0.2.114-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 diff --git a/docs/releases/v1.45.0-next.2-changelog.md b/docs/releases/v1.45.0-next.2-changelog.md new file mode 100644 index 0000000000..4bac9094e7 --- /dev/null +++ b/docs/releases/v1.45.0-next.2-changelog.md @@ -0,0 +1,1448 @@ +# Release v1.45.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.45.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.45.0-next.2) + +## @backstage/backend-app-api@1.3.0-next.1 + +### Minor Changes + +- a17d9df: Updates API for `instanceMetadata` service to return a list of plugins not features. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/backend-plugin-api@1.5.0-next.1 + +### Minor Changes + +- a17d9df: Promote `instanceMetadata` service to main entrypoint. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/ui@0.9.0-next.2 + +### Minor Changes + +- 539cf26: **BREAKING**: Migrated Avatar component from Base UI to custom implementation with size changes: + + - Base UI-specific props are no longer supported + - Size values have been updated: + - New `x-small` size added (1.25rem / 20px) + - `small` size unchanged (1.5rem / 24px) + - `medium` size unchanged (2rem / 32px, default) + - `large` size **changed from 3rem to 2.5rem** (40px) + - New `x-large` size added (3rem / 48px) + + Migration: + + ```diff + # Remove Base UI-specific props + - + + + + # Update large size usage to x-large for same visual size + - + + + ``` + + Added `purpose` prop for accessibility control (`'informative'` or `'decoration'`). + +- 134151f: Fixing styles on SearchField in Backstage UI after migration to CSS modules. `SearchField` has now its own set of class names. We previously used class names from `TextField` but this approach was creating some confusion so going forward in your theme you'll be able to theme `TextField` and `SearchField` separately. + +### Patch Changes + +- d01de00: Fix broken external links in Backstage UI Header component. +- deaa427: Fixed Text component to prevent `truncate` prop from being spread to the underlying DOM element. +- 1059f95: Improved the Link component structure in Backstage UI. +- 6874094: Migrated CellProfile component from Base UI Avatar to Backstage UI Avatar component. +- 719d772: Avatar components in x-small and small sizes now display only one initial instead of two, improving readability at smaller dimensions. +- 3b18d80: Fixed RadioGroup radio button ellipse distortion by preventing flex shrink and grow. +- e16ece5: Set the color-scheme property depending on theme + +## @backstage/plugin-catalog@1.32.0-next.1 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-catalog-backend@3.2.0-next.1 + +### Minor Changes + +- 2d229b2: Enable YAML merge keys in yamlPlaceholderResolver +- 9d3ec06: Make YAML merge (<<:) support configurable in the Backstage Catalog instead of always being enabled +- 8c26af4: Enable YAML merge keys in yamlPlaceholderResolver + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.0-next.1 + +### Minor Changes + +- 980f240: Moved from `ldapjs` dependency to `ldapts` + + ### Breaking Changes + + **Type Migration** + + Custom transformers must now accept `Entry` from ldapts instead of `SearchEntry` + from ldapjs The Entry type provides direct property access without need for + `.object()` or `.raw()` methods. + + If you have custom user or group transformers, update the signature from: + + ```typescript + (vendor: LdapVendor, config: UserConfig, entry: SearchEntry) => + Promise; + ``` + + to + + ```typescript + (vendor: LdapVendor, config: UserConfig, entry: Entry) => + Promise; + ``` + + **Search Options** + + Updated LDAP search configuration `typesOnly: false` → `attributeValues: true` + This inverts the boolean logic: ldapjs used negative form while ldapts uses + positive form. Both achieve the same result: retrieving attribute values rather + than just attribute names. + + Update LDAP search options in configuration from + + ```yaml + options: + typesOnly: false + ``` + + to + + ```yaml + options: + attributeValues: true + ``` + + **API Changes** Removed `LdapClient.searchStreaming()` method. Users should + migrate to `LdapClient.search()` instead + + If you're using `searchStreaming` directly: + + ```typescript + // Before + await client.searchStreaming(dn, options, async entry => { + // process each entry + }); + + // After + const entries = await client.search(dn, options); + for (const entry of entries) { + // process each entry + } + ``` + + > **_NOTE:_**: Both methods have always loaded all entries into memory. The + > searchStreaming method was only needed internally to handle ldapjs's + > event-based API. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-node@1.20.0-next.1 + +### Minor Changes + +- 9d3ec06: Make YAML merge (<<:) support configurable in the Backstage Catalog instead of always being enabled +- 8c26af4: Enable YAML merge keys in yamlPlaceholderResolver + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-gateway-backend@1.1.0-next.1 + +### Minor Changes + +- a17d9df: Update usage of the `instanceMetadata` service. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.10.0-next.1 + +### Minor Changes + +- ff96d7e: fix scaffolder action createDeployToken to allow usage of oauth tokens + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-search@1.5.0-next.1 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-search-react@1.10.0-next.1 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + +## @backstage/plugin-techdocs@1.16.0-next.1 + +### Minor Changes + +- a521911: Add support for customizable icons in `SearchResultListItemBlueprint` and related components + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/backend-defaults@0.13.1-next.1 + +### Patch Changes + +- 91ab2eb: Fix a bug in the Gitlab URL reader where `search` did not handle multiple globs +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-app-api@1.3.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/backend-dynamic-feature-service@0.7.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-app-node@0.1.39-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/backend-openapi-utils@0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/backend-test-utils@1.10.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-app-api@1.3.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/cli@0.34.5-next.1 + +### Patch Changes + +- da19cb5: Fix inconsistent behavior in the `new` command for the `@internal` scope: it now consistently defaults to the `backstage-plugin-` infix whether the `--scope` option is not set or it's set to `internal`. +- b2bef92: Convert all enums to erasable-syntax compliant patterns + +## @backstage/core-app-api@1.19.2-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/core-plugin-api@1.11.2-next.1 + +## @backstage/core-components@0.18.3-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/core-plugin-api@1.11.2-next.1 + +## @backstage/core-plugin-api@1.11.2-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns + +## @backstage/create-app@0.7.6-next.2 + +### Patch Changes + +- 9f939a6: Added `@backstage/plugin-app-visualizer` to the app in the `--next` template. + +## @backstage/frontend-plugin-api@0.12.2-next.1 + +### Patch Changes + +- 878c251: Updated to `ExtensionInput` to make all type parameters optional. +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + +## @backstage/repo-tools@0.15.4-next.1 + +### Patch Changes + +- 8f56eae: Updated knip-reports to detect dependencies in dev/alpha pattern +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-api-docs@0.13.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-app-backend@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-app-node@0.1.39-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-app-node@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-app-visualizer@0.1.25-next.1 + +### Patch Changes + +- e81b3f0: Improve tree visualizer to use a horizontal layout and fill the content space. +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + +## @backstage/plugin-auth-backend@0.25.6-next.1 + +### Patch Changes + +- 51ff7d8: Allow configuring dynamic client registration token expiration with config `auth.experimentalDynamicClientRegistration.tokenExpiration`. + + Maximum expiration for the DCR token is 24 hours. Default expiration is 1 hour. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-backend@0.25.6-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-backend@0.25.6-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-auth-node@0.6.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-azure@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-github@0.11.2-next.1 + +### Patch Changes + +- 999d1c1: Added configurable `pageSizes` for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors with organizations with large number of repositories. Please see the [GitHub Discovery documentation](https://backstage.io/docs/integrations/github/discovery#configuration) for new configuration options. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.16-next.1 + +### Patch Changes + +- 999d1c1: Added configurable `pageSizes` for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors with organizations with large number of teams and members. Please see the [GitHub Org Data documentation](https://backstage.io/docs/integrations/github/org#configuration-details) for new configuration options. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.2-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.5-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.6-next.1 + +### Patch Changes + +- 70745c5: Correctly handle entity removal computation when DB count query returns string +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-logs@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.16-next.1 + +### Patch Changes + +- a5bcb2a: fix wrong dereferencing for AsyncApi 3 documents +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-catalog-graph@0.5.3-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-catalog-react@1.21.3-next.1 + +### Patch Changes + +- 2b7924b: Apply default ordering of templates +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-devtools-backend@0.5.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-devtools-common@0.1.19-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-devtools-common@0.1.19-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3-next.1 + +## @backstage/plugin-events-backend@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-azure@0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-github@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-backend-module-kafka@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-events-node@0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-kubernetes-backend@0.20.4-next.1 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-kubernetes-node@0.3.6-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-kubernetes-node@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-mcp-actions-backend@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-notifications-backend@0.5.12-next.1 + +### Patch Changes + +- 15fb764: Show default settings for notifications even before receiving first notification. + + Previously, it was not possible for the users to see or modify their notification settings until they had received at + least one notification from specific origin or topic. + This update ensures that default settings are displayed from the outset, + allowing users to customize their preferences immediately. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + +## @backstage/plugin-notifications-node@0.2.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + +## @backstage/plugin-permission-backend@0.7.6-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + +## @backstage/plugin-permission-common@0.9.3-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns + +## @backstage/plugin-permission-node@0.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + +## @backstage/plugin-proxy-backend@0.6.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-proxy-node@0.1.10-next.1 + +## @backstage/plugin-proxy-node@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-scaffolder-backend@3.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.10.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.15-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.15-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.15-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.5-next.1 + +## @backstage/plugin-scaffolder-node@0.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-test-utils@1.10.0-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + +## @backstage/plugin-search-backend@2.0.8-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-catalog@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.8-next.1 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-explore@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-pg@0.5.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + - @backstage/plugin-techdocs-node@1.13.9-next.1 + +## @backstage/plugin-search-backend-node@1.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + +## @backstage/plugin-signals@0.0.25-next.1 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- f0f006e: Fixes a bug where the `SignalClient` would try to subscribe to the same channel twice after an error, instead of just once. +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/core-compat-api@0.5.4-next.0 + +## @backstage/plugin-signals-backend@0.3.10-next.1 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + +## @backstage/plugin-signals-node@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + +## @backstage/plugin-techdocs-backend@2.1.2-next.1 + +### Patch Changes + +- 71c22f3: Removed/moved unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-techdocs-node@1.13.9-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.30-next.1 + +### Patch Changes + +- 6929480: ExpandableCollapse Techdocs Addon was breaking native sidebar collapse on Firefox +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + +## @backstage/plugin-techdocs-node@1.13.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + +## @backstage/plugin-user-settings-backend@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + +## example-app@0.2.115-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-signals@0.0.25-next.1 + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-search@1.5.0-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + - @backstage/plugin-catalog-graph@0.5.3-next.1 + - @backstage/plugin-scaffolder@1.34.3-next.0 + - @backstage/plugin-api-docs@0.13.1-next.1 + - @backstage/plugin-catalog-import@0.13.7-next.0 + - @backstage/plugin-org@0.6.46-next.0 + - @backstage/plugin-scaffolder-react@1.19.3-next.0 + - @backstage/plugin-user-settings@0.8.29-next.0 + +## example-app-next@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-signals@0.0.25-next.1 + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-search@1.5.0-next.1 + - @backstage/plugin-app-visualizer@0.1.25-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + - @backstage/plugin-catalog-graph@0.5.3-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/plugin-scaffolder@1.34.3-next.0 + - @backstage/core-compat-api@0.5.4-next.0 + - @backstage/plugin-api-docs@0.13.1-next.1 + - @backstage/plugin-catalog-import@0.13.7-next.0 + - @backstage/plugin-org@0.6.46-next.0 + - @backstage/plugin-scaffolder-react@1.19.3-next.0 + - @backstage/plugin-user-settings@0.8.29-next.0 + +## example-backend@0.0.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.8-next.1 + - @backstage/plugin-kubernetes-backend@0.20.4-next.1 + - @backstage/plugin-techdocs-backend@2.1.2-next.1 + - @backstage/plugin-signals-backend@0.3.10-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/plugin-notifications-backend@0.5.12-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.16-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-backend@0.7.6-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-search-backend@2.0.8-next.1 + - @backstage/plugin-auth-backend@0.25.6-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.6-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.5-next.1 + - @backstage/plugin-scaffolder-backend@3.0.1-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.8-next.1 + - @backstage/plugin-app-backend@0.5.8-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.3.9-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.14-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.2-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-devtools-backend@0.5.11-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.6-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-proxy-backend@0.6.8-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.9-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + +## techdocs-cli-embedded-app@0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + +## @internal/plugin-todo-list-backend@1.0.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 diff --git a/docs/releases/v1.45.0-next.3-changelog.md b/docs/releases/v1.45.0-next.3-changelog.md new file mode 100644 index 0000000000..0f66b0ca65 --- /dev/null +++ b/docs/releases/v1.45.0-next.3-changelog.md @@ -0,0 +1,282 @@ +# Release v1.45.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.45.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.45.0-next.3) + +## @backstage/backend-plugin-api@1.5.0-next.2 + +### Minor Changes + +- 62fc2de: Explicitly mark `coreServices.rootInstanceMetadata` as a root service. + +## @backstage/repo-tools@0.16.0-next.2 + +### Minor Changes + +- 11c61f2: The `package-docs` command will now automatically use a `typedoc.json` file if one exists at the root of your project. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + +## @backstage/ui@0.9.0-next.3 + +### Minor Changes + +- 83c100e: **BREAKING**: Removed `Collapsible` component. Migrate to `Accordion` or use React Aria `Disclosure`. + + ## Migration Path 1: Accordion (Opinionated Styled Component) + + Accordion provides preset styling with a similar component structure. + + ```diff + - import { Collapsible } from '@backstage/ui'; + + import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui'; + + - + - } /> + - Content + - + + + + + + + Content + + + ``` + + CSS classes: `.bui-CollapsibleRoot` → `.bui-Accordion`, `.bui-CollapsibleTrigger` → `.bui-AccordionTrigger` (now on heading element), `.bui-CollapsiblePanel` → `.bui-AccordionPanel` + + ## Migration Path 2: React Aria Disclosure (Full Customization) + + For custom styling without preset styles: + + ```tsx + import { Disclosure, Button, DisclosurePanel } from 'react-aria-components'; + + + + Content + ; + ``` + +- 816af0f: **BREAKING**: The `SelectProps` interface now accepts a generic type parameter for selection mode. + + Added searchable and multiple selection support to Select component. The component now accepts `searchable`, `selectionMode`, and `searchPlaceholder` props to enable filtering and multi-selection modes. + + Migration: If you're using `SelectProps` type directly, update from `SelectProps` to `SelectProps<'single' | 'multiple'>`. Component usage remains backward compatible. + +### Patch Changes + +- 35a3614: Fixed CSS issues in Select component including popover width constraints, focus outline behavior, and overflow handling. +- 01476f0: Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components. +- 836b0c7: Fixed dialog backdrop appearance in dark mode. +- 6d35a6b: Removed `@base-ui-components/react` dependency as all components now use React Aria Components. +- 7839e7b: Added `loading` prop to Button and ButtonIcon components for displaying spinner during async operations. +- a00fb88: Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the href prop. + +## @backstage/plugin-notifications-backend@0.6.0-next.2 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## @backstage/plugin-notifications-common@0.2.0-next.1 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +## @backstage/backend-test-utils@1.10.0-next.2 + +### Patch Changes + +- f3001fd: Tweak some of the mock services to have more precise types +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + +## @backstage/core-components@0.18.3-next.2 + +### Patch Changes + +- 96ad674: Line numbers in LogViewer will not be selectable in UI anymore + +## @backstage/create-app@0.7.6-next.3 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/frontend-plugin-api@0.12.2-next.2 + +### Patch Changes + +- 7c6a66d: Added support for plugin-relative `attachTo` declarations for extension definitions. This allows for the creation of extension and extension blueprints that attach to other extensions of a particular `kind` in the same plugin, rather than needing to provide the exact extension ID. This is particularly useful when wanting to provide extension blueprints with a built-in hierarchy where the extensions created from one blueprint attach to extensions created from the other blueprint, for example: + + ```ts + // kind: 'tabbed-page' + const parentPage = TabbedPageBlueprint.make({ + params: {....} + }) + // attachTo: { kind: 'tabbed-page', input: 'tabs' } + const child1 = TabContentBlueprint.make({ + name: 'tab1', + params: {....} + }) + ``` + +- Updated dependencies + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-app-visualizer@0.1.25-next.2 + +### Patch Changes + +- 722e2df: Migrated to use `@backstage/ui`. +- Updated dependencies + - @backstage/ui@0.9.0-next.3 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-catalog-graph@0.5.3-next.2 + +### Patch Changes + +- a2d7ae7: Ensure the catalog graph entity card respects the height prop so the visualization scales down properly on wide screens. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3-next.2 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-catalog-react@1.21.3-next.2 + +### Patch Changes + +- 36d7582: Added missing i18n +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-home@0.8.14-next.1 + +### Patch Changes + +- 2ac5d29: Allow customization of VisitList by adding optional enrichVisit, transformPathname, canSave functions to VisitsStorageApi, along with VisitDisplayProvider for colors, labels +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3-next.2 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-kubernetes-backend@0.20.4-next.2 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-kubernetes-common@0.9.8-next.1 + - @backstage/plugin-kubernetes-node@0.3.6-next.2 + +## @backstage/plugin-kubernetes-common@0.9.8-next.1 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. + +## @backstage/plugin-kubernetes-node@0.3.6-next.2 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-kubernetes-common@0.9.8-next.1 + +## @backstage/plugin-kubernetes-react@0.5.13-next.1 + +### Patch Changes + +- 1906d37: Updated dependency `@kubernetes/client-node` to `1.4.0`. +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.9.8-next.1 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-notifications@0.5.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-notifications-backend-module-email@0.3.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## @backstage/plugin-notifications-backend-module-slack@0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## @backstage/plugin-notifications-node@0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + +## @backstage/plugin-org@0.6.46-next.1 + +### Patch Changes + +- 6db9e7e: Improved responsiveness of GroupProfileCard component +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3-next.2 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## example-backend@0.0.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-backend@0.6.0-next.2 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-kubernetes-backend@0.20.4-next.2 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.2 + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-scaffolder-backend@3.0.1-next.1 + - @backstage/plugin-search-backend@2.0.8-next.1 diff --git a/docs/releases/v1.45.0.md b/docs/releases/v1.45.0.md new file mode 100644 index 0000000000..2afc9e7c35 --- /dev/null +++ b/docs/releases/v1.45.0.md @@ -0,0 +1,72 @@ +--- +id: v1.45.0 +title: v1.45.0 +description: Backstage Release v1.45.0 +--- + +These are the release notes for the v1.45.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 + +### Backstage UI: Breaking Changes + New Components + +Multiple components migrated from Base UI to React Aria Components including `Avatar`, `Checkbox`, and removal of the `Collapsible` component in favour of `Accordion`. + +Check the [CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/ui/CHANGELOG.md) for more migration guides on any breaking changes that come with these latest updates. + +### BREAKING: `ldapjs` -> `ldapts` + +Moved from deprecated `ldapjs` dependency to `ldapts`, with breaking changes to custom transformer types and search options. + +Contributed by [@ganives](https://github.com/ganievs) in [#30594](https://github.com/backstage/backstage/pull/30594) + +Check the [CHANGELOG.md](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/CHANGELOG.md) for more migration guides on any breaking changes that come with these latest updates. + +### NFS: Plugin-Relative Extension Attachments + +Added support for plugin-relative `attachTo` declarations for extension definitions, allowing extensions to attach to other extensions of a particular kind in the same plugin rather than requiring exact extension IDs. + +### NFS: Forwards Compatibility for Route Refs + +It is now possible to use route references from the old frontend system directly in the new one. That means there's no longer a need to use `convertLegacyRouteRef` or `convertLegacyRouteRefs` to re-use route refs in implementations for the new system. This both simplifies migration, and reduces risk for cross system issues while partially migrated. + +This requires no immediate action on your part. As long as a plugin intends to support the old system, it can still keep defining its route refs using the old system without issues. And calling `convertLegacyRouteRef` or `convertLegacyRouteRefs` does not cause problems. + +### Configurable Dynamic Client Registration Token Expiration + +Allow configuring dynamic client registration token expiration with config `auth.experimentalDynamicClientRegistration.tokenExpiration`. Maximum expiration for the DCR token is 24 hours. Default expiration is 1 hour. Contributed by [@drodil](https://github.com/drodil) in [#31278](https://github.com/backstage/backstage/pull/31278) + +### Support for Bitbucket Cloud API tokens + +Since Bitbucket Cloud is phasing out support for the `appPassword` tokens, you can now instead specify the more modern API tokens using the `token` field of your Bitbucket config. + +### Support for PostgreSQL 18 + +The default setup for `TestDatabases` will start running tests against PG14 and PG18 where available, instead of PG13 and PG17 as previously. If you pass in explicit database IDs to your test database instance, those will still be respected. + +### `coreServices.rootInstanceMetadata` is stable + +The backend service `coreServices.rootInstanceMetadata` is now available as stable. It currently has one method - that lets you list all of the installed backend plugins and their modules on the current instance. + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.45.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://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.46.0-changelog.md b/docs/releases/v1.46.0-changelog.md new file mode 100644 index 0000000000..22e6e39dab --- /dev/null +++ b/docs/releases/v1.46.0-changelog.md @@ -0,0 +1,2369 @@ +# Release v1.46.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.46.0](https://backstage.github.io/upgrade-helper/?to=1.46.0) + +## @backstage/plugin-techdocs-addons-test-utils@2.0.0 + +### Major Changes + +- 8d6709e: **BREAKING**: `TechDocsAddonTester.renderWithEffects()` no longer returns a screen; this means that you can no longer grab assertions such as `getByText` from its return value. + + Newer versions of `@testing-library` recommends using the `screen` export for assertions - and removing this from the addon tester contract allows us to more freely iterate on which underlying version of the testing library is being used. + + One notable effect of this, however, is that the `@testing-library` `screen` does NOT support assertions on the shadow DOM, which techdocs relies on. You will therefore want to add a dependency on [the `shadow-dom-testing-library` package](https://github.com/konnorrogers/shadow-dom-testing-library/) in your tests, and using its `screen` and its dedicated `*Shadow*` methods. As an example, if you keep doing `getByText` you will not get matches inside the shadow DOM - switch to `getByShadowText` instead. + + ```ts + import { screen } from 'shadow-dom-testing-library'; + + // ... render the addon ... + await TechDocsAddonTester.buildAddonsInTechDocs([]) + .withDom(TEST_CONTENT) + .renderWithEffects(); + + expect(screen.getByShadowText('TEST_CONTENT')).toBeInTheDocument(); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-search-react@1.10.1 + - @backstage/test-utils@1.7.14 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-techdocs-react@1.3.6 + +## @backstage/backend-app-api@1.4.0 + +### Minor Changes + +- 5a2d538: Introduced backend startup result tracking and error handling. The `Backend.start()` method now returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed. + + This also improves the default error message when backend startup fails, and of course makes it possible to craft your own custom error reporting based on the startup results. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/backend-defaults@0.14.0 + +### Minor Changes + +- fa43826: Move `better-sqlite3` from dependencies to peer dependencies +- 2bc4e02: **BREAKING** The correct configuration options for Valkey are now being used. + + These changes are **required** to `app-config.yaml`: + + ```diff + backend: + cache: + store: valkey + connection: ... + client: + - namespace: 'my-app' + - keyPrefixSeparator: ':' + + keyPrefix: 'my-app:' + - clearBatchSize: 1000 + - useUnlink: false + ``` + + In comparison to Redis, Valkey requires the full `keyPrefix` including the separator to be specified instead of separate `namespace` and `keyPrefixSeparator` options. Also, Valkey does not support the `clearBatchSize` and `useUnlink` options. + +### Patch Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 + +- aa79251: build(deps): bump `node-forge` from 1.3.1 to 1.3.2 + +- f96edff: Allow configuration of the `referrerPolicy` + +- fb029b6: Updated luxon types + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. + +- 847a330: Fix for `jose` types + +- 25b560e: Internal change to support new versions of the `logform` library + +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. + +- 3016a79: Updated dependency `@types/archiver` to `^7.0.0`. + +- 42db6a6: Don't warn when parsing `storeOptions` for `memory` cache + +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/integration@1.19.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config-loader@1.10.7 + - @backstage/backend-dev-utils@0.1.6 + +## @backstage/backend-plugin-api@1.6.0 + +### Minor Changes + +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/cli-common@0.1.16 + +## @backstage/cli@0.35.0 + +### Minor Changes + +- f6f22a9: Provide `--no-node-snapshot` by default when running the `package start` or `package test`. You can disable this behavior by providing `NODE_OPTIONS='--node-snapshot'`. + +- f8dff94: Switched the default module resolution to `bundler` and the `module` setting to `ES2020`. + + You may need to bump some dependencies as part of this change and fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages. + + You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript. + + A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field. + +- cd0b8a1: **BREAKING**: `jest` is now a peer dependency. If you run tests using Backstage CLI, you must add Jest and its environment dependencies as `devDependencies` in your project. + + You can choose to install either Jest 29 or Jest 30. The built-in Jest version before this change was Jest 29, however, we recommend that you switch to Jest 30. Upgrading will solve the `Could not parse CSS stylesheet` errors, allow you to use MSW v2 in web packages, and ensure that you remain compatible with future versions of the Backstage CLI. Support for Jest 29 is temporary, with the purpose of allowing you to upgrade at your own pace, but it will eventually be removed. + + - **Jest 29**: Install `jest@^29` and `jest-environment-jsdom@^29`. No migration needed, but you may see `Could not parse CSS stylesheet` warnings/errors when testing components from `@backstage/ui` or other packages using CSS `@layer` declarations. + - **Jest 30**: Install `jest@^30`, `@jest/environment-jsdom-abstract@^30`, and `jsdom@^27`. Fixes the stylesheet parsing warnings/errors, but requires migration steps. + + See the [Jest 30 migration guide](https://backstage.io/docs/tutorials/jest30-migration) for detailed migration instructions. + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 + +- e7db290: Add missing peer/dev dependencies to the frontend plugin template. + + `react-dom` was not declared as a peer dependency, causing module resolution + errors when generating plugins outside a Backstage monorepo. This adds + `react-dom` to `peerDependencies` (for consuming apps) and `devDependencies` + (for local development). `react-router-dom` is also added to `peerDependencies` (for consuming apps) and `devDependencies` + to support routing during plugin development. + + Fixes: + + - Module not found: Can't resolve 'react-dom' + - Module not found: Can't resolve 'react-router-dom' + +- 1226647: Updated dependency `esbuild` to `^0.27.0`. + +- f89a074: Updated dependency `@pmmmwh/react-refresh-webpack-plugin` to `^0.6.0`. + +- 2b81751: Updated dependency `webpack` to `~5.103.0`. + +- fafd9e1: Fixed internal usage of `yargs`. + +- c8c2329: Add proxy configuration from env-vars to create-app tasks + +- 2bae83a: Switched compilation target to ES2022 in order to match the new set of supported Node.js versions, which are 22 and 24. + + The TypeScript compilation target has been set to ES2022, because setting it to a higher target will break projects on older TypeScript versions. If you use a newer TypeScript version in your own project, you can bump `compilerOptions.target` to ES2023 or ES2024 in your own `tsconfig.json` file. + +- 7fbac5c: Updated to use new utilities from `@backstage/cli-common`. + +- 2bae83a: Bumped dev dependencies `@types/node` + +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/integration@1.19.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + +## @backstage/integration@1.19.0 + +### Minor Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +### Patch Changes + +- a26a322: Added support for using a GitHub App installation to generate tokens for public repository access when the `publicAccess` option is enabled. When all other authentication methods fail (e.g., the app is not installed in that organization), the provider will now use an available installation to generate a token that can be used to access public repositories as read only. +- fb029b6: Updated luxon types +- e15fdae: Made the github urls case insensitive. + +## @backstage/ui@0.10.0 + +### Minor Changes + +- 16543fa: **Breaking change** The `Cell` component has been refactored to be a generic wrapper component that accepts `children` for custom cell content. The text-specific functionality (previously part of `Cell`) has been moved to a new `CellText` component. + + ### Migration Guide + + If you were using `Cell` with text-specific props (`title`, `description`, `leadingIcon`, `href`), you need to update your code to use `CellText` instead: + + **Before:** + + ```tsx + } + href="/path" + /> + ``` + + **After:** + + ```tsx + } + href="/path" + /> + ``` + + For custom cell content, use the new generic `Cell` component: + + ```tsx + {/* Your custom content */} + ``` + +### Patch Changes + +- 50b7927: Fixed Checkbox indicator showing checkmark color when unchecked. + + Affected components: Checkbox + +- 5bacf55: Fixed `ButtonIcon` incorrectly applying `className` to inner elements instead of only the root element. + + Affected components: ButtonIcon + +- b3ad928: Fixed Table Row component to correctly handle cases where no `href` is provided, preventing unnecessary router provider wrapping and fixing the cursor incorrectly showing as a pointer despite the element not being a link. + + Affected components: Row + +- a20d317: Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode. + + Affected components: Table, TableHeader, Row, Column + +- fe7c751: Fixed `useTable` hook to prioritize `providedRowCount` over data length for accurate row count in server-side pagination scenarios. + +- c145031: Fixed Table column sorting indicator to show up arrow when no sort is active, correctly indicating that clicking will sort ascending. + + Affected components: Column + +## @backstage/plugin-auth-backend-module-github-provider@0.4.0 + +### Minor Changes + +- b3286d5: Added the `github.com/user-id` annotation to store GitHub's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-catalog-backend@3.3.0 + +### Minor Changes + +- dce1824: Added `ActionsRegistry` actions for `register-entity` and `unregister-entity` + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 8101ec1: Fixed default refresh service to go through the whole ancestry of the entity. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-github@0.12.0 + +### Minor Changes + +- b3286d5: Added the `github.com/user-id` annotation to store GitHub's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- ed5a7a3: Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. + + When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances. + Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. + +- a413977: Added configurable `pageSizes` option to `GithubOrgEntityProvider` for GitHub GraphQL API queries to prevent `RESOURCE_LIMITS_EXCEEDED` errors with organizations with large number of teams and members. This aligns the configuration options with `GithubMultiOrgEntityProvider`. + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-events-backend-module-kafka@0.2.0 + +### Minor Changes + +- 2c74ea9: Added support for multiple named instances in `kafkaConsumingEventPublisher` configuration. The previous single configuration format is still supported for backward compatibility. +- 2c74ea9: Added `KafkaPublishingEventConsumer` to support sending Backstage events to Kafka topics. + + This addition enables Backstage to publish events to external Kafka systems, complementing the existing ability to receive events from Kafka. This allows for better integration with external systems that rely on Kafka for event streaming. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-kubernetes-backend@0.21.0 + +### Minor Changes + +- 7f9846f: Add possibility to extends Kubernetes REST API. Add fetcher to parameters for custom objects provider + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- fb029b6: Updated luxon types +- e9589d9: Replace `@aws-sdk/signature-v4` with `@smithy/signature-v4`, + as stated in the [package documentation](https://www.npmjs.com/package/@aws-sdk/signature-v4?activeTab=readme) +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas +- Updated dependencies + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-kubernetes-node@0.4.0 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-kubernetes-node@0.4.0 + +### Minor Changes + +- 7f9846f: Add possibility to extends Kubernetes REST API. Add fetcher to parameters for custom objects provider + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-kubernetes-common@0.9.9 + +## @backstage/plugin-notifications-backend-module-slack@0.3.0 + +### Minor Changes + +- f95a516: Enables optional routes to Slack channels for broadcast notifications based on origin and/or topics. + +### Patch Changes + +- b80857a: Slack notification handler throttling can now be configured with the `concurrencyLimit` and `throttleInterval` options. +- f8230e4: Updated dependency `@faker-js/faker` to `^10.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## @backstage/plugin-scaffolder@1.35.0 + +### Minor Changes + +- dab3d3f: Added field extension `RepoOwnerPicker` for retrieving GitHub repository owners. + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder-common@1.7.4 + - @backstage/plugin-techdocs-react@1.3.6 + +## @backstage/plugin-scaffolder-backend@3.1.0 + +### Minor Changes + +- a4cd405: Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity. + +### Patch Changes + +- be5972b: Fixed a bug where config was not passed to NunjucksWorkflowRunner, causing defaultEnvironment to be undefined +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 2bae83a: Updated `isolated-vm` to `6.0.1` +- 25b560e: Internal change to support new versions of the `logform` library +- 8f4aded: Fixing OpenAPI definition +- 1226647: Updated dependency `esbuild` to `^0.27.0`. +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.0 + - @backstage/integration@1.19.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.16 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.16 + - @backstage/plugin-scaffolder-common@1.7.4 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.0 + +### Minor Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.0 + +### Minor Changes + +- f2d034b: In the `gitlabRepoPush` action, add 'auto' possibility for `commitAction` input. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-search-backend-node@1.4.0 + +### Minor Changes + +- 4d3ddb9: Improving method that search tokenizer breaks apart entity names + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/app-defaults@1.7.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/backend-dev-utils@0.1.6 + +### Patch Changes + +- 2bae83a: Internal update for Node.js v24 support. + +## @backstage/backend-dynamic-feature-service@0.7.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-app-node@0.1.40 + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-events-backend@0.5.9 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/backend-openapi-utils@0.6.4 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/backend-test-utils@1.10.2 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 8be23a4: Switched `textextensions` dependency for `text-extensions`. +- 5a737e1: Fix PostgreSQL 18 `TestDatabases` by pinning the data directory +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/cli-common@0.1.16 + +### Patch Changes + +- 5cfb2a4: Added new `run`, `runOutput`, and `runCheck` utilities to help run child processes in a safe and portable way. +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- 2bae83a: Bumped dev dependencies `@types/node` + +## @backstage/cli-node@0.2.16 + +### Patch Changes + +- 4e8c726: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @backstage/codemods@0.1.53 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @backstage/config-loader@1.10.7 + +### Patch Changes + +- 741c47a: Updated dependency `typescript-json-schema` to `^0.67.0`. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @backstage/core-app-api@1.19.3 + +### Patch Changes + +- 75683ed: Added replay functionality to `AlertApiForwarder` to buffer and replay recent alerts to new subscribers, preventing missed alerts that were posted before subscription. +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + +## @backstage/core-compat-api@0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/core-components@0.18.4 + +### Patch Changes + +- 9a942a4: Fixed bug in the `LogViewer` component where shift + click always opened a new window instead of just changing the selection. + + In addition, improved the `LogViewer` component by a few usability enhancements: + + - Added support for multiple selections using cmd/ctrl + click + - Improved the generated hash that is added to the URL to also support ranges & multiple selections + - Added an hover effect & info tooltip to the "Copy to clipboard" button to indicate its functionality + - Added some color and a separator to the line numbers to improve readability + +- 207c3c8: long words like urls now breaks to new line on warning panels instead of overflowing the container + +- 4c00303: Add `tooltipClasses` prop to `OverflowTooltip` component to allow customisation of the tooltip + +- 5d52dab: Add i18n support for LogViewer search control + +- f6b49ce: added support for wrapLongLines option in CodeSnippet + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + +## @backstage/core-plugin-api@1.12.1 + +### Patch Changes + +- 358c6f7: The `useApp` and `useRouteRef` functions are now forwards compatible with the new frontend system. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/create-app@0.7.7 + +### Patch Changes + +- 336db00: Bumped create-app version. +- 2bae83a: Updated engines to support Node 22 or 24 +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @backstage/dev-utils@1.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/ui@0.10.0 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/app-defaults@1.7.3 + - @backstage/integration-react@1.2.13 + +## @backstage/frontend-app-api@0.13.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-defaults@0.3.4 + +## @backstage/frontend-defaults@0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/frontend-app-api@0.13.3 + +## @backstage/frontend-dynamic-feature-loader@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/frontend-plugin-api@0.13.2 + +### Patch Changes + +- 75683ed: Added a new `errorPresentation` prop to `ExtensionBoundary` to control how errors are presented to the user. The default is `'error-display'`, which is the current behavior of showing the error in the `ErrorDisplay` component. The new option is `'error-api'`, posts errors to the `ErrorApi` and does not allow retries. + + The `AppRootElementBlueprint` now wraps its element in an `ErrorBoundary` using the new `'error-api'` presentation mode. + +- 0bc1ce9: Fixed a versioning conflict that could result in a `.withContext` is not a function error. + +- f3f84f1: Made the return type of `.withOverrides` to be simplified. + +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. + +- 9b8bde4: Removed unnecessary dependencies on `@backstage/core-components`, `@backstage/config`, `@material-ui/core`, and `lodash`. + +## @backstage/frontend-test-utils@0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/test-utils@1.7.14 + - @backstage/frontend-app-api@0.13.3 + +## @backstage/integration-react@1.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/repo-tools@0.16.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. + +- 85895f9: Updates OpenAPI generator templates to preserve original property names (like 'group-name', 'user-id') from OpenAPI specs when propertyNaming=original is specified. Previously, these were always converted to camelCase regardless of the propertyNaming setting. + + - Updates modelGeneric.mustache templates in both client and server generators + - Updates modelTaggedUnion.mustache templates in both client and server generators + - Uses {{baseName}} when available, falls back to {{name}} for backward compatibility + - Maintains backward compatibility - no changes when propertyNaming=original is not used + +- 2bae83a: Bump `@microsoft/api-documenter` and `@microsoft/api-extractor` to latest versions. + +- d1e38a7: Properly create workspace in OS temporary directory for `generate-patch` command + +- 2bae83a: Bumped dev dependencies `@types/node` + +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + +## @techdocs/cli@1.10.3 + +### Patch Changes + +- 43629b1: Updated to use new utilities from `@backstage/cli-common`. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-techdocs-node@1.13.10 + - @backstage/cli-common@0.1.16 + +## @backstage/test-utils@1.7.14 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/theme@0.7.1 + +### Patch Changes + +- fa06f6b: Added a `themeName` prop to `UnifiedThemeProvider`, enabling Backstage UI `data-theme-name` CSS attribute to be set based on active theme. + +## @backstage/plugin-api-docs@0.13.2 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/plugin-catalog@1.32.1 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-app@0.3.3 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- f7bc228: Support to set `defaultLanguage` and `availableLanguages` for the app language API in the new frontend system +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-app-backend@0.5.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-app-node@0.1.40 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config-loader@1.10.7 + +## @backstage/plugin-app-node@0.1.40 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config-loader@1.10.7 + +## @backstage/plugin-app-visualizer@0.1.26 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/ui@0.10.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-auth@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/theme@0.7.1 + +## @backstage/plugin-auth-backend@0.25.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.15 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.15 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-node@0.6.10 + +### Patch Changes + +- 2389358: remove leading dot in auth cookie cleanup call +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- b35f8b2: Fixed chunked cookie replacing edge case in OAuthCookieManager class where some of the old chunks would not get removed if new chunked cookie would have fewer chunks. +- e9dd634: fix flawed cookie removal logic with chunked tokens +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-auth-react@0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-bitbucket-cloud-common@0.3.5 + +### Patch Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +- Updated dependencies + - @backstage/integration@1.19.0 + +## @backstage/plugin-catalog@1.32.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- 91f5ed8: Fixed `catalogAboutEntityCard` to filter icon links before calling useProps(), preventing side effects from hooks in filtered-out links +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/core-compat-api@0.5.5 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder-common@1.7.4 + - @backstage/plugin-techdocs-react@1.3.6 + +## @backstage/plugin-catalog-backend-module-aws@0.4.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-azure@0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.17 + +### Patch Changes + +- ed5a7a3: Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. + + When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances. + Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-catalog-backend-module-github@0.12.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.6 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- fb029b6: Updated luxon types +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-logs@0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-scaffolder-common@1.7.4 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.12 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-graph@0.5.4 + +### Patch Changes + +- a67c144: Fixed zoom out description (replace `&` with &) +- f3f84f1: Minor extension type updates after frontend API bump +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-catalog-import@0.13.8 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-catalog-node@1.20.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-catalog-react@1.21.4 + +### Patch Changes + +- 6d39141: Fixed an issue where `EntityOwnerPicker` failed to filter options when the input text contained uppercase characters. +- b3c0594: Use a versioned context for `useEntityList`, to better work with mixed `@backstage/plugin-catalog-react` versions. +- c51c901: $contains may have string value in an entity filter. Typescript type and Zod parser were not the same. +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/core-compat-api@0.5.5 + - @backstage/frontend-test-utils@0.4.2 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.24 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. + +- df4d646: Moved types, API and client to the common package, allowing both frontend and + backend plugins to use the `CatalogUnprocessedEntitiesClient`. + + The following types, clients and interfaces have been deprecated and should be + imported from the `@backstage/plugin-catalog-unprocessed-entities-common` instead: + `CatalogUnprocessedEntitiesApi`, `CatalogUnprocessedEntitiesApiResponse`, `UnprocessedEntity`, + `UnprocessedEntityCache`, `UnprocessedEntityError`, `CatalogUnprocessedEntitiesClient`. + + All those types, clients and interfaces are re-exported temporarily in the + `@backstage/plugin-catalog-unprocessed-entities` package until cleaned up. + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.12 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.12 + +### Patch Changes + +- df4d646: Moved types, API and client to the common package, allowing both frontend and + backend plugins to use the `CatalogUnprocessedEntitiesClient`. + + The following types, clients and interfaces have been deprecated and should be + imported from the `@backstage/plugin-catalog-unprocessed-entities-common` instead: + `CatalogUnprocessedEntitiesApi`, `CatalogUnprocessedEntitiesApiResponse`, `UnprocessedEntity`, + `UnprocessedEntityCache`, `UnprocessedEntityError`, `CatalogUnprocessedEntitiesClient`. + + All those types, clients and interfaces are re-exported temporarily in the + `@backstage/plugin-catalog-unprocessed-entities` package until cleaned up. + +## @backstage/plugin-config-schema@0.1.75 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-devtools@0.1.34 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- 291bf9d: Added scheduled tasks UI feature for the DevTools plugin +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-devtools-common@0.1.20 + - @backstage/core-compat-api@0.5.5 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-devtools-backend@0.5.12 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 291bf9d: Added scheduled tasks UI feature for the DevTools plugin +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + - @backstage/plugin-devtools-common@0.1.20 + +## @backstage/plugin-devtools-common@0.1.20 + +### Patch Changes + +- 291bf9d: Added scheduled tasks UI feature for the DevTools plugin + +## @backstage/plugin-events-backend@0.5.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.18 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-azure@0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-github@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-gitlab@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-events-backend-test-utils@0.1.51 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + +## @backstage/plugin-events-node@0.4.18 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-gateway-backend@1.1.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 7e860dd: Updated dependency `eventsource` to `^4.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-home@0.8.15 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-home-react@0.1.33 + - @backstage/theme@0.7.1 + +## @backstage/plugin-home-react@0.1.33 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-kubernetes@0.12.14 + +### Patch Changes + +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-kubernetes-react@0.5.14 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-kubernetes-cluster@0.0.32 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.5.14 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-permission-react@0.4.39 + +## @backstage/plugin-kubernetes-common@0.9.9 + +### Patch Changes + +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas + +## @backstage/plugin-kubernetes-react@0.5.14 + +### Patch Changes + +- f966a85: Enabled a pod terminal at GKE +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-mcp-actions-backend@0.1.6 + +### Patch Changes + +- e83e038: Added `@cfworker/json-schema` as a dependency to this package part of the `@modelcontextprotocol/sdk` bump as it's required in the types +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 79ef471: Clarify error handling in readme and update handleError.ts to include all backstage/errors +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-mui-to-bui@0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + +## @backstage/plugin-notifications@0.5.12 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. + +- 53347cc: Move long notification descriptions behind `Show more/less` button. + + This improves readability of the notifications list by preventing long descriptions from taking up too much space + or rendering very small scrollable areas. + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-signals-react@0.0.18 + +## @backstage/plugin-notifications-backend@0.6.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## @backstage/plugin-notifications-backend-module-email@0.3.17 + +### Patch Changes + +- a5d5b3a: SES config for the notification email processor now supports utilizing an ARN for the SES identity when sending an email after the SES SDK V2 update. + + The `sesConfig.fromArn` will set the `fromEmailAddressIdentityArn` option for the SES `SendEmailCommand`. The `sesConfig.sourceArn` field is removed since no equivalent option is available in the send email command options. Setting `sesConfig.sourceArn` will have no effect and log a warning. Example changes: + + ```diff + notifications: + processors: + email: + transportConfig: + transport: "ses" + region: "us-west-2" + sender: "sender@mycompany.com" + replyTo: "no-reply@mycompany.com" + sesConfig: + - sourceArn: "arn:aws:ses:us-west-2:123456789012:identity/example.com" + fromArn: "arn:aws:ses:us-west-2:123456789012:identity/example.com" + ``` + +- b267aea: Updated dependency `@types/nodemailer` to `^7.0.0`. + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## @backstage/plugin-notifications-node@0.2.22 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-org@0.6.47 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-org-react@0.1.45 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-permission-backend@0.7.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-permission-node@0.10.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-permission-react@0.4.39 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-proxy-backend@0.6.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-proxy-node@0.1.11 + +## @backstage/plugin-proxy-node@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16 + +### Patch Changes + +- 5a6aca2: Improve error message when provided target branch is missing +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.3 + +### Patch Changes + +- dab3d3f: Added autocompletion resource `owners` for retrieving GitHub repository owners. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-notifications-node@0.2.22 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.16 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.6 + +## @backstage/plugin-scaffolder-common@1.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + +## @backstage/plugin-scaffolder-node@0.12.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-common@1.7.4 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.10.2 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-react@1.19.4 + +### Patch Changes + +- fb029b6: Updated luxon types +- 5ca461e: Fixed bug where custom `review.name` values were incorrectly formatted by `startCase`, preserving them exactly as written. +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- 9b38f22: Updated dependency `use-immer` to `^0.11.0`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder-common@1.7.4 + +## @backstage/plugin-search@1.5.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-search-backend@2.0.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-search-backend-module-catalog@0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-search-backend-module-explore@0.3.10 + +### Patch Changes + +- 9b69262: Updated dependency `@backstage-community/plugin-explore-common` to `^0.9.0`. +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-search-backend-module-pg@0.5.51 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.16 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-search-backend-module-techdocs@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-techdocs-node@1.13.10 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-search-react@1.10.1 + +### Patch Changes + +- 8947a4e: Skip the very first empty search when going to the landing page +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + +## @backstage/plugin-signals@0.0.26 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-signals-react@0.0.18 + +## @backstage/plugin-signals-backend@0.3.11 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-signals-node@0.1.27 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-signals-react@0.0.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-techdocs@1.16.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- 5c33821: Fixed schema type for additionalAllowedURIProtocols +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- 592361e: The `techdocs` config is now marked as optional. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-techdocs-react@1.3.6 + +## @backstage/plugin-techdocs-backend@2.1.3 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 2bae83a: Corrected `ErrorCallback` type to work with Node 22 types +- 592361e: The `techdocs` config is now marked as optional. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-techdocs-node@1.13.10 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + +### Patch Changes + +- 8d6709e: Updated tests to match test-utils change +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-techdocs-react@1.3.6 + +## @backstage/plugin-techdocs-node@1.13.10 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 703f8c0: There was an issue in the uploading of large size files to the AWS S3. We have modified the logic by adding retry along with multipart uploading functionality. +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/backend-plugin-api@1.6.0 + +## @backstage/plugin-techdocs-react@1.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @backstage/plugin-user-settings@0.8.30 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-signals-react@0.0.18 + +## @backstage/plugin-user-settings-backend@0.3.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + +## example-app@0.2.116 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/core-components@0.18.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-user-settings@0.8.30 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-devtools@0.1.34 + - @backstage/plugin-home@0.8.15 + - @backstage/theme@0.7.1 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/app-defaults@1.7.3 + - @backstage/frontend-app-api@0.13.3 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-mui-to-bui@0.2.2 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs-react@1.3.6 + +## example-app-next@0.0.30 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-app@0.3.3 + - @backstage/plugin-org@0.6.47 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/core-components@0.18.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-app-visualizer@0.1.26 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-user-settings@0.8.30 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-home@0.8.15 + - @backstage/theme@0.7.1 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/app-defaults@1.7.3 + - @backstage/core-compat-api@0.5.5 + - @backstage/frontend-app-api@0.13.3 + - @backstage/frontend-defaults@0.3.4 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-auth@0.1.3 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs-react@1.3.6 + +## app-next-example-plugin@0.0.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + +## example-backend@0.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mcp-actions-backend@0.1.6 + - @backstage/plugin-scaffolder-backend@3.1.0 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-devtools-backend@0.5.12 + - @backstage/plugin-events-backend@0.5.9 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-search-backend@2.0.9 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.3 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-search-backend-module-explore@0.3.10 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17 + - @backstage/plugin-search-backend-module-catalog@0.3.11 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9 + - @backstage/plugin-search-backend-module-techdocs@0.4.9 + +## e2e-test@0.2.35 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.7 + - @backstage/cli-common@0.1.16 + +## @internal/frontend@0.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## @internal/scaffolder@0.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-scaffolder-react@1.19.4 + +## techdocs-cli-embedded-app@0.2.115 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/core-components@0.18.4 + - @backstage/test-utils@1.7.14 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/app-defaults@1.7.3 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-techdocs-react@1.3.6 + +## yarn-plugin-backstage@0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @internal/plugin-todo-list@1.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## @internal/plugin-todo-list-backend@1.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 diff --git a/docs/releases/v1.46.0-next.0-changelog.md b/docs/releases/v1.46.0-next.0-changelog.md new file mode 100644 index 0000000000..6947caf67a --- /dev/null +++ b/docs/releases/v1.46.0-next.0-changelog.md @@ -0,0 +1,2269 @@ +# Release v1.46.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.46.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.46.0-next.0) + +## @backstage/backend-app-api@1.4.0-next.0 + +### Minor Changes + +- 5a2d538: Introduced backend startup result tracking and error handling. The `Backend.start()` method now returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed. + + This also improves the default error message when backend startup fails, and of course makes it possible to craft your own custom error reporting based on the startup results. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/backend-defaults@0.14.0-next.0 + +### Minor Changes + +- fa43826: Move `better-sqlite3` from dependencies to peer dependencies + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-app-api@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/backend-dev-utils@0.1.5 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend@3.1.0-next.0 + +### Minor Changes + +- a4cd405: Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity. + +### Patch Changes + +- 8f4aded: Fixing OpenAPI definition +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.10.1-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-search-backend-node@1.4.0-next.0 + +### Minor Changes + +- 4d3ddb9: Improving method that search tokenizer breaks apart entity names + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/app-defaults@1.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## @backstage/backend-dynamic-feature-service@0.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-backend@0.5.9-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-app-node@0.1.40-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/backend-openapi-utils@0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/backend-plugin-api@1.5.1-next.0 + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/backend-test-utils@1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-app-api@1.4.0-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/cli@0.34.6-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/release-manifests@0.0.13 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/types@1.2.2 + +## @backstage/cli-common@0.1.16-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks + +## @backstage/cli-node@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/codemods@0.1.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + +## @backstage/config-loader@1.10.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-app-api@1.19.3-next.0 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/core-compat-api@0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/core-components@0.18.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/version-bridge@1.0.11 + +## @backstage/core-plugin-api@1.12.1-next.0 + +### Patch Changes + +- 358c6f7: The `useApp` and `useRouteRef` functions are now forwards compatible with the new frontend system. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/create-app@0.7.7-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + +## @backstage/dev-utils@1.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-model@1.7.6 + +## @backstage/frontend-app-api@0.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/frontend-defaults@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/frontend-dynamic-feature-loader@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/config@1.3.6 + +## @backstage/frontend-plugin-api@0.13.2-next.0 + +### Patch Changes + +- 0bc1ce9: Fixed a versioning conflict that could result in a `.withContext` is not a function error. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/frontend-test-utils@0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/integration@1.18.3-next.0 + +### Patch Changes + +- e15fdae: Made the github urls case insensitive. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/integration-react@1.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/config@1.3.6 + +## @backstage/repo-tools@0.16.1-next.0 + +### Patch Changes + +- 85895f9: Updates OpenAPI generator templates to preserve original property names (like 'group-name', 'user-id') from OpenAPI specs when propertyNaming=original is specified. Previously, these were always converted to camelCase regardless of the propertyNaming setting. + + - Updates modelGeneric.mustache templates in both client and server generators + - Updates modelTaggedUnion.mustache templates in both client and server generators + - Uses {{baseName}} when available, falls back to {{name}} for backward compatibility + - Maintains backward compatibility - no changes when propertyNaming=original is not used + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @techdocs/cli@1.10.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-techdocs-node@1.13.10-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## @backstage/test-utils@1.7.14-next.0 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/theme@0.7.1-next.0 + +### Patch Changes + +- fa06f6b: Added a `themeName` prop to `UnifiedThemeProvider`, enabling Backstage UI `data-theme-name` CSS attribute to be set based on active theme. + +## @backstage/ui@0.9.1-next.0 + +### Patch Changes + +- b3ad928: Fixed Table Row component to correctly handle cases where no `href` is provided, preventing unnecessary router provider wrapping and fixing the cursor incorrectly showing as a pointer despite the element not being a link. + + Affected components: Row + +- fe7c751: Fixed `useTable` hook to prioritize `providedRowCount` over data length for accurate row count in server-side pagination scenarios. + +- c145031: Fixed Table column sorting indicator to show up arrow when no sort is active, correctly indicating that clicking will sort ascending. + + Affected components: Column + +## @backstage/plugin-api-docs@0.13.2-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-app@0.3.3-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/plugin-app-backend@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-app-node@0.1.40-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-app-node@0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config-loader@1.10.7-next.0 + +## @backstage/plugin-app-visualizer@0.1.26-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + +## @backstage/plugin-auth@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend@0.25.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-auth-backend@0.25.7-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-auth-backend@0.25.7-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-node@0.6.10-next.0 + +### Patch Changes + +- 2389358: remove leading dot in auth cookie cleanup call +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-react@0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + +## @backstage/plugin-catalog@1.32.1-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-catalog-backend@3.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-backend-module-aws@0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-catalog-backend-module-azure@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-github@0.11.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-backend-module-github@0.11.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.6-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-logs@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.11 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-graph@0.5.4-next.0 + +### Patch Changes + +- a67c144: Fixed zoom out description (replace `&` with &) +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-import@0.13.8-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-catalog-node@1.20.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-react@1.21.4-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/frontend-test-utils@0.4.2-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-config-schema@0.1.75-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-devtools@0.1.34-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-devtools-common@0.1.19 + +## @backstage/plugin-devtools-backend@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.19 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-events-backend@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-azure@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-events-backend-module-github@0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-gitlab@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-kafka@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-test-utils@0.1.51-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.0 + +## @backstage/plugin-events-node@0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-gateway-backend@1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-home@0.8.15-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-home-react@0.1.33-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-home-react@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + +## @backstage/plugin-kubernetes@0.12.14-next.0 + +### Patch Changes + +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-kubernetes-react@0.5.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-kubernetes-backend@0.20.5-next.0 + +### Patch Changes + +- e9589d9: Replace `@aws-sdk/signature-v4` with `@smithy/signature-v4`, + as stated in the [package documentation](https://www.npmjs.com/package/@aws-sdk/signature-v4?activeTab=readme) +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-kubernetes-node@0.3.7-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-kubernetes-cluster@0.0.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.5.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-kubernetes-node@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-kubernetes-react@0.5.14-next.0 + +### Patch Changes + +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.8 + +## @backstage/plugin-mcp-actions-backend@0.1.6-next.0 + +### Patch Changes + +- 79ef471: Clarify error handling in readme and update handleError.ts to include all backstage/errors +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-mui-to-bui@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + +## @backstage/plugin-notifications@0.5.12-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-signals-react@0.0.18-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-notifications-backend@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-notifications-backend-module-email@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-notifications-backend-module-slack@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-notifications-node@0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-org@0.6.47-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-org-react@0.1.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-permission-backend@0.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-permission-node@0.10.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-permission-react@0.4.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-proxy-backend@0.6.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-proxy-node@0.1.11-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-proxy-node@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + +## @backstage/plugin-scaffolder@1.34.4-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.6-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-common@1.7.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-scaffolder-node@0.12.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/backend-test-utils@1.10.1-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-react@1.19.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/plugin-search@1.5.1-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend@2.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-catalog@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-explore@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-pg@0.5.51-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-techdocs@0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-techdocs-node@1.13.10-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-react@1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-signals@0.0.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-signals-react@0.0.18-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals-backend@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals-node@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals-react@0.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs@1.16.1-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + +## @backstage/plugin-techdocs-backend@2.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-techdocs-node@1.13.10-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/core-components@0.18.4-next.0 + +## @backstage/plugin-techdocs-node@1.13.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-techdocs-react@1.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings@0.8.30-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-signals-react@0.0.18-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.116-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.5.4-next.0 + - @backstage/ui@0.9.1-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.0 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + - @backstage/plugin-scaffolder@1.34.4-next.0 + - @backstage/plugin-api-docs@0.13.2-next.0 + - @backstage/plugin-devtools@0.1.34-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/plugin-search@1.5.1-next.0 + - @backstage/plugin-home@0.8.15-next.0 + - @backstage/plugin-org@0.6.47-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.2-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## example-app-next@0.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.5.4-next.0 + - @backstage/ui@0.9.1-next.0 + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.0 + - @backstage/plugin-app-visualizer@0.1.26-next.0 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + - @backstage/plugin-scaffolder@1.34.4-next.0 + - @backstage/plugin-api-docs@0.13.2-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/plugin-search@1.5.1-next.0 + - @backstage/plugin-home@0.8.15-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/plugin-org@0.6.47-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/plugin-auth@0.1.3-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## app-next-example-plugin@0.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-components@0.18.4-next.0 + +## example-backend@0.0.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-scaffolder-backend@3.1.0-next.0 + - @backstage/plugin-kubernetes-backend@0.20.5-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.6-next.0 + - @backstage/plugin-search-backend@2.0.9-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.11-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.10-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.9-next.0 + - @backstage/plugin-app-backend@0.5.9-next.0 + - @backstage/plugin-auth-backend@0.25.7-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.0 + - @backstage/plugin-permission-backend@0.7.7-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-backend@0.5.9-next.0 + - @backstage/plugin-proxy-backend@0.6.9-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/plugin-devtools-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.0 + - @backstage/plugin-notifications-backend@0.6.1-next.0 + - @backstage/plugin-signals-backend@0.3.11-next.0 + - @backstage/plugin-techdocs-backend@2.1.3-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-permission-common@0.9.3 + +## e2e-test@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/create-app@0.7.7-next.0 + - @backstage/errors@1.2.7 + +## @internal/frontend@0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @internal/scaffolder@0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + +## techdocs-cli-embedded-app@0.2.115-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## yarn-plugin-backstage@0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/release-manifests@0.0.13 + - @backstage/errors@1.2.7 + +## @internal/plugin-todo-list@1.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + +## @internal/plugin-todo-list-backend@1.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 diff --git a/docs/releases/v1.46.0-next.1-changelog.md b/docs/releases/v1.46.0-next.1-changelog.md new file mode 100644 index 0000000000..42a188ec7f --- /dev/null +++ b/docs/releases/v1.46.0-next.1-changelog.md @@ -0,0 +1,392 @@ +# Release v1.46.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.46.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.46.0-next.1) + +## @backstage/plugin-techdocs-addons-test-utils@2.0.0-next.1 + +### Major Changes + +- 8d6709e: **BREAKING**: `TechDocsAddonTester.renderWithEffects()` no longer returns a screen; this means that you can no longer grab assertions such as `getByText` from its return value. + + Newer versions of `@testing-library` recommends using the `screen` export for assertions - and removing this from the addon tester contract allows us to more freely iterate on which underlying version of the testing library is being used. + + One notable effect of this, however, is that the `@testing-library` `screen` does NOT support assertions on the shadow DOM, which techdocs relies on. You will therefore want to add a dependency on [the `shadow-dom-testing-library` package](https://github.com/konnorrogers/shadow-dom-testing-library/) in your tests, and using its `screen` and its dedicated `*Shadow*` methods. As an example, if you keep doing `getByText` you will not get matches inside the shadow DOM - switch to `getByShadowText` instead. + + ```ts + import { screen } from 'shadow-dom-testing-library'; + + // ... render the addon ... + await TechDocsAddonTester.buildAddonsInTechDocs([]) + .withDom(TEST_CONTENT) + .renderWithEffects(); + + expect(screen.getByShadowText('TEST_CONTENT')).toBeInTheDocument(); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + +## @backstage/ui@0.10.0-next.1 + +### Minor Changes + +- 16543fa: **Breaking change** The `Cell` component has been refactored to be a generic wrapper component that accepts `children` for custom cell content. The text-specific functionality (previously part of `Cell`) has been moved to a new `CellText` component. + + ### Migration Guide + + If you were using `Cell` with text-specific props (`title`, `description`, `leadingIcon`, `href`), you need to update your code to use `CellText` instead: + + **Before:** + + ```tsx + } + href="/path" + /> + ``` + + **After:** + + ```tsx + } + href="/path" + /> + ``` + + For custom cell content, use the new generic `Cell` component: + + ```tsx + {/* Your custom content */} + ``` + +### Patch Changes + +- 50b7927: Fixed Checkbox indicator showing checkmark color when unchecked. + + Affected components: Checkbox + +- 5bacf55: Fixed `ButtonIcon` incorrectly applying `className` to inner elements instead of only the root element. + + Affected components: ButtonIcon + +- a20d317: Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode. + + Affected components: Table, TableHeader, Row, Column + +## @backstage/cli@0.34.6-next.1 + +### Patch Changes + +- 7fbac5c: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-node@0.2.16-next.1 + - @backstage/cli-common@0.1.16-next.1 + +## @backstage/cli-common@0.1.16-next.1 + +### Patch Changes + +- 5cfb2a4: Added new `run`, `runOutput`, and `runCheck` utilities to help run child processes in a safe and portable way. + +## @backstage/cli-node@0.2.16-next.1 + +### Patch Changes + +- 4e8c726: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## @backstage/codemods@0.1.53-next.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## @backstage/core-components@0.18.4-next.1 + +### Patch Changes + +- 9a942a4: Fixed bug in the `LogViewer` component where shift + click always opened a new window instead of just changing the selection. + + In addition, improved the `LogViewer` component by a few usability enhancements: + + - Added support for multiple selections using cmd/ctrl + click + - Improved the generated hash that is added to the URL to also support ranges & multiple selections + - Added an hover effect & info tooltip to the "Copy to clipboard" button to indicate its functionality + - Added some color and a separator to the line numbers to improve readability + +- 207c3c8: long words like urls now breaks to new line on warning panels instead of overflowing the container + +- 5d52dab: Add i18n support for LogViewer search control + +## @backstage/create-app@0.7.7-next.1 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## @backstage/dev-utils@1.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + +## @backstage/repo-tools@0.16.1-next.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- d1e38a7: Properly create workspace in OS temporary directory for `generate-patch` command +- Updated dependencies + - @backstage/cli-node@0.2.16-next.1 + - @backstage/cli-common@0.1.16-next.1 + +## @techdocs/cli@1.10.3-next.1 + +### Patch Changes + +- 43629b1: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## @backstage/plugin-app-visualizer@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.11.3-next.1 + +### Patch Changes + +- ed5a7a3: Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. + + When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances. + Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. + +## @backstage/plugin-catalog-backend-module-github-org@0.3.17-next.1 + +### Patch Changes + +- ed5a7a3: Introduce new configuration option to exclude suspended users from GitHub Enterprise instances. + + When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances. + Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail. + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.11.3-next.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.12-next.0 + +## @backstage/plugin-catalog-react@1.21.4-next.1 + +### Patch Changes + +- 6d39141: Fixed an issue where `EntityOwnerPicker` failed to filter options when the input text contained uppercase characters. +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + +### Patch Changes + +- df4d646: Moved types, API and client to the common package, allowing both frontend and + backend plugins to use the `CatalogUnprocessedEntitiesClient`. + + The following types, clients and interfaces have been deprecated and should be + imported from the `@backstage/plugin-catalog-unprocessed-entities-common` instead: + `CatalogUnprocessedEntitiesApi`, `CatalogUnprocessedEntitiesApiResponse`, `UnprocessedEntity`, + `UnprocessedEntityCache`, `UnprocessedEntityError`, `CatalogUnprocessedEntitiesClient`. + + All those types, clients and interfaces are re-exported temporarily in the + `@backstage/plugin-catalog-unprocessed-entities` package until cleaned up. + +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.12-next.0 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.12-next.0 + +### Patch Changes + +- df4d646: Moved types, API and client to the common package, allowing both frontend and + backend plugins to use the `CatalogUnprocessedEntitiesClient`. + + The following types, clients and interfaces have been deprecated and should be + imported from the `@backstage/plugin-catalog-unprocessed-entities-common` instead: + `CatalogUnprocessedEntitiesApi`, `CatalogUnprocessedEntitiesApiResponse`, `UnprocessedEntity`, + `UnprocessedEntityCache`, `UnprocessedEntityError`, `CatalogUnprocessedEntitiesClient`. + + All those types, clients and interfaces are re-exported temporarily in the + `@backstage/plugin-catalog-unprocessed-entities` package until cleaned up. + +## @backstage/plugin-kubernetes@0.12.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.5.14-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-kubernetes-backend@0.20.5-next.1 + +### Patch Changes + +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + - @backstage/plugin-kubernetes-node@0.3.7-next.1 + +## @backstage/plugin-kubernetes-cluster@0.0.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.5.14-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-kubernetes-common@0.9.9-next.0 + +### Patch Changes + +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas + +## @backstage/plugin-kubernetes-node@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-kubernetes-react@0.5.14-next.1 + +### Patch Changes + +- f966a85: Enabled a pod terminal at GKE +- 8fa8d87: Add Kubernetes Plugin Secrets Accordion with masked secret datas +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-mui-to-bui@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + +## @backstage/plugin-scaffolder-react@1.19.4-next.1 + +### Patch Changes + +- 5ca461e: Fixed bug where custom `review.name` values were incorrectly formatted by `startCase`, preserving them exactly as written. +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + +## @backstage/plugin-techdocs@1.16.1-next.1 + +### Patch Changes + +- 592361e: The `techdocs` config is now marked as optional. +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + +## @backstage/plugin-techdocs-backend@2.1.3-next.1 + +### Patch Changes + +- 592361e: The `techdocs` config is now marked as optional. + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + +### Patch Changes + +- 8d6709e: Updated tests to match test-utils change +- Updated dependencies + - @backstage/core-components@0.18.4-next.1 + +## example-app@0.2.116-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + - @backstage/plugin-mui-to-bui@0.2.2-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.1 + +## example-app-next@0.0.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + - @backstage/plugin-app-visualizer@0.1.26-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.1 + +## techdocs-cli-embedded-app@0.2.115-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 diff --git a/docs/releases/v1.46.0-next.2-changelog.md b/docs/releases/v1.46.0-next.2-changelog.md new file mode 100644 index 0000000000..0bb2bf687d --- /dev/null +++ b/docs/releases/v1.46.0-next.2-changelog.md @@ -0,0 +1,1897 @@ +# Release v1.46.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.46.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.46.0-next.2) + +## @backstage/backend-plugin-api@1.6.0-next.1 + +### Minor Changes + +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/cli@0.35.0-next.2 + +### Minor Changes + +- f8dff94: Switched the default module resolution to `bundler` and the `module` setting to `ES2020`. + + You may need to bump some dependencies as part of this change and fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages. + + You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript. + + A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field. + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 1226647: Updated dependency `esbuild` to `^0.27.0`. +- f89a074: Updated dependency `@pmmmwh/react-refresh-webpack-plugin` to `^0.6.0`. +- 2b81751: Updated dependency `webpack` to `~5.103.0`. +- fafd9e1: Fixed internal usage of `yargs`. +- 2bae83a: Switched ECMAScript version to ES2023. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/plugin-kubernetes-backend@0.21.0-next.2 + +### Minor Changes + +- 7f9846f: Add possibility to extends Kubernetes REST API. Add fetcher to parameters for custom objects provider + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- fb029b6: Updated luxon types +- Updated dependencies + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/plugin-kubernetes-node@0.4.0-next.2 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-kubernetes-node@0.4.0-next.2 + +### Minor Changes + +- 7f9846f: Add possibility to extends Kubernetes REST API. Add fetcher to parameters for custom objects provider + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.0-next.1 + +### Minor Changes + +- f2d034b: In the `gitlabRepoPush` action, add 'auto' possibility for `commitAction` input. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/backend-app-api@1.4.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/backend-defaults@0.14.0-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- aa79251: build(deps): bump `node-forge` from 1.3.1 to 1.3.2 +- f96edff: Allow configuration of the `referrerPolicy` +- fb029b6: Updated luxon types +- 847a330: Fix for `jose` types +- 25b560e: Internal change to support new versions of the `logform` library +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. +- 3016a79: Updated dependency `@types/archiver` to `^7.0.0`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/backend-dev-utils@0.1.6-next.0 + - @backstage/backend-app-api@1.4.0-next.1 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + +## @backstage/backend-dev-utils@0.1.6-next.0 + +### Patch Changes + +- 2bae83a: Internal update for Node.js v24 support. + +## @backstage/backend-dynamic-feature-service@0.7.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-app-node@0.1.40-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-events-backend@0.5.9-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/backend-openapi-utils@0.6.4-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/backend-test-utils@1.10.2-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 8be23a4: Switched `textextensions` dependency for `text-extensions`. +- 5a737e1: Fix PostgreSQL 18 `TestDatabases` by pinning the data directory +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/backend-app-api@1.4.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/cli-common@0.1.16-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/errors@1.2.7 + +## @backstage/codemods@0.1.53-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + +## @backstage/config-loader@1.10.7-next.1 + +### Patch Changes + +- 741c47a: Updated dependency `typescript-json-schema` to `^0.67.0`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-app-api@1.19.3-next.1 + +### Patch Changes + +- 75683ed: Added replay functionality to `AlertApiForwarder` to buffer and replay recent alerts to new subscribers, preventing missed alerts that were posted before subscription. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/core-components@0.18.4-next.2 + +### Patch Changes + +- 4c00303: Add `tooltipClasses` prop to `OverflowTooltip` component to allow customisation of the tooltip +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.1-next.0 + - @backstage/version-bridge@1.0.11 + +## @backstage/create-app@0.7.7-next.2 + +### Patch Changes + +- 2bae83a: Updated engines to support Node 22 or 24 +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + +## @backstage/frontend-plugin-api@0.13.2-next.1 + +### Patch Changes + +- 75683ed: Added a new `errorPresentation` prop to `ExtensionBoundary` to control how errors are presented to the user. The default is `'error-display'`, which is the current behavior of showing the error in the `ErrorDisplay` component. The new option is `'error-api'`, posts errors to the `ErrorApi` and does not allow retries. + + The `AppRootElementBlueprint` now wraps its element in an `ErrorBoundary` using the new `'error-api'` presentation mode. + +- f3f84f1: Made the return type of `.withOverrides` to be simplified. + +- Updated dependencies + - @backstage/core-components@0.18.4-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## @backstage/integration@1.18.3-next.1 + +### Patch Changes + +- fb029b6: Updated luxon types +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/repo-tools@0.16.1-next.2 + +### Patch Changes + +- 2bae83a: Bump `@microsoft/api-documenter` and `@microsoft/api-extractor` to latest versions. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/errors@1.2.7 + +## @techdocs/cli@1.10.3-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-techdocs-node@1.13.10-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## @backstage/plugin-api-docs@0.13.2-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- Updated dependencies + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## @backstage/plugin-app@0.3.3-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- f7bc228: Support to set `defaultLanguage` and `availableLanguages` for the app language API in the new frontend system +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## @backstage/plugin-app-backend@0.5.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-app-node@0.1.40-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-app-node@0.1.40-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + +## @backstage/plugin-auth-backend@0.25.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.7-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.15-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.7-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + +## @backstage/plugin-auth-node@0.6.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- e9dd634: fix flawed cookie removal logic with chunked tokens +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog@1.32.1-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- 91f5ed8: Fixed `catalogAboutEntityCard` to filter icon links before calling useProps(), preventing side effects from hooks in filtered-out links +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + +## @backstage/plugin-catalog-backend@3.2.1-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-backend-module-aws@0.4.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-github@0.11.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-backend-module-github@0.11.3-next.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.6-next.1 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- fb029b6: Updated luxon types +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-logs@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.12-next.0 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-graph@0.5.4-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-node@1.20.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-react@1.21.4-next.2 + +### Patch Changes + +- b3c0594: Use a versioned context for `useEntityList`, to better work with mixed `@backstage/plugin-catalog-react` versions. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-test-utils@0.4.2-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## @backstage/plugin-devtools-backend@0.5.12-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.19 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-events-backend@0.5.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-azure@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-events-backend-module-github@0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-gitlab@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-backend-module-kafka@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-events-node@0.4.18-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-gateway-backend@1.1.1-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-home@0.8.15-next.1 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home-react@0.1.33-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + +## @backstage/plugin-home-react@0.1.33-next.1 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/core-plugin-api@1.12.1-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.32-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + - @backstage/plugin-kubernetes-react@0.5.14-next.1 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## @backstage/plugin-mcp-actions-backend@0.1.6-next.1 + +### Patch Changes + +- e83e038: Added `@cfworker/json-schema` as a dependency to this package part of the `@modelcontextprotocol/sdk` bump as it's required in the types +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-notifications-backend@0.6.1-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.17-next.1 + +### Patch Changes + +- b267aea: Updated dependency `@types/nodemailer` to `^7.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## @backstage/plugin-notifications-node@0.2.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.0 + +## @backstage/plugin-org@0.6.47-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-common@1.1.7 + +## @backstage/plugin-permission-backend@0.7.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-permission-node@0.10.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-proxy-backend@0.6.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-proxy-node@0.1.11-next.1 + +## @backstage/plugin-proxy-node@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + +## @backstage/plugin-scaffolder@1.34.4-next.1 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.2 + - @backstage/integration@1.18.3-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + +## @backstage/plugin-scaffolder-backend@3.1.0-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 2bae83a: Updated `isolated-vm` to `6.0.1` +- 25b560e: Internal change to support new versions of the `logform` library +- 1226647: Updated dependency `esbuild` to `^0.27.0`. +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.0-next.1 + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.1 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.16-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.16-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.1 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.1 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.5-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.1 + +### Patch Changes + +- 5a6aca2: Improve error message when provided target branch is missing +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.16-next.1 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.6-next.1 + +## @backstage/plugin-scaffolder-node@0.12.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.10.2-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + +## @backstage/plugin-scaffolder-react@1.19.4-next.2 + +### Patch Changes + +- fb029b6: Updated luxon types +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- 9b38f22: Updated dependency `use-immer` to `^0.11.0`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-scaffolder-common@1.7.4-next.0 + +## @backstage/plugin-search@1.5.1-next.1 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + +## @backstage/plugin-search-backend@2.0.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-catalog@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-explore@0.3.10-next.1 + +### Patch Changes + +- 9b69262: Updated dependency `@backstage-community/plugin-explore-common` to `^0.9.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-pg@0.5.51-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-module-techdocs@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.13.10-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-search-backend-node@1.4.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-signals-backend@0.3.11-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals-node@0.1.27-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs@1.16.1-next.2 + +### Patch Changes + +- f3f84f1: Minor extension type updates after frontend API bump +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + +## @backstage/plugin-techdocs-backend@2.1.3-next.2 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 2bae83a: Corrected `ErrorCallback` type to work with Node 22 types +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-techdocs-node@1.13.10-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## @backstage/plugin-techdocs-node@1.13.10-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 703f8c0: There was an issue in the uploading of large size files to the AWS S3. We have modified the logic by adding retry along with multipart uploading functionality. +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings-backend@0.3.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.116-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-catalog-graph@0.5.4-next.1 + - @backstage/plugin-api-docs@0.13.2-next.1 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/plugin-search@1.5.1-next.1 + - @backstage/plugin-org@0.6.47-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.2 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home@0.8.15-next.1 + - @backstage/plugin-scaffolder@1.34.4-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/plugin-devtools@0.1.34-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-mui-to-bui@0.2.2-next.1 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + +## example-app-next@0.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-catalog-graph@0.5.4-next.1 + - @backstage/plugin-api-docs@0.13.2-next.1 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/plugin-search@1.5.1-next.1 + - @backstage/plugin-app@0.3.3-next.1 + - @backstage/plugin-org@0.6.47-next.1 + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.2 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home@0.8.15-next.1 + - @backstage/plugin-scaffolder@1.34.4-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-app-visualizer@0.1.26-next.1 + - @backstage/plugin-auth@0.1.3-next.0 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + +## example-backend@0.0.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mcp-actions-backend@0.1.6-next.1 + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-app-backend@0.5.9-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.1 + - @backstage/plugin-auth-backend@0.25.7-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-devtools-backend@0.5.12-next.1 + - @backstage/plugin-events-backend@0.5.9-next.1 + - @backstage/plugin-kubernetes-backend@0.21.0-next.2 + - @backstage/plugin-notifications-backend@0.6.1-next.1 + - @backstage/plugin-permission-backend@0.7.7-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/plugin-proxy-backend@0.6.9-next.1 + - @backstage/plugin-scaffolder-backend@3.1.0-next.1 + - @backstage/plugin-search-backend@2.0.9-next.1 + - @backstage/plugin-signals-backend@0.3.11-next.1 + - @backstage/plugin-techdocs-backend@2.1.3-next.2 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.10-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.2 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.1 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.11-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.9-next.1 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + +## techdocs-cli-embedded-app@0.2.115-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + +## @internal/plugin-todo-list-backend@1.0.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 diff --git a/docs/releases/v1.46.0.md b/docs/releases/v1.46.0.md new file mode 100644 index 0000000000..9ee4c68681 --- /dev/null +++ b/docs/releases/v1.46.0.md @@ -0,0 +1,129 @@ +--- +id: v1.46.0 +title: v1.46.0 +description: Backstage Release v1.46.0 +--- + +These are the release notes for the v1.46.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 + +To begin with, this release has a few important framework level updates. + +### **BREAKING:** Updated supported Node.js versions to 22 and 24 + +This release moves up the supported Node.js versions to 22 and 24, according to our [versioning policy](https://backstage.io/docs/overview/versioning-policy#nodejs-releases). As part of this change, we are also setting the compilation targets to ES2023, which lines up with the supported Node.js versions. + +The default TypeScript `target` compiler option is however left at ES2022. This is because older versions of TypeScript will error if they see an unsupported value, even if an override is set in your local `tsconfig.json`. You can still choose to set the TypeScript `target` to either `ES2023` or even `ES2024` in your own `tsconfig.json` if you prefer. + +### **BREAKING:** Switched modules to `ES2020` and resolution mode to `bundler` + +Now that the ecosystem has had some time to mature and adapt, we have finally switched over the default module resolution mode to `bundler` instead of `node`. The `module` setting was also changed to `ES2022`. + +You _may_ need to bump some dependencies as part of this change, or fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages, but if you depend on old versions or unmaintained packages, you may still notice some effects of this. + +You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings of your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript. + +A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field. + +### **BREAKING:** Moving toward updating `jest` and its DOM + +The `jest` test framework used to be a builtin dependency of the Backstage CLI. In this release, it has been made into a peer dependency to allow for easy upgrades to newer versions as you see fit. + +If you run tests using the Backstage CLI, you must therefore add Jest and its environment dependencies as `devDependencies` in your project root `package.json` when you upgrade. + +You can choose to install either Jest 29 or Jest 30. The built-in Jest version before this change was Jest 29, however, we recommend that you switch to Jest 30. Upgrading will solve the `Could not parse CSS stylesheet` errors, allow you to use MSW v2 in web packages, and ensure that you remain compatible with future versions of the Backstage CLI. Support for Jest 29 is temporary, with the purpose of allowing you to upgrade at your own pace, but it will eventually be removed. + +- **Jest 29**: Install `jest@^29` and `jest-environment-jsdom@^29` in your root `package.json`. No migration needed, but you may see `Could not parse CSS stylesheet` warnings/errors when testing components from `@backstage/ui` or other packages using CSS `@layer` declarations. +- **Jest 30**: Install `jest@^30`, `@types/jest@^30`, `@jest/environment-jsdom-abstract@^30`, and `jsdom@^27` in your root `package.json`. Fixes the stylesheet parsing warnings/errors, but requires migration steps. + +See the [Jest 30 migration guide](https://backstage.io/docs/tutorials/jest30-migration) for detailed migration instructions. + +### **BREAKING:** Now using correct configuration options for Valkey + +If you are using Valkey as your backend cache provider, take note. It used to piggyback on the Redis configuration format, but that was not quite correct. So its config options have been updated to better match its capabilities. + +So if you use Valkey, you need to upgrade your `app-config.yaml`: + +```diff +backend: + cache: + store: valkey + connection: ... + client: +- namespace: 'my-app' +- keyPrefixSeparator: ':' ++ keyPrefix: 'my-app:' +- clearBatchSize: 1000 +- useUnlink: false +``` + +Contributed by [@benjidotsh](https://github.com/benjidotsh) in [#31497](https://github.com/backstage/backstage/pull/31497) + +### **BREAKING:** Techdocs addon test utils removed explicit `screen` + +`TechdocsAddonTester.renderWithEffects()` used to return a `screen` that you could use for things like `getByText`. In order to support moving to newer versions of `@testing-library`, we have removed this, and you should import and use the normal `screen` from `@testing-library/react` instead. + +However: that `screen` can’t see inside the shadow DOM, which TechDocs uses. So if your tests need to look inside shadow DOM, you must install `shadow-dom-testing-library` and use its `screen` which provides special shadow-DOM queries, such as `getByShadowText` instead of `getByText. + +Example: + +```tsx +import { screen } from 'shadow-dom-testing-library'; +// ... render the addon ... +await TechDocsAddonTester.buildAddonsInTechDocs([]) + .withDom(TEST_CONTENT) + .renderWithEffects(); +expect(screen.getByShadowText('TEST_CONTENT')).toBeInTheDocument(); +``` + +### **BREAKING:** Backstage-UI advances! + +Again, Backstage-UI has a bunch of updates! One of them is breaking: If you were using the `Cell` component with text-specific props (`title` etc), you should now instead use the `CellText` component for that. Plain `Cell` is now meant to be used with generic child elements instead. + +### Simplified route compatibility + +If you are partway through a migration to the New Frontend System, you may enjoy the fact that the `useApp` and `useRouteRef` hooks now are forwards compatible with NFS. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs. + +### Support for Bitbucket Cloud OAuth + +Support for Bitbucket Cloud OAuth has been added - which was much welcome. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + +As part of this change, some breaking changes were made to low level auth getter functions, such that they now return a promise instead of a plain value. + +Contributed by [@jksmth](https://github.com/jksmth) in [#31848](https://github.com/backstage/backstage/pull/31848) + +### New Catalog actions + +There are two new MCP-usable actions on the block for registering and unregistering entities: `catalog:register-entity` and `catalog:unregister-entity`. + +Contributed by [@gabemontero](https://github.com/gabemontero) in [#32042](https://github.com/backstage/backstage/pull/32042) + +### New `RepoOwnerPicker` for Scaffolder + +A new field extension for selecting an organization or owner for a particular repository has been shipped! Currently, this only supports GitHub autocomplete, but contributions welcome in making this available for other providers! + +Contributed by [@benjidotsh](https://github.com/benjidotsh) in [#32105](https://github.com/backstage/backstage/pull/32105) + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.46.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://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.47.0-changelog.md b/docs/releases/v1.47.0-changelog.md new file mode 100644 index 0000000000..d71078327c --- /dev/null +++ b/docs/releases/v1.47.0-changelog.md @@ -0,0 +1,1467 @@ +# Release v1.47.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.47.0](https://backstage.github.io/upgrade-helper/?to=1.47.0) + +## @backstage/backend-defaults@0.15.0 + +### Minor Changes + +- 6fc00e6: Added action filtering support with glob patterns and attribute constraints. + + The `ActionsService` now supports filtering actions based on configuration. This allows controlling which actions are exposed to consumers like the MCP backend. + + Configuration example: + + ```yaml + backend: + actions: + pluginSources: + - catalog + - scaffolder + filter: + include: + - id: 'catalog:*' + attributes: + destructive: false + - id: 'scaffolder:*' + exclude: + - id: '*:delete-*' + - attributes: + readOnly: false + ``` + + Filtering logic: + + - `include`: Rules for actions to include. Each rule can specify an `id` glob pattern and/or `attributes` constraints. An action must match at least one rule to be included. If no include rules are specified, all actions are included by default. + - `exclude`: Rules for actions to exclude. Takes precedence over include rules. + - Each rule combines `id` and `attributes` with AND logic (both must match if specified). + +- 27f9061: **BREAKING**: The constructor for `FetchUrlReader` is now private. If you have to construct an instance of it, please use `FetchUrlReader.fromConfig` instead. + +- 27f9061: **BREAKING**: `coreServices.urlReader` now validates that redirect chains are subject to the allow list in `reading.allow` of your app config. If you were relying on redirects that pointed to URLs that were not allowlisted, you will now have to add those to your config as well. + + Example: + + ```diff + backend: + reading: + allow: + - host: example.com + + - host: storage-api.example.com + ``` + +### Patch Changes + +- 3afeab4: Implementing `readTree` for `GoogleGcsReader` +- c641c14: Wrap some of the action logic with `resolveSafeChildPath` and improve symlink handling when fetching remote and local files +- 7126bf2: Fixed a spelling mistake in root health service shutdown response. +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-app-api@1.4.1 + - @backstage/integration@1.19.2 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-node@0.10.8 + +## @backstage/frontend-app-api@0.14.0 + +### Minor Changes + +- 3bd2a1a: BREAKING: The ability for plugins to override APIs has been restricted to only allow overrides of APIs within the same plugin. For example, a plugin can no longer override any of the core APIs provided by the `app` plugin, this must be done with an `app` module instead. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/frontend-defaults@0.3.5 + +## @backstage/ui@0.11.0 + +### Minor Changes + +- 243e5e7: **BREAKING**: Redesigned Table component with new `useTable` hook API. + + - The `Table` component (React Aria wrapper) is renamed to `TableRoot` + - New high-level `Table` component that handles data display, pagination, sorting, and selection + - The `useTable` hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor) + - New types: `ColumnConfig`, `TableProps`, `TableItem`, `UseTableOptions`, `UseTableResult` + + New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. + + **Migration guide:** + + 1. Update imports and use the new `useTable` hook: + + ```diff + -import { Table, useTable } from '@backstage/ui'; + -const { data, paginationProps } = useTable({ data: items, pagination: {...} }); + +import { Table, useTable, type ColumnConfig } from '@backstage/ui'; + +const { tableProps } = useTable({ + + mode: 'complete', + + getData: () => items, + +}); + ``` + + 2. Define columns and render with the new Table API: + + ```diff + - + - ... + - ... + -
    + - + +const columns: ColumnConfig[] = [ + + { id: 'name', label: 'Name', isRowHeader: true, cell: item => }, + + { id: 'type', label: 'Type', cell: item => }, + +]; + + + + + ``` + + Affected components: Table, TableRoot, TablePagination + +- 95246eb: **Breaking** Updating color tokens to match the new neutral style on different surfaces. + + ## Migration notes + + There's no direct replacement for the old tint tokens but you can use the new neutral set of color tokens on surface 0 or 1 as a replacement. + + - `--bui-bg-tint` can be replaced by `--bui-bg-neutral-on-surface-0` + - `--bui-bg-tint-hover` can be replaced by `--bui-bg-neutral-on-surface-0-hover` + - `--bui-bg-tint-pressed` can be replaced by `--bui-bg-neutral-on-surface-0-pressed` + - `--bui-bg-tint-disabled` can be replaced by `--bui-bg-neutral-on-surface-0-disabled` + +- ea0c6d8: Introduce new `ToggleButton` & `ToggleButtonGroup` components in Backstage UI + +- 4ea1d15: **BREAKING**: Renamed CSS variable `--bui-bg` to `--bui-bg-surface-0` for consistency. + +### Patch Changes + +- 1880402: Fixes app background color on dark mode. + +- d2fdded: Added indeterminate state support to the Checkbox component for handling partial selection scenarios like table header checkboxes. + + Affected components: Checkbox + +- 4fb15d2: Added missing `aria-label` attributes to `SearchField` components in `Select`, `MenuAutocomplete`, and `MenuAutocompleteListbox` to fix accessibility warnings. + + Affected components: Select, MenuAutocomplete, MenuAutocompleteListbox + +- 21c87cc: Fixes disabled state in primary and secondary buttons in Backstage UI. + +- 9c76682: build(deps-dev): bump `storybook` from 10.1.9 to 10.1.10 + +- de80336: Fixed disabled tertiary buttons incorrectly showing hover effects on surfaces. + +- 133d5c6: Added new Popover component for Backstage UI with automatic overflow handling, and full placement support. Also introduced `--bui-shadow` token for consistent elevation styling across overlay components (Popover, Tooltip, Menu). + +- 973c839: Fixed Table sorting indicator not being visible when a column is actively sorted. + + Affected components: Table, Column + +- df40cfc: Fixed Menu component trigger button not toggling correctly. Removed custom click-outside handler that was interfering with React Aria's built-in state management, allowing the menu to properly open and close when clicking the trigger button. + +- b01ab96: Added support for column width configuration in Table component. Columns now accept `width`, `defaultWidth`, `minWidth`, and `maxWidth` props for responsive layout control. + + Affected components: Table, Column + +- b4a4911: Fixed SearchField `startCollapsed` prop not working correctly in Backstage UI. The field now properly starts in a collapsed state, expands when clicked and focused, and collapses back when unfocused with no input. Also fixed CSS logic to work correctly in all layout contexts (flex row, flex column, and regular containers). + + Affected components: SearchField + +- b3253b6: Fixed `Link` component causing hard page refreshes for internal routes. The component now properly uses React Router's navigation instead of full page reloads. + +- fe7fe69: Added support for custom pagination options in `useTable` hook and `Table` component. You can now configure `pageSizeOptions` to customize the page size dropdown, and hook into pagination events via `onPageSizeChange`, `onNextPage`, and `onPreviousPage` callbacks. When `pageSize` doesn't match any option, the first option is used and a warning is logged. + + Affected components: Table, TablePagination + +- cfac8a4: Fixed missing border styles on table selection cells in multi-select mode. + + Affected components: Table + +- 2532d2a: Added `className` and `style` props to the `Table` component. + + Affected components: Table + +## @backstage/plugin-app-react@0.1.0 + +### Minor Changes + +- 9ccf84e: Initial release of this web library for `@backstage/plugin-app`. + +### Patch Changes + +- 9ccf84e: Moved the following blueprints from `@backstage/frontend-plugin-api`: + + - `AppRootWrapperBlueprint` + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + +## @backstage/plugin-auth-backend@0.26.0 + +### Minor Changes + +- 7ffc873: Fix `user_created_at` migration causing `SQLiteError` regarding use of non-constants for defaults + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-auth-node@0.6.11 + +## @backstage/plugin-devtools-react@0.1.0 + +### Minor Changes + +- be6cef5: Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + +## @backstage/plugin-events-backend-module-kafka@0.3.0 + +### Minor Changes + +- ef5bbd8: Add support for Kafka offset configuration (`fromBeginning`) and `autoCommit` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-home@0.9.0 + +### Minor Changes + +- e091a83: Widget configurations are now only saved to storage when the Save button is explicitly clicked. Added a Cancel button that allows users to discard unsaved changes and revert to the last saved state. + +### Patch Changes + +- bdda543: Updated WidgetOverlay color to use `alpha(theme.palette.background.paper, 0.93)` for better theme alignment instead of hardcoded RGBA +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-home-react@0.1.34 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.0 + +### Minor Changes + +- ab606b2: Add ability to configure the API Base URL + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + +## @backstage/plugin-techdocs-node@1.14.0 + +### Minor Changes + +- 63c459c: **BREAKING:** It's now possible to use the credentials from the `integrations.awsS3` config to authenticate with AWS S3. The new priority is: + + 1. `aws.accounts` + 2. `techdocs.publisher.awsS3.credentials` + 3. `integrations.awsS3` + 4. Default credential chain + + In case of multiple `integrations.awsS3` are present, the target integration is determined by the `accessKeyId` in `techdocs.publisher.awsS3.credentials` if provided. Otherwise, the default credential chain is used. + + This means that depending on your setup, this feature may break your existing setup. + In general: + + - if you are configuring `aws.accounts`, no action is required + - if you are configuring `techdocs.publisher.awsS3.credentials`, no action is required + - if you are configuring multiple integrations under `integrations.awsS3`, no action is required + - if you are configuring a single integration under `integrations.awsS3`, make sure that the integration has access to the bucket you are using for TechDocs + +### Patch Changes + +- f0951aa: Updated the `defaultDockerImage` to reflect the latest TechDocs Container version of v1.2.8 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/app-defaults@1.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## @backstage/backend-app-api@1.4.1 + +### Patch Changes + +- 04db26b: Clean up process event listeners on backend stop to prevent leaks +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/backend-dynamic-feature-service@0.7.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/cli-common@0.1.17 + - @backstage/plugin-app-node@0.1.41 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/backend-openapi-utils@0.6.5 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + - @backstage/plugin-catalog-backend@3.3.1 + - @backstage/plugin-events-backend@0.5.10 + +## @backstage/backend-openapi-utils@0.6.5 + +### Patch Changes + +- 6678b78: Internal update to use native feature from our request validation library for handling base path determination. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/backend-plugin-api@1.6.1 + +### Patch Changes + +- ae4dd5d: Move some of the symlink resolution to `isChildPath` +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + +## @backstage/backend-test-utils@1.10.3 + +### Patch Changes + +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-app-api@1.4.1 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + +## @backstage/cli@0.35.2 + +### Patch Changes + +- 320c6a9: Bump `@swc/core` to support `ES2023` and `ES2024` +- c0d7bf6: Added `--include` and `--format` options to `backstage-cli info` command for including additional packages via glob patterns and outputting as JSON or Text. +- f6a5d2f: Fixed CSS module class name collisions when running multiple versions of packages simultaneously by using content-based hashing for class name generation. +- 140cbc2: Added `@backstage/backend-test-utils` to backend package templates. +- 4eeba9e: Upgrade `zod-validation-error` to version 4 +- 9ee5996: Bump minimum required `@swc/core` to avoid transpilation bug +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/integration@1.19.2 + +## @backstage/cli-common@0.1.17 + +### Patch Changes + +- ae4dd5d: Move some of the symlink resolution to `isChildPath` + +## @backstage/core-compat-api@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/core-components@0.18.5 + +### Patch Changes + +- a723b8a: The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown +- c671db9: Fixed bug in Table component where the toolbar layout would break when both a title and filters were present. +- 55a9dc2: Update colour token again in ItemCardHeader to respect theme definition. + +## @backstage/create-app@0.7.8 + +### Patch Changes + +- fea3e39: Bumped create-app version. +- 9f1ee3e: Bumped create-app version. +- 880310b: Bumped create-app version. +- f1fe6fe: Updated Dockerfile to use Node 24 and Debian Trixie +- Updated dependencies + - @backstage/cli-common@0.1.17 + +## @backstage/dev-utils@1.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/core-components@0.18.5 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/frontend-defaults@0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-app@0.3.4 + - @backstage/frontend-app-api@0.14.0 + +## @backstage/frontend-plugin-api@0.13.3 + +### Patch Changes + +- 3bd2a1a: Updated documentation for `createApiRef` to clarify the role of the ID in specifying the owning plugin of an API. + +- 9ccf84e: The following blueprints are being restricted to only be used in app plugin overrides and modules. They are being moved to the `@backstage/plugin-app-react` package and have been deprecated: + + - `AppRootWrapperBlueprint` + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- 4554a4e: Added an alpha `PluginWrapperBlueprint` exported from `@backstage/frontend-plugin-api/alpha`, which can install components that will wrap all plugin elements. + +- 872eb91: Upgrade `zod-to-json-schema` to latest version + +## @backstage/frontend-test-utils@0.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-app@0.3.4 + - @backstage/frontend-app-api@0.14.0 + +## @backstage/integration@1.19.2 + +### Patch Changes + +- 3afeab4: Implementing `ScmIntegration` for `GoogleGcs` +- 9083273: Rollback the lowercase replacing in GitHub integration config + +## @backstage/integration-react@1.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2 + +## @backstage/repo-tools@0.16.2 + +### Patch Changes + +- 498f9dd: Fixed help text for `backstage-repo-tools package schema openapi generate` command. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/cli-common@0.1.17 + +## @techdocs/cli@1.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/cli-common@0.1.17 + - @backstage/plugin-techdocs-node@1.14.0 + +## @backstage/plugin-api-docs@0.13.3 + +### Patch Changes + +- 0216090: Updated dependency `@types/swagger-ui-react` to `^5.0.0`. +- abeba2b: Fix types with new bumped dependency +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-catalog@1.32.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-app@0.3.4 + +### Patch Changes + +- 4554a4e: Implemented support for the new `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api/alpha`. + +- 9ccf84e: The following blueprints are being restricted to only be used in app plugin overrides and modules. They will now produce a deprecation warning when used outside of the app plugin: + + - `AppRootWrapperBlueprint` + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-app-react@0.1.0 + - @backstage/core-components@0.18.5 + - @backstage/integration-react@1.2.14 + +## @backstage/plugin-app-backend@0.5.10 + +### Patch Changes + +- 9ccf84e: Updated plugin metadata. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-app-node@0.1.41 + - @backstage/plugin-auth-node@0.6.11 + +## @backstage/plugin-app-node@0.1.41 + +### Patch Changes + +- 9ccf84e: Updated plugin metadata. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-app-visualizer@0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-auth@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-auth-backend@0.26.0 + - @backstage/plugin-auth-node@0.6.11 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.11 + +### Patch Changes + +- e54fcb2: Added support for custom start URL search parameters (with the new `startUrlSearchParams` config property) +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-auth-backend@0.26.0 + - @backstage/plugin-auth-node@0.6.11 + +## @backstage/plugin-auth-node@0.6.11 + +### Patch Changes + +- 4eeba9e: Upgrade `zod-validation-error` to version 4 +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-auth-react@0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## @backstage/plugin-bitbucket-cloud-common@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog@1.32.2 + +### Patch Changes + +- 7ca91e8: Header in EntityLayout should always be shown. + Monitoring the loading status caused flickering when the refresh() method of the Async Entity was invoked. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/core-compat-api@0.5.6 + - @backstage/plugin-search-react@1.10.2 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-scaffolder-common@1.7.5 + - @backstage/plugin-techdocs-react@1.3.7 + +## @backstage/plugin-catalog-backend@3.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + - @backstage/backend-openapi-utils@0.6.5 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + +## @backstage/plugin-catalog-backend-module-aws@0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-azure@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-openapi-utils@0.6.5 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + - @backstage/plugin-bitbucket-cloud-common@0.3.6 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-github@0.12.1 + +### Patch Changes + +- cb4b907: Improved efficiency of `GithubOrgEntityProvider` membership event handling and edit team. The provider now fetches only the specific user's teams instead of all organization users when processing membership events, and uses `addEntitiesOperation` instead of `replaceEntitiesOperation` to avoid unnecessary entity deletions. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-catalog-backend-module-github@0.12.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.7 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-catalog-backend@3.3.1 + +## @backstage/plugin-catalog-backend-module-logs@0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-catalog-backend@3.3.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.4 + +### Patch Changes + +- 115b378: Changed the logger level from 'warning' to 'debug' when we are unable to load the user's photo. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.18 + +### Patch Changes + +- a307700: Fixed crash when `latest_report_status` is undefined +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-common@1.7.5 + +## @backstage/plugin-catalog-graph@0.5.6 + +### Patch Changes + +- 5c49a00: Update for the `qs` library bump: the old array limit setting has changed to be more strict; you can no longer just give a zero to mean unlimited. So we choose an arbitrary high value, to at least go higher than the default 20. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-catalog-import@0.13.9 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/integration@1.19.2 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-catalog-react@1.21.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/core-compat-api@0.5.6 + - @backstage/frontend-test-utils@0.4.3 + - @backstage/integration-react@1.2.14 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.25 + +### Patch Changes + +- be6cef5: Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-devtools-react@0.1.0 + - @backstage/core-compat-api@0.5.6 + +## @backstage/plugin-config-schema@0.1.76 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## @backstage/plugin-devtools@0.1.35 + +### Patch Changes + +- be6cef5: Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-devtools-react@0.1.0 + - @backstage/plugin-devtools-common@0.1.21 + - @backstage/core-compat-api@0.5.6 + +## @backstage/plugin-devtools-backend@0.5.13 + +### Patch Changes + +- be6cef5: Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/cli-common@0.1.17 + - @backstage/plugin-devtools-common@0.1.21 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + +## @backstage/plugin-devtools-common@0.1.21 + +### Patch Changes + +- be6cef5: Add support for adding `unprocessed-entities` and other tabs to `devtools` when using the New Frontend system +- Updated dependencies + - @backstage/plugin-permission-common@0.9.4 + +## @backstage/plugin-events-backend@0.5.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-openapi-utils@0.6.5 + +## @backstage/plugin-events-backend-module-github@0.4.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## @backstage/plugin-home-react@0.1.34 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-kubernetes@0.12.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes-react@0.5.15 + +## @backstage/plugin-kubernetes-cluster@0.0.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes-react@0.5.15 + +## @backstage/plugin-kubernetes-react@0.5.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## @backstage/plugin-mcp-actions-backend@0.1.7 + +### Patch Changes + +- 4d82a35: build(deps): bump `@modelcontextprotocol/sdk` from 1.24.3 to 1.25.2 +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + +## @backstage/plugin-mui-to-bui@0.2.3 + +### Patch Changes + +- f157f43: Fix installation command +- e4a1180: Updated tokens from `--bui-bg` to `--bui-bg-surface-0` +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/frontend-plugin-api@0.13.3 + +## @backstage/plugin-notifications@0.5.13 + +### Patch Changes + +- 4452d15: Added i18n support. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-org@0.6.48 + +### Patch Changes + +- db3cfd7: Adds the Resource kind to the `useGetEntities` fallback so that Resources are included by default when no specific kinds are provided in the group ownership card. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-org-react@0.1.46 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-permission-common@0.9.4 + +### Patch Changes + +- 872eb91: Upgrade `zod-to-json-schema` to latest version + +## @backstage/plugin-permission-node@0.10.8 + +### Patch Changes + +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + +## @backstage/plugin-scaffolder@1.35.1 + +### Patch Changes + +- 9d75495: Fixed bug in RepoUrlPickerComponent component where repository names were not being autocompleted. +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/integration@1.19.2 + - @backstage/plugin-scaffolder-react@1.19.5 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-scaffolder-common@1.7.5 + - @backstage/plugin-techdocs-react@1.3.7 + +## @backstage/plugin-scaffolder-backend@3.1.1 + +### Patch Changes + +- 5012852: Remove unused abort controller in debug:wait action +- c641c14: Wrap some of the action logic with `resolveSafeChildPath` and improve symlink handling when fetching remote and local files +- 27f9061: REwrite] +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + - @backstage/backend-openapi-utils@0.6.5 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.17 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + - @backstage/plugin-bitbucket-cloud-common@0.3.6 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.18 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.17 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.17 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.1 + - @backstage/plugin-scaffolder-common@1.7.5 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.17 + +### Patch Changes + +- 88abcc6: Improved README with clearer setup and usage guidance. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + - @backstage/plugin-bitbucket-cloud-common@0.3.6 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.4 + +### Patch Changes + +- bb7088b: Added options to set [workflow access level][access-level] for repositories to `github:repo:create` + + This is useful when creating repositories for GitHub Actions to manage access + to the workflows during creation. + + ```diff + - action: github:repo:create + id: create-repo + input: + repoUrl: github.com?owner=owner&repo=repo + visibility: private + + workflowAccess: organization + ``` + + [access-level]: https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-the-level-of-access-for-workflows-outside-of-the-repository + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/integration@1.19.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/plugin-scaffolder-node-test-utils@0.3.7 + +## @backstage/plugin-scaffolder-common@1.7.5 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2 + - @backstage/plugin-permission-common@0.9.4 + +## @backstage/plugin-scaffolder-node@0.12.3 + +### Patch Changes + +- c641c14: Wrap some of the action logic with `resolveSafeChildPath` and improve symlink handling when fetching remote and local files +- 27f9061: REwrite] +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-scaffolder-common@1.7.5 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/backend-test-utils@1.10.3 + +## @backstage/plugin-scaffolder-react@1.19.5 + +### Patch Changes + +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-scaffolder-common@1.7.5 + +## @backstage/plugin-search@1.5.3 + +### Patch Changes + +- 5c49a00: Update for the `qs` library bump: the old array limit setting has changed to be more strict; you can no longer just give a zero to mean unlimited. So we choose an arbitrary high value, to at least go higher than the default 20. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-search-react@1.10.2 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-search-backend@2.0.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-openapi-utils@0.6.5 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + +## @backstage/plugin-search-backend-module-techdocs@0.4.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-techdocs-node@1.14.0 + - @backstage/plugin-permission-common@0.9.4 + +## @backstage/plugin-search-react@1.10.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-signals@0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-techdocs@1.16.2 + +### Patch Changes + +- 0afb8a6: Corrected color of some elements such as Grid cards and Tables. +- 94ff7ab: Code block "Copy to clipboard" button was not positioned correctly for docs built with `mkdocs-material>=9.7` +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/integration@1.19.2 + - @backstage/plugin-search-react@1.10.2 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-auth-react@0.1.23 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-techdocs-react@1.3.7 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.32.2 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/plugin-search-react@1.10.2 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-techdocs-react@1.3.7 + +## @backstage/plugin-techdocs-backend@2.1.4 + +### Patch Changes + +- b6ff2a5: Some AWS `publisher` config options such as `region`, `endpoint`, `accountId` are now marked as `@visibility backend` instead of `secret`. +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + - @backstage/plugin-techdocs-node@1.14.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/integration@1.19.2 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-techdocs-react@1.3.7 + +## @backstage/plugin-techdocs-react@1.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## @backstage/plugin-user-settings@0.8.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + +## @backstage/plugin-user-settings-backend@0.3.10 + +### Patch Changes + +- ad01e54: Resolves an issue where user setting keys containing slashes returned 404 not found. +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-auth-node@0.6.11 + +## example-app@0.2.117 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/plugin-catalog@1.32.2 + - @backstage/plugin-home@0.9.0 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-org@0.6.48 + - @backstage/plugin-mui-to-bui@0.2.3 + - @backstage/plugin-notifications@0.5.13 + - @backstage/frontend-app-api@0.14.0 + - @backstage/plugin-api-docs@0.13.3 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25 + - @backstage/plugin-devtools@0.1.35 + - @backstage/plugin-scaffolder@1.35.1 + - @backstage/plugin-catalog-graph@0.5.6 + - @backstage/plugin-search@1.5.3 + - @backstage/plugin-scaffolder-react@1.19.5 + - @backstage/plugin-search-react@1.10.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-auth-react@0.1.23 + - @backstage/plugin-catalog-import@0.13.9 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes@0.12.15 + - @backstage/plugin-kubernetes-cluster@0.0.33 + - @backstage/plugin-signals@0.0.27 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32 + - @backstage/plugin-techdocs-react@1.3.7 + - @backstage/plugin-user-settings@0.8.31 + +## example-app-next@0.0.31 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-catalog@1.32.2 + - @backstage/plugin-home@0.9.0 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-org@0.6.48 + - @backstage/plugin-app@0.3.4 + - @backstage/plugin-notifications@0.5.13 + - @backstage/frontend-app-api@0.14.0 + - @backstage/plugin-api-docs@0.13.3 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25 + - @backstage/plugin-devtools@0.1.35 + - @backstage/plugin-scaffolder@1.35.1 + - @backstage/plugin-catalog-graph@0.5.6 + - @backstage/plugin-search@1.5.3 + - @backstage/plugin-scaffolder-react@1.19.5 + - @backstage/core-compat-api@0.5.6 + - @backstage/frontend-defaults@0.3.5 + - @backstage/plugin-search-react@1.10.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-app-visualizer@0.1.27 + - @backstage/plugin-auth@0.1.4 + - @backstage/plugin-auth-react@0.1.23 + - @backstage/plugin-catalog-import@0.13.9 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes@0.12.15 + - @backstage/plugin-kubernetes-cluster@0.0.33 + - @backstage/plugin-signals@0.0.27 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32 + - @backstage/plugin-techdocs-react@1.3.7 + - @backstage/plugin-user-settings@0.8.31 + +## app-next-example-plugin@0.0.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## example-backend@0.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-app-backend@0.5.10 + - @backstage/plugin-techdocs-backend@2.1.4 + - @backstage/plugin-scaffolder-backend@3.1.1 + - @backstage/plugin-mcp-actions-backend@0.1.7 + - @backstage/plugin-auth-backend@0.26.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4 + - @backstage/plugin-devtools-backend@0.5.13 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + - @backstage/plugin-catalog-backend@3.3.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10 + - @backstage/plugin-catalog-backend-module-openapi@0.2.18 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16 + - @backstage/plugin-events-backend@0.5.10 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.18 + - @backstage/plugin-search-backend@2.0.10 + - @backstage/plugin-search-backend-module-techdocs@0.4.10 + +## e2e-test@0.2.36 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/create-app@0.7.8 + +## @internal/scaffolder@0.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-scaffolder-react@1.19.5 + +## techdocs-cli-embedded-app@0.2.116 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/plugin-catalog@1.32.2 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-techdocs-react@1.3.7 + +## @internal/plugin-todo-list@1.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 diff --git a/docs/releases/v1.47.0-next.0-changelog.md b/docs/releases/v1.47.0-next.0-changelog.md new file mode 100644 index 0000000000..e1fd3f9ebb --- /dev/null +++ b/docs/releases/v1.47.0-next.0-changelog.md @@ -0,0 +1,578 @@ +# Release v1.47.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.47.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.47.0-next.0) + +## @backstage/ui@0.11.0-next.0 + +### Minor Changes + +- 4ea1d15: **BREAKING**: Renamed CSS variable `--bui-bg` to `--bui-bg-surface-0` for consistency. + +### Patch Changes + +- 1880402: Fixes app background color on dark mode. +- 9c76682: build(deps-dev): bump `storybook` from 10.1.9 to 10.1.10 +- b4a4911: Fixed SearchField `startCollapsed` prop not working correctly in Backstage UI. The field now properly starts in a collapsed state, expands when clicked and focused, and collapses back when unfocused with no input. Also fixed CSS logic to work correctly in all layout contexts (flex row, flex column, and regular containers). + + Affected components: SearchField + +## @backstage/backend-defaults@0.14.1-next.0 + +### Patch Changes + +- 7126bf2: Fixed a spelling mistake in root health service shutdown response. +- Updated dependencies + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-dev-utils@0.1.6 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/backend-dynamic-feature-service@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.40 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-scaffolder-node@0.12.2 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/backend-openapi-utils@0.6.5-next.0 + +### Patch Changes + +- 6678b78: Internal update to use native feature from our request validation library for handling base path determination. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/backend-test-utils@1.10.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/cli@0.35.2-next.0 + +### Patch Changes + +- 320c6a9: Bump `@swc/core` to support `ES2023` and `ES2024` +- 9ee5996: Bump minimum required `@swc/core` to avoid transpilation bug +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/cli-common@0.1.16 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/integration@1.19.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/create-app@0.7.8-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## @backstage/dev-utils@1.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-catalog-react@1.21.4 + +## @techdocs/cli@1.10.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-common@0.1.16 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.13.10 + +## @backstage/plugin-app-visualizer@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.11-next.0 + +### Patch Changes + +- e54fcb2: Added support for custom start URL search parameters (with the new `startUrlSearchParams` config property) +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/plugin-auth-node@0.6.10 + +## @backstage/plugin-catalog-backend@3.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/plugin-catalog-backend-module-aws@0.4.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-kubernetes-common@0.9.9 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration@1.19.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-events-node@0.4.18 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.7-next.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-events-node@0.4.18 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + +## @backstage/plugin-catalog-backend-module-logs@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-events-node@0.4.18 + +## @backstage/plugin-devtools-backend@0.5.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.20 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/plugin-events-backend@0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.18 + +## @backstage/plugin-mcp-actions-backend@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1 + +## @backstage/plugin-mui-to-bui@0.2.3-next.0 + +### Patch Changes + +- e4a1180: Updated tokens from `--bui-bg` to `--bui-bg-surface-0` +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/theme@0.7.1 + +## @backstage/plugin-scaffolder-backend@3.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-bitbucket-cloud-common@0.3.5 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.16 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.17 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.16 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.16 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.0 + - @backstage/plugin-scaffolder-common@1.7.4 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.7-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/backend-test-utils@1.10.3-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.2 + +## @backstage/plugin-search-backend@2.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-search-common@1.2.21 + +## @backstage/plugin-techdocs-backend@2.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-techdocs-node@1.13.10 + +## @backstage/plugin-user-settings-backend@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-signals-node@0.1.27 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.117-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/plugin-mui-to-bui@0.2.3-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-app-api@0.13.3 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-devtools@0.1.34 + - @backstage/plugin-home@0.8.15 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-techdocs-react@1.3.6 + - @backstage/plugin-user-settings@0.8.30 + +## example-app-next@0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/plugin-app-visualizer@0.1.27-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-compat-api@0.5.5 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-app-api@0.13.3 + - @backstage/frontend-defaults@0.3.4 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-app@0.3.3 + - @backstage/plugin-auth@0.1.3 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-home@0.8.15 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-techdocs-react@1.3.6 + - @backstage/plugin-user-settings@0.8.30 + +## example-backend@0.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7 + - @backstage/plugin-devtools-backend@0.5.13-next.0 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-mcp-actions-backend@0.1.7-next.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-scaffolder-backend@3.1.1-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17 + - @backstage/plugin-search-backend@2.0.10-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.11 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9 + - @backstage/plugin-search-backend-module-explore@0.3.10 + - @backstage/plugin-search-backend-module-techdocs@0.4.9 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.4-next.0 + +## e2e-test@0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.8-next.0 + - @backstage/cli-common@0.1.16 + - @backstage/errors@1.2.7 + +## techdocs-cli-embedded-app@0.2.116-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/test-utils@1.7.14 + - @backstage/theme@0.7.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-react@1.3.6 diff --git a/docs/releases/v1.47.0-next.1-changelog.md b/docs/releases/v1.47.0-next.1-changelog.md new file mode 100644 index 0000000000..5c93c23480 --- /dev/null +++ b/docs/releases/v1.47.0-next.1-changelog.md @@ -0,0 +1,648 @@ +# Release v1.47.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.47.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.47.0-next.1) + +## @backstage/backend-defaults@0.14.1-next.1 + +### Patch Changes + +- 3afeab4: Implementing `readTree` for `GoogleGcsReader` +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/backend-dynamic-feature-service@0.7.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/cli@0.35.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/core-compat-api@0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/create-app@0.7.8-next.1 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + +## @backstage/frontend-app-api@0.13.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-defaults@0.3.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/frontend-defaults@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/frontend-test-utils@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/integration@1.19.2-next.0 + +### Patch Changes + +- 3afeab4: Implementing `ScmIntegration` for `GoogleGcs` +- 9083273: Rollback the lowercase replacing in GitHub integration config + +## @backstage/integration-react@1.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @techdocs/cli@1.10.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-techdocs-node@1.13.11-next.0 + +## @backstage/plugin-api-docs@0.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-app@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-bitbucket-cloud-common@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog@1.32.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/core-compat-api@0.5.6-next.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-search-react@1.10.1 + +## @backstage/plugin-catalog-backend@3.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.6-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + +## @backstage/plugin-catalog-graph@0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-catalog-import@0.13.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-catalog-react@1.21.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/frontend-test-utils@0.4.3-next.0 + - @backstage/core-compat-api@0.5.6-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-devtools@0.1.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.6-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-events-backend-module-github@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-home@0.8.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-kubernetes@0.12.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-kubernetes-cluster@0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + +## @backstage/plugin-org@0.6.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-org-react@0.1.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + +## @backstage/plugin-scaffolder@1.35.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-scaffolder-backend@3.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.6-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.17-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.18-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.17-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.17-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.1-next.0 + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.6-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.7-next.1 + +## @backstage/plugin-scaffolder-common@1.7.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-scaffolder-node@0.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## @backstage/plugin-scaffolder-react@1.19.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.7.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-search@1.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-search-react@1.10.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.13.11-next.0 + +## @backstage/plugin-techdocs@1.16.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-search-react@1.10.1 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-search-react@1.10.1 + +## @backstage/plugin-techdocs-backend@2.1.4-next.1 + +### Patch Changes + +- b6ff2a5: Some AWS `publisher` config options such as `region`, `endpoint`, `accountId` are now marked as `@visibility backend` instead of `secret`. +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/integration@1.19.2-next.0 + - @backstage/plugin-techdocs-node@1.13.11-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## @backstage/plugin-techdocs-node@1.13.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## @backstage/plugin-user-settings@0.8.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## example-app@0.2.117-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-import@0.13.9-next.0 + - @backstage/plugin-scaffolder@1.35.1-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/plugin-api-docs@0.13.3-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.0 + - @backstage/plugin-org@0.6.48-next.0 + - @backstage/plugin-user-settings@0.8.31-next.0 + - @backstage/plugin-home@0.8.16-next.0 + - @backstage/plugin-kubernetes@0.12.15-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.0 + - @backstage/plugin-search@1.5.2-next.0 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-devtools@0.1.35-next.0 + +## example-app-next@0.0.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-import@0.13.9-next.0 + - @backstage/plugin-scaffolder@1.35.1-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.0 + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-defaults@0.3.5-next.0 + - @backstage/core-compat-api@0.5.6-next.0 + - @backstage/plugin-api-docs@0.13.3-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.0 + - @backstage/plugin-org@0.6.48-next.0 + - @backstage/plugin-user-settings@0.8.31-next.0 + - @backstage/plugin-home@0.8.16-next.0 + - @backstage/plugin-kubernetes@0.12.15-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.0 + - @backstage/plugin-search@1.5.2-next.0 + - @backstage/plugin-app-visualizer@0.1.27-next.0 + - @backstage/plugin-auth@0.1.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-search-react@1.10.1 + +## example-backend@0.0.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.4-next.1 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.18-next.0 + - @backstage/plugin-scaffolder-backend@3.1.1-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.18-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.10-next.0 + +## @internal/scaffolder@0.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## techdocs-cli-embedded-app@0.2.116-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 diff --git a/docs/releases/v1.47.0-next.2-changelog.md b/docs/releases/v1.47.0-next.2-changelog.md new file mode 100644 index 0000000000..443a981de0 --- /dev/null +++ b/docs/releases/v1.47.0-next.2-changelog.md @@ -0,0 +1,385 @@ +# Release v1.47.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.47.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.47.0-next.2) + +## @backstage/app-defaults@1.7.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/core-components@0.18.5-next.0 + +### Patch Changes + +- a723b8a: The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown + +## @backstage/create-app@0.7.8-next.2 + +### Patch Changes + +- f1fe6fe: Updated Dockerfile to use Node 24 and Debian Trixie + +## @backstage/dev-utils@1.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/frontend-defaults@0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-app@0.3.4-next.1 + +## @backstage/plugin-api-docs@0.13.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-app@0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + +## @backstage/plugin-app-visualizer@0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-auth@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-auth-react@0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-catalog@1.32.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @backstage/plugin-catalog-graph@0.5.5-next.1 + +### Patch Changes + +- 5c49a00: Update for the `qs` library bump: the old array limit setting has changed to be more strict; you can no longer just give a zero to mean unlimited. So we choose an arbitrary high value, to at least go higher than the default 20. +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-catalog-import@0.13.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-catalog-react@1.21.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-config-schema@0.1.76-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-devtools@0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-home@0.8.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-home-react@0.1.34-next.0 + +## @backstage/plugin-home-react@0.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-kubernetes@0.12.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-kubernetes-react@0.5.15-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-kubernetes-react@0.5.15-next.0 + +## @backstage/plugin-kubernetes-react@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-notifications@0.5.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-org@0.6.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-org-react@0.1.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-scaffolder@1.35.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-scaffolder-react@1.19.5-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @backstage/plugin-scaffolder-react@1.19.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## @backstage/plugin-search@1.5.2-next.1 + +### Patch Changes + +- 5c49a00: Update for the `qs` library bump: the old array limit setting has changed to be more strict; you can no longer just give a zero to mean unlimited. So we choose an arbitrary high value, to at least go higher than the default 20. +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + +## @backstage/plugin-search-react@1.10.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-signals@0.0.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-techdocs@1.16.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-auth-react@0.1.23-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @backstage/plugin-techdocs-react@1.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## @backstage/plugin-user-settings@0.8.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## example-app@0.2.117-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.1 + - @backstage/plugin-search@1.5.2-next.1 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-api-docs@0.13.3-next.1 + - @backstage/plugin-auth-react@0.1.23-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-import@0.13.9-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25-next.0 + - @backstage/plugin-devtools@0.1.35-next.1 + - @backstage/plugin-home@0.8.16-next.1 + - @backstage/plugin-kubernetes@0.12.15-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.1 + - @backstage/plugin-notifications@0.5.13-next.0 + - @backstage/plugin-org@0.6.48-next.1 + - @backstage/plugin-scaffolder@1.35.1-next.1 + - @backstage/plugin-scaffolder-react@1.19.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-signals@0.0.27-next.0 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + - @backstage/plugin-user-settings@0.8.31-next.1 + +## example-app-next@0.0.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.1 + - @backstage/plugin-search@1.5.2-next.1 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/frontend-defaults@0.3.5-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-api-docs@0.13.3-next.1 + - @backstage/plugin-app@0.3.4-next.1 + - @backstage/plugin-app-visualizer@0.1.27-next.1 + - @backstage/plugin-auth@0.1.4-next.0 + - @backstage/plugin-auth-react@0.1.23-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-import@0.13.9-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25-next.0 + - @backstage/plugin-home@0.8.16-next.1 + - @backstage/plugin-kubernetes@0.12.15-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.1 + - @backstage/plugin-notifications@0.5.13-next.0 + - @backstage/plugin-org@0.6.48-next.1 + - @backstage/plugin-scaffolder@1.35.1-next.1 + - @backstage/plugin-scaffolder-react@1.19.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-signals@0.0.27-next.0 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + - @backstage/plugin-user-settings@0.8.31-next.1 + +## app-next-example-plugin@0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## techdocs-cli-embedded-app@0.2.116-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## @internal/plugin-todo-list@1.0.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 diff --git a/docs/releases/v1.47.0-next.3-changelog.md b/docs/releases/v1.47.0-next.3-changelog.md new file mode 100644 index 0000000000..053fef2253 --- /dev/null +++ b/docs/releases/v1.47.0-next.3-changelog.md @@ -0,0 +1,365 @@ +# Release v1.47.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.47.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.47.0-next.3) + +## @backstage/backend-defaults@0.15.0-next.2 + +### Minor Changes + +- 6fc00e6: Added action filtering support with glob patterns and attribute constraints. + + The `ActionsService` now supports filtering actions based on configuration. This allows controlling which actions are exposed to consumers like the MCP backend. + + Configuration example: + + ```yaml + backend: + actions: + pluginSources: + - catalog + - scaffolder + filter: + include: + - id: 'catalog:*' + attributes: + destructive: false + - id: 'scaffolder:*' + exclude: + - id: '*:delete-*' + - attributes: + readOnly: false + ``` + + Filtering logic: + + - `include`: Rules for actions to include. Each rule can specify an `id` glob pattern and/or `attributes` constraints. An action must match at least one rule to be included. If no include rules are specified, all actions are included by default. + - `exclude`: Rules for actions to exclude. Takes precedence over include rules. + - Each rule combines `id` and `attributes` with AND logic (both must match if specified). + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/ui@0.11.0-next.1 + +### Minor Changes + +- 243e5e7: **BREAKING**: Redesigned Table component with new `useTable` hook API. + + - The `Table` component (React Aria wrapper) is renamed to `TableRoot` + - New high-level `Table` component that handles data display, pagination, sorting, and selection + - The `useTable` hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor) + - New types: `ColumnConfig`, `TableProps`, `TableItem`, `UseTableOptions`, `UseTableResult` + + New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. + + **Migration guide:** + + 1. Update imports and use the new `useTable` hook: + + ```diff + -import { Table, useTable } from '@backstage/ui'; + -const { data, paginationProps } = useTable({ data: items, pagination: {...} }); + +import { Table, useTable, type ColumnConfig } from '@backstage/ui'; + +const { tableProps } = useTable({ + + mode: 'complete', + + getData: () => items, + +}); + ``` + + 2. Define columns and render with the new Table API: + + ```diff + -
    + - ... + - ... + -
    + - + +const columns: ColumnConfig[] = [ + + { id: 'name', label: 'Name', isRowHeader: true, cell: item => }, + + { id: 'type', label: 'Type', cell: item => }, + +]; + + + + + ``` + + Affected components: Table, TableRoot, TablePagination + +- 95246eb: **Breaking** Updating color tokens to match the new neutral style on different surfaces. + + ## Migration notes + + There's no direct replacement for the old tint tokens but you can use the new neutral set of color tokens on surface 0 or 1 as a replacement. + + - `--bui-bg-tint` can be replaced by `--bui-bg-neutral-on-surface-0` + - `--bui-bg-tint-hover` can be replaced by `--bui-bg-neutral-on-surface-0-hover` + - `--bui-bg-tint-pressed` can be replaced by `--bui-bg-neutral-on-surface-0-pressed` + - `--bui-bg-tint-disabled` can be replaced by `--bui-bg-neutral-on-surface-0-disabled` + +- ea0c6d8: Introduce new `ToggleButton` & `ToggleButtonGroup` components in Backstage UI + +### Patch Changes + +- 21c87cc: Fixes disabled state in primary and secondary buttons in Backstage UI. +- b3253b6: Fixed `Link` component causing hard page refreshes for internal routes. The component now properly uses React Router's navigation instead of full page reloads. + +## @backstage/plugin-auth-backend@0.26.0-next.0 + +### Minor Changes + +- 7ffc873: Fix `user_created_at` migration causing `SQLiteError` regarding use of non-constants for defaults + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + +## @backstage/plugin-events-backend-module-kafka@0.3.0-next.0 + +### Minor Changes + +- ef5bbd8: Add support for Kafka offset configuration (`fromBeginning`) and `autoCommit` + +## @backstage/plugin-home@0.9.0-next.2 + +### Minor Changes + +- e091a83: Widget configurations are now only saved to storage when the Save button is explicitly clicked. Added a Cancel button that allows users to discard unsaved changes and revert to the last saved state. + +## @backstage/backend-dynamic-feature-service@0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-search-backend-node@1.4.0 + +## @backstage/backend-test-utils@1.10.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/backend-app-api@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + +## @backstage/create-app@0.7.8-next.3 + +### Patch Changes + +- Bumped create-app version. + +## @techdocs/cli@1.10.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-api-docs@0.13.3-next.2 + +### Patch Changes + +- 0216090: Updated dependency `@types/swagger-ui-react` to `^5.0.0`. +- abeba2b: Fix types with new bumped dependency +- Updated dependencies + - @backstage/plugin-catalog@1.32.2-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.26.0-next.0 + - @backstage/plugin-auth-node@0.6.10 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.26.0-next.0 + - @backstage/plugin-auth-node@0.6.10 + +## @backstage/plugin-catalog@1.32.2-next.2 + +### Patch Changes + +- 7ca91e8: Header in EntityLayout should always be shown. + Monitoring the loading status caused flickering when the refresh() method of the Async Entity was invoked. + +## @backstage/plugin-catalog-backend-module-aws@0.4.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-catalog-backend-module-github@0.12.1-next.1 + +### Patch Changes + +- cb4b907: Improved efficiency of `GithubOrgEntityProvider` membership event handling and edit team. The provider now fetches only the specific user's teams instead of all organization users when processing membership events, and uses `addEntitiesOperation` instead of `replaceEntitiesOperation` to avoid unnecessary entity deletions. + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.18-next.0 + +### Patch Changes + +- a307700: Fixed crash when `latest_report_status` is undefined + +## @backstage/plugin-devtools-backend@0.5.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/plugin-mcp-actions-backend@0.1.7-next.1 + +### Patch Changes + +- 4d82a35: build(deps): bump `@modelcontextprotocol/sdk` from 1.24.3 to 1.25.2 +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-scaffolder@1.35.1-next.2 + +### Patch Changes + +- 9d75495: Fixed bug in RepoUrlPickerComponent component where repository names were not being autocompleted. + +## @backstage/plugin-scaffolder-backend@3.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.1 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.1 + +### Patch Changes + +- bb7088b: Added options to set [workflow access level][access-level] for repositories to `github:repo:create` + + This is useful when creating repositories for GitHub Actions to manage access + to the workflows during creation. + + ```diff + - action: github:repo:create + id: create-repo + input: + repoUrl: github.com?owner=owner&repo=repo + visibility: private + + workflowAccess: organization + ``` + + [access-level]: https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-the-level-of-access-for-workflows-outside-of-the-repository + +## @backstage/plugin-search-backend@2.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-search-backend-node@1.4.0 + +## @backstage/plugin-techdocs-backend@2.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## @backstage/plugin-user-settings-backend@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-auth-node@0.6.10 + +## example-app@0.2.117-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.1 + - @backstage/plugin-catalog@1.32.2-next.2 + - @backstage/plugin-home@0.9.0-next.2 + - @backstage/plugin-api-docs@0.13.3-next.2 + - @backstage/plugin-scaffolder@1.35.1-next.2 + - @backstage/cli@0.35.2-next.1 + +## example-app-next@0.0.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.1 + - @backstage/plugin-catalog@1.32.2-next.2 + - @backstage/plugin-home@0.9.0-next.2 + - @backstage/plugin-api-docs@0.13.3-next.2 + - @backstage/plugin-scaffolder@1.35.1-next.2 + - @backstage/cli@0.35.2-next.1 + +## example-backend@0.0.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-mcp-actions-backend@0.1.7-next.1 + - @backstage/plugin-auth-backend@0.26.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.1 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-devtools-backend@0.5.13-next.1 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-scaffolder-backend@3.1.1-next.2 + - @backstage/plugin-search-backend@2.0.10-next.1 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.4-next.2 diff --git a/docs/releases/v1.47.0.md b/docs/releases/v1.47.0.md new file mode 100644 index 0000000000..6831b96b46 --- /dev/null +++ b/docs/releases/v1.47.0.md @@ -0,0 +1,128 @@ +--- +id: v1.47.0 +title: v1.47.0 +description: Backstage Release v1.47.0 +--- + +These are the release notes for the v1.47.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 + +### BREAKING: Redesigned `Table` and `useTable` in `@backstage/ui` + +The `Table` component and accompanying `useTable` hook have been redesigned. The low-level React Aria wrapper `Table` is now `TableRoot`, while a higher level `Table` component now handles pagination, sorting, selection, and data display. The `useTable` also has a new API, that supports 3 pagination modes: `complete`, `offset`, and `cursor`. + +More information along with a migration guide can be found in the [BUI Changelog](https://ui.backstage.io/changelog). + +### BREAKING: Updated color tokens in `@backstage/ui` + +UI color tokens have been updated to align with the new neutral surface-based design system, and the `-tint` tokens have been removed. + +More information can be found in the [BUI Changelog](https://ui.backstage.io/changelog). + +### BREAKING: Redirect validation in the URL reader + +`coreServices.urlReader` now validates that redirect chains are subject to the allow list in `backend.reading.allow` of your app config. If you were relying on redirects that pointed to URLs that were not allowlisted, you will now have to add those to your config as well. + +To support this, the `FetchUrlReader` class no longer has a public constructor, but instead has a `fromConfig` static factory method. + +### BREAKING: Better AWS S3 auth handling in techdocs + +Techdocs now knows how to properly use `integrations.awsS3` config to authenticate with AWS S3. Since this affects the priority order of which auth setting is being used, it’s technically a breaking change. + +The new priority is: + +1. `aws.accounts` +2. `techdocs.publisher.awsS3.credentials` +3. `integrations.awsS3` +4. Default credential chain + +### Deprecation and upcoming restrictions of blueprints for app customization in `@backstage/frontend-plugin-api` + +There were several blueprints in `@backstage/frontend-plugin-api` that were intended only for use in customization of the central app. For example `SignInPageBlueprint` that replaces the sign-in page, or `NavContentBlueprint` that replaces the sidebar content. The following blueprints have all been deprecated and moved to a new `@backstage/plugin-app-react` package: + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` + +These blueprints are also being restricted from use outside of the `app` plugin. This means that you have to either use an `app` plugin override or a module with `pluginId` set to `app`. These currently trigger deprecation warnings, but will instead be rejected in a future release. + +### Deprecations and upcoming restrictions for API factories in new frontend system + +Similar to the above change, restrictions are being introduced that limit the ability for plugins to override both core Utility APIs and API factories from other plugins. Plugins will be restricted from installing factory overrides for Utility APIs that belong to other plugins. This restriction also applies to the core APIs provided by the app plugin. These overrides now instead need to be made using a plugin override or module. + +Just like the blueprint changes, this release will only trigger deprecation warnings, with the change taking effect in a future release. + +### Backend action filtering support + +`ActionsService` in `@backstage/backend-defaults` now supports action filtering based on configuration to enable control over which actions are exposed to consumers like the MCP backend. + +At the time of writing, this is primarily documented in [the relevant configuration schema](https://github.com/backstage/backstage/blob/master/packages/backend-defaults/config.d.ts). + +### `scaffolder` now supports configuration for `workflowAccess` level for creating Github repositories + +Workflow files can now configure the level of access to Github Actions for workflows outside of a repository. + +This is useful when creating repositories for GitHub Actions to manage access to the workflows for `github:repo:create`. + +```diff + - action: github:repo:create + id: create-repo + input: + repoUrl: github.com?owner=owner&repo=repo + visibility: private ++ workflowAccess: organization +``` + +Github Actions access-level documentation can be found [here](https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-the-level-of-access-for-workflows-outside-of-the-repository). + +Contributed by [@fearphage](https://github.com/fearphage) in [#32237](https://github.com/backstage/backstage/pull/32237) + +### Additional Kafka Events streaming settings + +The Kafka event module now supports `fromBeginning` offsets and `autoCommit`. + +Contributed by [@imod](https://github.com/imod) in [#31410](https://github.com/backstage/backstage/pull/31410) + +### Improvements to the `home` plugin UI + +Widget configuration changes are now only saved when the Save button is explicitly clicked. A new Cancel button has been added to enable users to discard unsaved changes + +Contributed by [@kmikko](https://github.com/kmikko) in [#31198](https://github.com/pull/31198) + +### Improved performance for `GithubOrgEntityProvider` + +`GithubOrgEntityProvider` membership event handling and edit team has been improved. The provider now fetches only the specific user's teams instead of all organization users when processing membership events, and uses `addEntitiesOperation` instead of `replaceEntitiesOperation` to avoid unnecessary entity deletions. + +Contributed by [@angeliski](https://github.com/angeliski) in [#32184](https://github.com/pull/32184) + +### `Link` component now uses React Router’s navigation system + +Previously the `Link` component would cause hard page refreshes for internal routes. With this update, the component now properly uses React Router’s navigation instead of full page reloads. + +## Security Fixes + +This release contains security fixes for Software Templates and reading external content. + +## 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.47.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://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index c8d9034955..c896aa1399 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -417,7 +417,7 @@ The following is an example of a `Dockerfile` that can be used to package the output of building a package with role `'backend'` into an image: ```Dockerfile -FROM node:20-bookworm-slim +FROM node:24-trixie-slim WORKDIR /app COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ @@ -610,7 +610,7 @@ With that in mind, here are some IDEs configurations to run backstage components 1. Click on "Edit Configurations" on top panel 2. In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner. 3. "Configuration file": leave empty (`backstage-cli` adds the config) - 4. "Node options": `--no-node-snapshot --experimental-vm-modules` + 4. "Node options": ` --experimental-vm-modules` 5. "Jest package": `~/workspace/backstage/node_modules/@backstage/cli` - the location of the backstage cli package. 6. "Working directory": `~/workspace/backstage` 7. "Jest Options": `repo test --runInBand --watch=false` @@ -621,20 +621,21 @@ With that in mind, here are some IDEs configurations to run backstage components #### VS Code +1. Install the [Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) for VS Code. +2. Update `settings.json` in the `.vscode` folder with: + ```jsonc { "jest.jestCommandLine": "yarn test", // In a large repo like the Backstage main repo you likely want to disable // watch mode and the initial test run too, leaving just manual and perhaps // on-save test runs in place. - "jest.autoRun": { - "watch": false, - "onSave": "test-src-file" - } + "jest.runMode": "on-save" } ``` -A complete launch configuration for VS Code debugging may look like this: +3. Add a launch configuration for VS Code in `launch.json` in the `.vscode` folder. + A complete configuration for debugging may look like this: ```jsonc { @@ -653,11 +654,13 @@ A complete launch configuration for VS Code debugging may look like this: ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "program": "${workspaceFolder}/node_modules/.bin/backstage-cli" + "program": "${workspaceFolder}/node_modules/@backstage/cli/bin/backstage-cli" } ``` +4. The configuration is not for manual runs from the "Run and Debug" view. + Instead use the Jest test explorer or the [test's gutter menu](https://github.com/jest-community/vscode-jest#how-to-trigger-a-test-run). + ## Publishing Package publishing is an optional part of the Backstage build system and not diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index ce8f862c98..07f6dee879 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -432,8 +432,79 @@ Usage: backstage-cli create-github-app Outputs debug information which is useful when opening an issue. Outputs system information, node.js and npm versions, CLI version and type (inside backstage -repo or a created app), all `@backstage/*` package dependency versions. +repo or a created app), all `@backstage/*` package dependency versions, and any +packages that contain a `backstage` field in their `package.json`. + +The command distinguishes between installed packages (from npm) and local +workspace packages, making it easier to understand your Backstage setup. ```text -Usage: backstage-cli info +Usage: backstage-cli info [options] + +Options: + --include Glob patterns for additional packages to include + (e.g., @mycompany/backstage-*) + --format Output format (default: text) + -h, --help display help for command +``` + +### Examples + +Output debug information to the console: + +```bash +yarn backstage-cli info +``` + +Include additional packages matching a glob pattern: + +```bash +yarn backstage-cli info --include "@mycompany/*" +``` + +Output as JSON: + +```bash +yarn backstage-cli info --format json +``` + +Export JSON to a file for further processing: + +```bash +yarn backstage-cli info --format json > backstage-info.json +``` + +Combine options to include custom packages and export to JSON: + +```bash +yarn backstage-cli info --include "@mycompany/backstage-*" --include "@internal/*" --format json > debug-info.json +``` + +Export text output to a file: + +```bash +yarn backstage-cli info --format text > backstage-info.txt +``` + +### JSON Output Format + +When using `--format json`, the output is structured as follows: + +```json +{ + "system": { + "os": "Darwin 23.0.0 - darwin/arm64", + "node": "v18.17.0", + "yarn": "3.6.0", + "cli": { "version": "0.27.0", "local": false }, + "backstage": "1.20.0" + }, + "dependencies": { + "@backstage/core-plugin-api": "1.8.0", + "@backstage/plugin-catalog": "1.15.0" + }, + "local": { + "@mycompany/backstage-plugin-custom": "0.1.0" + } +} ``` diff --git a/docs/tutorials/jest30-migration.md b/docs/tutorials/jest30-migration.md new file mode 100644 index 0000000000..ea79b03009 --- /dev/null +++ b/docs/tutorials/jest30-migration.md @@ -0,0 +1,95 @@ +--- +id: jest30-migration +title: Migrating to Jest 30 +description: A guide to migrating your project to Jest 30 and JSDOM 27 +--- + +Starting with a recent version of `@backstage/cli`, `jest` is a peer dependency. If you run tests using Backstage CLI, you must add Jest and its environment dependencies as `devDependencies` in your project. + +You can choose to install either Jest 29 or Jest 30. The built-in Jest version before this change was Jest 29, however, we recommend that you switch to Jest 30. Upgrading will solve the `Could not parse CSS stylesheet` errors, allow you to use MSW v2 in web packages, and ensure that you remain compatible with future versions of the Backstage CLI. Support for Jest 29 is temporary, with the purpose of allowing you to upgrade at your own pace, but it will eventually be removed. + +- **Jest 29**: + + ```bash + # in your repository root, run: + yarn add --dev jest@^29 @types/jest@^29 jest-environment-jsdom@^29 + ``` + + Pros: No migration needed + + Cons: You may see `Could not parse CSS stylesheet` warnings/errors when testing components from `@backstage/ui` or other packages using CSS `@layer` declarations + +- **Jest 30**: + + ```bash + # in your repository root, run: + yarn add --dev jest@^30 @types/jest@^30 @jest/environment-jsdom-abstract@^30 jsdom@^27 + ``` + + Pros: Fixes the stylesheet parsing warnings/errors + + Cons: Requires migration steps (see below) in your tests + +## Migration Guide + +The examples below are issues we encountered while migrating the Backstage repository. For a complete list of breaking changes, see the official documentation: + +- [Jest 30 upgrade guide](https://jestjs.io/docs/upgrading-to-jest30) +- [JSDOM changelog](https://github.com/jsdom/jsdom/releases) + +### Jest 30 + +**Asymmetric matchers with arrays**: `expect.objectContaining()` no longer works with arrays. + +```diff +- expect(result).toEqual(expect.objectContaining([{ id: '123' }])); ++ expect(result).toEqual([{ id: '123' }]); +// or ++ expect(result).toEqual(expect.arrayContaining([{ id: '123' }])); +``` + +**Array length assertions**: `expect.objectContaining({ length: N })` no longer works. + +```diff +- expect(fn).toHaveBeenCalledWith(expect.objectContaining({ length: 2 })); ++ expect(fn).toHaveBeenCalledWith(expect.any(Array)); ++ expect(fn.mock.calls[0][0]).toHaveLength(2); +``` + +**Deprecated matcher aliases removed**: Replace with canonical names. + +```diff +- expect(fn).toBeCalled(); ++ expect(fn).toHaveBeenCalled(); +``` + +**Snapshots**: Regenerate snapshots as the header format has changed. + +```bash +yarn test --no-watch -u +``` + +### JSDOM 27 + +**window.location is non-configurable**: You can no longer mock location via `Object.defineProperty`. + +```diff +- Object.defineProperty(window, 'location', { value: { href: '' } }); ++ // Option 1: Use history API ++ history.replaceState({}, '', '/new-path'); ++ // Option 2: Spy on navigation methods ++ const spy = jest.spyOn(component, 'navigate'); +``` + +**CSS color values**: Colors may be returned as RGB instead of named colors. + +```diff +- expect(element.style.color).toBe('red'); ++ expect(element.style.color).toBe('rgb(255, 0, 0)'); +``` + +**Error format changes**: Error messages and stack traces may have different formatting. + +#### If you run into `Cannot read properties of null (reading 'constructor')` + +Certain Backstage UI-components (e.g. Button) have a combination of CSS that triggers this error in tests. The solution is to make sure you have at least v0.9.25 of `@acemir/cssom`. diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md index 58e973d891..bb416bf731 100644 --- a/docs/tutorials/using-backstage-proxy-within-plugin.md +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -94,15 +94,15 @@ export const AwesomeUsersTable = () => { This section describes the steps to wrap your API client in a [Utility API](../api/utility-apis.md), which are: -- use [`createApiRef`](../reference/core-plugin-api.createapiref.md) to create a - new [`ApiRef`](../reference/core-plugin-api.apiref.md) -- register an [`ApiFactory`](../reference/core-plugin-api.apifactory.md) with +- use [`createApiRef`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiRef.html) to create a + new [`ApiRef`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiRef.html) +- register an [`ApiFactory`](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.ApiFactory.html) with your plugin using - [`createApiFactory`](../reference/core-plugin-api.createapifactory.md). This + [`createApiFactory`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.createApiFactory.html). This will wrap your API implementation, associate your `ApiRef` with your implementation and tell backstage how to instantiate it - finally, you can use your API in your components by calling - [`useApi`](../reference/core-plugin-api.useapi.md) + [`useApi`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.useApi.html) ### Defining the API client interface @@ -187,8 +187,8 @@ export class MyAwesomeApiClient implements MyAwesomeApi { ``` > Check out the docs for more information on the -> [DiscoveryApi](../reference/core-plugin-api.discoveryapi.md) or the -> [FetchApi](../reference/core-plugin-api.fetchapi.md) +> [DiscoveryApi](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.DiscoveryApi.html) or the +> [FetchApi](https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.FetchApi.html) ### Bundling your ApiRef with your plugin @@ -233,7 +233,7 @@ export const myCustomPlugin = createPlugin({ ### Using the API in your components Now you should be able to access your API using the backstage hook -[`useApi`](../reference/core-plugin-api.useapi.md) from within your plugin code. +[`useApi`](https://backstage.io/api/stable/functions/_backstage_frontend-plugin-api.useApi.html) from within your plugin code. ```ts title="plugins/my-awesome-plugin/src/components/AwesomeUsersTable.tsx" import { useApi } from '@backstage/core-plugin-api'; diff --git a/microsite/blog/2025-11-25-backstagecon-kubecon-25-atlanta.mdx b/microsite/blog/2025-11-25-backstagecon-kubecon-25-atlanta.mdx new file mode 100644 index 0000000000..62f8c36dae --- /dev/null +++ b/microsite/blog/2025-11-25-backstagecon-kubecon-25-atlanta.mdx @@ -0,0 +1,93 @@ +--- +title: 'Backstage in Atlanta: Highlights from BackstageCon and KubeCon + CloudNativeCon North America 2025' +author: Kate Klymkovska, Spotify & André Wanlin, Spotify +--- + +![Backstage in Atlanta: Highlights from BackstageCon and KubeCon + CloudNativeCon North America 2025](assets/2025-11-25/backstagecon-kubecon-na-2025-header.png) + +Cancelled flights and wonky Wi-Fi couldn't hold us back from having an amazing time in the Big Peach! It was great to head south and commune with the community in person at [BackstageCon](https://colocatedeventsna2025.sched.com/overview/type/BackstageCon) and [KubeCon + CloudNativeCon North America 2025](https://kccncna2025.sched.com) in Atlanta. We gave talks, shared insights, chatted without having to type into little text boxes, and tackled issues elbow-to-elbow during [ContribFest](https://backstage.io/blog/2025/10/15/backstage-contribfest-kubecon-guide). The week was capped off with the [Backstage maintainers talk](https://www.youtube.com/watch?v=evmmr-uxNsc&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) where we celebrated a year of milestones, the release of the adoption-ready New Frontend System, and work towards an AI-Native Backstage. Catch up with recordings of [all the Backstage talks](https://www.youtube.com/playlist?list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) and check out highlights from our week in the Big A below. + +{/* truncate */} + +## Scenes from BackstageCon (and beyond) + +![Scenes from BackstageCon and KubeCon + CloudNativeCon NA 2025](assets/2025-11-25/kcon25-atl-slideshow.gif) +_📸 [CNCF](https://www.flickr.com/photos/143247548@N03/albums/72177720330018728/)_ + +Brrr…it was a cold start with “Hot 'Lanta” bringing a serious autumn chill on Monday morning. But all the spicy DevEx insights would soon heat things up in the halls of the Georgia World Conference Center. Throughout the day, we heard discussions around how organizations adopt Backstage and scale it for productivity, as well as practical lessons on governance, deployment models, change management, and standardizing best practices around Backstage. + +And despite an unfortunate amount of flight disruptions waylaying some of the speakers, the organizers were able to work around all the last-minute issues for a day full of presentations, demos, lightning talks, and keynotes. Check out our community YouTube channel to watch recordings of [all the Backstage talks](https://www.youtube.com/playlist?list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) from Atlanta. Not sure where to start? + +### A few personal picks + +Here are six BackstageCon talks that we attended and thought were worth sharing. They feature Backstage adopters across a range of industries — from finance and healthcare, to networking, automotive experiences, and beyond — all sharing use cases that relate to almost any engineering org: + +- 🏦 [Supercharging Backstage Scaffolder for Workflows](https://www.youtube.com/watch?v=0m94ERF0k4Q&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): Jonathan Chan and Francis Hackenberger, engineers from the Royal Bank of Canada's cloud team, talk about how they scale up the power of Backstage Software Templates at one of Canada's largest banks. + +- 🩺 [From Friction to Flow: How Backstage Transformed Our Security Experience](https://www.youtube.com/watch?v=ZCM9i1zD364&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): In this lightning talk, Christopher Diaz, principal engineer at RVO Health, squeezes three years' worth of Backstage learnings into a 10-minute lightning talk — including how Backstage has unlocked an inner source culture within their org, with teams building custom plugins to extend their instance (“That's what makes it so much fun to build on Backstage: everything you need to solve a problem is literally just a pull request away” 🙌). + +- 📶 [How Ericsson Scaled Developer Experience with Backstage](https://www.youtube.com/watch?v=1mDEkiN4NTs&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): Ericsson principal engineer Damien O'Toole and engineering manager Kieran Egan share what they learned on the way to scaling internal Backstage adoption to over 1,260 users and over 4,073 weekly visits. + +- 🚗 [Cruising the AI Highway: Standardized MCP Deployment and Governance with Backstage](https://www.youtube.com/watch?v=pFSSpDHB2rU&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): Engineers from Toyota Connected — “recovering system administrator” Rob Heckel and platform engineer Tyler Rothenberg — share how they deploy MCP servers across their org in an easy, secure, and sustainable way, covering everything from the basics of MCP to deploying MCP servers using cloud native tools and integrating them with Backstage. + +- 📊 [Lego Bricks for Developers: Turning Insights into Golden Paths at Neo4j](https://www.youtube.com/watch?v=IdLXeK8motg&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): Developer advocate Jeremy Adams (stepping in for a waylayed Chris Heisz) joins engineer Luke Beamish to talk about how they use Golden Paths at Neo4j, a leader in graph technology whose tech is used for cancer research, GenAI, and NASA's mission to Mars. + +- 🔔 [Driving Adoption by Generating High Value Notifications in Backstage](https://www.youtube.com/watch?v=N4gCLbhxTHA&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU): How do notifications work in Backstage and what makes them such a powerful way to drive Backstage adoption within your org? Watch this talk by Spotify engineering manager Avantika Iyer and customer success engineer/Backstage community maintainer André Wanlin. [Hey, that's me! Seriously, tho, notifications are the jam. — AW] + +### Backstage at KubeCon + +The Backstage-related talks continued throughout the week during KubeCon + CloudNativeCon, including: + +- 🚀 [The Journey of Deploying Backstage in a Large Organization](https://www.youtube.com/watch?v=l7OQdrUkoVI&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU), as told by engineers Mathieu Girard and Teddy Poingt from Beneva, Canada's largest mutual insurance company. + +- 📚 [Build Your Internal Developer Platform with the Experts](https://www.youtube.com/watch?v=eqxKB6Gq7oc&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) in this hands-on workshop led by CNCF ambassadors and project maintainers. + +- 🎵 [Harmonizing Strategy and Engineering: Lessons Learnt in Building a Platform Plugin for Diverse Users](https://www.youtube.com/watch?v=2t_Pdiu3B1E&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) from Spotify senior product manager Sri Chandrasekaran and senior engineer Kate Klymkovska. [Hey, that's me! What André said, except product vision instead of notifications. — KK] + +### Watch 'em all! + +📺 There's much more to explore and dive into. Head over to the Backstage Community YouTube channel to [watch all the Backstage talks](https://www.youtube.com/playlist?list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) from BackstageCon and KubeCon. + +## Our third Backstage ContribFest + +![Backstage ContribFest at KubeCon + CloudNativeCon NA 2025!](assets/2025-11-25/kcon25-atl-contribfest.jpg) + +We talked about Backstage a ton, but we also invited attendees to help us build it. In Atlanta, we co-hosted our third [Backstage Contribfest](https://backstage.io/blog/2025/10/15/backstage-contribfest-kubecon-guide) with participants of all experience and skill levels diving into the Backstage repos to tackle issues together — and with plenty of Backstage maintainers and contributors close at hand to help out. + +A few ContribFest-fueled pull requests have already popped up in the repos: + +- fix(ui): render html in gfm mode in MarkdownContent component ([#31770](https://github.com/backstage/backstage/pull/31770)) +- catalog-react: OwnerEntityPicker case insensitive ([#31756](https://github.com/backstage/backstage/pull/31756)) +- fix: making the github urls case insensitive ([#31754](https://github.com/backstage/backstage/pull/31754)) +- fix: success announcements should be transient ([#6041](https://github.com/backstage/community-plugins/pull/6041) — ✅ merged!) + +Many thanks to all the participants and to our fellow hosts — Kurt King from Procore and Aramis Sennyey from DoorDash!🙏 + +🛠️ If you haven't yet joined our worldwide community of contributors, [there's no time like the present](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! + +## Maintainers talk: Stable foundations, MCP innovations + +![Backstage Maintainers Track at KubeCon + CloudNativeCon NA 2025!](assets/2025-11-25/kcon25-atl-maintainers.gif) + +As always, the main event for the community is the Maintainers Track session. Backstage core maintainers Ben Lambert and Patrik Oldsberg closed out the week celebrating a year of milestones — including contributions big and small from the community — while looking ahead to what's next for the framework. + +In their talk, [Backstage Celebrations: Stable Foundations and MCP Innovations](https://www.youtube.com/watch?v=evmmr-uxNsc&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU), they shared: + +- **Project Updates:** Adopters, plugins, and stars — and all the other ways the Backstage community continues to grow, including in the community-plugins repo. Plus, improvements to issue handling and more. + +- **Project Maturity:** With the new backend ([released as a stable 1.0 last year](https://backstage.io/docs/releases/v1.31.0/)) and new frontend ([adoption-ready as of this year](https://backstage.io/docs/releases/v1.42.0)) now both in place, the Backstage framework has modern foundations built for the future. With more emphasis on declarative integration (and less TypeScript code), Backstage is more accessible and easier to maintain — and plugins are simpler than ever to build and integrate. Make sure to watch the demo to see how quickly you can install plugins into a fresh Backstage instance. + +- **New Stuff ✨:** MCP, MCP, MCP, and MCP: What happens when you combine your AI tools with your engineering source of truth? Learn about Backstage + MCP, the latest improvements to the Backstage Actions Registry, authorization via new dynamic client registration and CLI authentication, and a live demo. Plus, all the latest on the framework's new design system, [Backstage UI](https://ui.backstage.io) (BUI). + +- **Roadmap:** Looking ahead to what's next for the New Frontend System, the Backstage UI rollout, the plugin ecosystem, MCP, CLI, and the release process. + +📺 Watch the entire [Backstage maintainers talk](https://www.youtube.com/watch?v=evmmr-uxNsc&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU) to dig into the past, present, and future of the framework, including live demos showcasing the newest features and capabilities. + +## See you in 🌷Amsterdam! + +![Cloud native puppies improving developer experience at KubeCon + CloudNativeCon NA 2025!](assets/2025-11-25/kcon25-atl-puppies.gif) +_Cloud native puppies improving developer experience in Atlanta. 📸 [CNCF](https://www.flickr.com/photos/143247548@N03/albums/72177720330018728/)_ + +And that's a wrap for Atlanta! We say goodbye to the home of 🎤 trap music and get ready to say hello to a [stroopwafel](https://en.wikipedia.org/wiki/Stroopwafel) fika 🧇. + +See you next year at [BackstageCon](https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/co-located-events/backstagecon/) and [KubeCon + CloudNativeCon Europe 2026](https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/) in Amsterdam! diff --git a/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx b/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx new file mode 100644 index 0000000000..0f8bdd21cf --- /dev/null +++ b/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx @@ -0,0 +1,115 @@ +--- +title: 'Backstage Wrapped 2025: Celebrating a mature framework with modern foundations' +author: Vincenzo Scamporlino, Spotify +authorURL: https://github.com/vinzscam +authorImageURL: https://avatars.githubusercontent.com/u/8433119?v=8 +--- + +![Let's blast into the New Year with a quick look back at all the big hits — backend, frontend, AI, UI, and more! See how today's Backstage is ready to build the future of developer experience.](assets/2025-12-30/wrapped2025.png) + +This year was another big one for Backstage and our worldwide community of adopters, contributors, and partners. Backstage turned five, the New Frontend System became adoption-ready, and your favorite platform for developer experience gained AI superpowers with the introduction of the Actions Registry and MCP server support. Let's blast into the New Year with a quick look back, along with a peek around the corner at what's coming next. + +{/* truncate */} + +--- + +## Busy beavers here, there, and everywhere + +As in years past, we've visualized how our vibrant global community lights up the Backstage open source repo with enthusiasm, creativity, and commits. This year, we're visualizing contributions to the Community Plugins repo, too. Can you spot your name in the videos below? + + + +_All 7.7k commits to `backstage/backstage` in 2025_ + + + +_All 3.3k commits to `backstage/community-plugins` in 2025_ + +Couldn't spot your name in the videos? Here's one more chance — every contributor who helped shape Backstage in 2025, all in one picture. + +![Backstage 2025 contributors](assets/2025-12-30/contributors-wrapped-2025.png) + +## Mature for a five-year-old + +![Backstage community stats: 3.4k+ adopters, 1,8k contributors, 15k Discord members, 7k project forks, 68k total contributions, 250+ open source plugins, 31k stars](assets/2025-12-30/wrapped2025-stats.png) +_The growing Backstage ecosystem, by the numbers_ + +Earlier this year, the Backstage project [celebrated its 5th birthday][bday] on the main stage at [KubeCon + CloudNativeCon in London][kcon-eu]. + +While the framework still isn't old enough to shave, it's matured into the industry standard for internal developer portals (IDPs). According to [this DX survey][dx] released in March: + +> Backstage is dominating the IDP market, holding an impressive 89% market share compared to SaaS competitors and achieving a remarkable 67% overall market penetration. These percentages are even higher for enterprise companies. + +As you can see above in the [latest project stats][video], the Backstage ecosystem continues to expand, growing in adopters and contributors, as well as plugins and third-party service providers. + +Want to dive into more Backstage stats? Check out the project's health scores and more on the relaunched [LFX Insights][lfx] site. + +[bday]: https://www.youtube.com/watch?v=JqG1wey7-Ao&t=563s&list=PL8iP9yIjU0Q3K8LV_a9CcFmhvvNTAUzL1 +[kcon-eu]: https://backstage.io/blog/2025/04/29/backstagecon-kubecon-25-london#big-birthday-energy-on-the-big-stage +[dx]: https://newsletter.getdx.com/p/backstage-and-the-developer-portal-market +[video]: https://www.youtube.com/watch?v=Wih0a1v1Et0&list=PL8iP9yIjU0Q33vpSaBlAvIhgDb-9smXUU +[lfx]: https://insights.linuxfoundation.org/project/backstage + +## Building the future on modern foundations + +Just because Backstage has cemented its place as the standard for IDPs doesn't mean it's sitting still. From backend to frontend, from AI to UI, the technical foundations of the platform have been modernized, updated, and revamped — this is why today's Backstage is ready to build the future of developer experience. + +Top 2025 highlights: + +- Goodbye to the old backend system +- New Frontend System is adoption-ready +- Hello to the Actions Registry + MCP server support +- Introducing a new design system with Backstage UI + +With the [New Backend System][backend] released as a stable 1.0 last year and the [New Frontend System][frontend] becoming [adoption-ready][1-42] this year, Backstage is easier than ever to use, maintain, and build on — making it both more accessible and more capable for adopters, old and new. + +This year also saw the debut of the [Actions Registry][actions] and MCP server support — so now Backstage can empower both your human developers and their AI helpers. + +And [Backstage UI][ui] is coming! The alpha release of the upcoming design system made huge strides in 2025, preparing the way for a new level of usability throughout the Backstage experience. + +Catch up on a whole year's worth of improvements — and see where Backstage is heading next — in the recordings below. + +[backend]: https://backstage.io/docs/backend-system/ +[frontend]: https://backstage.io/docs/frontend-system/ +[1-42]: https://backstage.io/docs/releases/v1.42.0 +[actions]: https://backstage.io/docs/backend-system/core-services/actions-registry +[ui]: https://ui.backstage.io/ + + + +_Watch the Backstage maintainers' talk at [KubeCon in London][lon], where they demoed the New Frontend System and introduced the Actions Registry._ + + +_Watch the maintainers' talk at [KubeCon in Atlanta][atl], where they shared +work towards an AI-Native Backstage, progress on Backstage UI, and more._ + +[lon]: https://backstage.io/blog/2025/04/29/backstagecon-kubecon-25-london +[atl]: https://backstage.io/blog/2025/11/25/backstagecon-kubecon-25-atlanta + +## See you in 2026! + +Happy New Year, Backstage Community! See you in the repos! diff --git a/microsite/blog/assets/2025-10-15/backstage-contribfest-kubecon-guide-header.png b/microsite/blog/assets/2025-10-15/backstage-contribfest-kubecon-guide-header.png index 23ed0309c1..cf09601b23 100644 Binary files a/microsite/blog/assets/2025-10-15/backstage-contribfest-kubecon-guide-header.png and b/microsite/blog/assets/2025-10-15/backstage-contribfest-kubecon-guide-header.png differ diff --git a/microsite/blog/assets/2025-11-25/backstagecon-kubecon-na-2025-header.png b/microsite/blog/assets/2025-11-25/backstagecon-kubecon-na-2025-header.png new file mode 100644 index 0000000000..ff2010776f Binary files /dev/null and b/microsite/blog/assets/2025-11-25/backstagecon-kubecon-na-2025-header.png differ diff --git a/microsite/blog/assets/2025-11-25/kcon25-atl-contribfest.jpg b/microsite/blog/assets/2025-11-25/kcon25-atl-contribfest.jpg new file mode 100644 index 0000000000..ae3414f104 Binary files /dev/null and b/microsite/blog/assets/2025-11-25/kcon25-atl-contribfest.jpg differ diff --git a/microsite/blog/assets/2025-11-25/kcon25-atl-maintainers.gif b/microsite/blog/assets/2025-11-25/kcon25-atl-maintainers.gif new file mode 100644 index 0000000000..ad71bc1123 Binary files /dev/null and b/microsite/blog/assets/2025-11-25/kcon25-atl-maintainers.gif differ diff --git a/microsite/blog/assets/2025-11-25/kcon25-atl-puppies.gif b/microsite/blog/assets/2025-11-25/kcon25-atl-puppies.gif new file mode 100644 index 0000000000..462afd87cb Binary files /dev/null and b/microsite/blog/assets/2025-11-25/kcon25-atl-puppies.gif differ diff --git a/microsite/blog/assets/2025-11-25/kcon25-atl-slideshow.gif b/microsite/blog/assets/2025-11-25/kcon25-atl-slideshow.gif new file mode 100644 index 0000000000..a7e5767441 Binary files /dev/null and b/microsite/blog/assets/2025-11-25/kcon25-atl-slideshow.gif differ diff --git a/microsite/blog/assets/2025-12-30/contributors-wrapped-2025.png b/microsite/blog/assets/2025-12-30/contributors-wrapped-2025.png new file mode 100644 index 0000000000..7aa16f106e Binary files /dev/null and b/microsite/blog/assets/2025-12-30/contributors-wrapped-2025.png differ diff --git a/microsite/blog/assets/2025-12-30/wrapped2025-stats.png b/microsite/blog/assets/2025-12-30/wrapped2025-stats.png new file mode 100644 index 0000000000..59d31f142c Binary files /dev/null and b/microsite/blog/assets/2025-12-30/wrapped2025-stats.png differ diff --git a/microsite/blog/assets/2025-12-30/wrapped2025.png b/microsite/blog/assets/2025-12-30/wrapped2025.png new file mode 100644 index 0000000000..b99768dfda Binary files /dev/null and b/microsite/blog/assets/2025-12-30/wrapped2025.png differ diff --git a/microsite/data/plugins/airbrake.yaml b/microsite/data/plugins/airbrake.yaml index 8facf13185..59910df629 100644 --- a/microsite/data/plugins/airbrake.yaml +++ b/microsite/data/plugins/airbrake.yaml @@ -6,5 +6,5 @@ category: Monitoring description: Access Airbrake error monitoring and other integrations from within Backstage documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/airbrake/plugins/airbrake iconUrl: https://wp-assets.airbrake.io/wp-content/uploads/2020/10/05222904/Square-white-A-on-Orange.png -npmPackageName: '@backstage/plugin-airbrake' +npmPackageName: '@backstage-community/plugin-airbrake' addedDate: '2022-01-10' diff --git a/microsite/data/plugins/analytics-module-ga.yaml b/microsite/data/plugins/analytics-module-ga.yaml index 3dcb27bcd5..537a360c8c 100644 --- a/microsite/data/plugins/analytics-module-ga.yaml +++ b/microsite/data/plugins/analytics-module-ga.yaml @@ -6,5 +6,5 @@ category: Monitoring description: Track usage of your Backstage instance using Google Analytics 4. documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/analytics/plugins/analytics-module-ga4#readme iconUrl: /img/ga-icon.png -npmPackageName: '@backstage/plugin-analytics-module-ga4' +npmPackageName: '@backstage-community/plugin-analytics-module-ga4' addedDate: '2021-10-07' diff --git a/microsite/data/plugins/announcements.yaml b/microsite/data/plugins/announcements.yaml index a9c66d6203..f24965f10a 100644 --- a/microsite/data/plugins/announcements.yaml +++ b/microsite/data/plugins/announcements.yaml @@ -1,7 +1,7 @@ --- title: Announcements -author: procore-oss -authorUrl: https://github.com/procore-oss +author: kurtaking +authorUrl: https://github.com/kurtaking category: Discovery description: Write and share announcements within Backstage. documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/announcements/ diff --git a/microsite/data/plugins/apollo-explorer.yaml b/microsite/data/plugins/apollo-explorer.yaml index 2b0f834f92..83d121e055 100644 --- a/microsite/data/plugins/apollo-explorer.yaml +++ b/microsite/data/plugins/apollo-explorer.yaml @@ -6,5 +6,5 @@ category: Debugging description: Integrates Apollo Explorer graphs as a tool to browse GraphQL API endpoints inside Backstage. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/apollo-explorer/plugins/apollo-explorer/README.md iconUrl: /img/apollo-explorer.png -npmPackageName: '@backstage/plugin-apollo-explorer' +npmPackageName: '@backstage-community/plugin-apollo-explorer' addedDate: '2022-07-20' diff --git a/microsite/data/plugins/aws-amazon-ecs.yaml b/microsite/data/plugins/aws-amazon-ecs.yaml index a94779babd..e16028d802 100644 --- a/microsite/data/plugins/aws-amazon-ecs.yaml +++ b/microsite/data/plugins/aws-amazon-ecs.yaml @@ -6,5 +6,5 @@ category: Infrastructure description: View resources related to Amazon Elastic Container Service for your components in Backstage. documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/plugins/ecs#readme iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/amazon-ecs-logo.png -npmPackageName: 'https://www.npmjs.com/package/@aws/amazon-ecs-plugin-for-backstage' +npmPackageName: '@aws/amazon-ecs-plugin-for-backstage' addedDate: '2024-04-22' diff --git a/microsite/data/plugins/aws-codebuild.yaml b/microsite/data/plugins/aws-codebuild.yaml index 2c12f92dce..504730402f 100644 --- a/microsite/data/plugins/aws-codebuild.yaml +++ b/microsite/data/plugins/aws-codebuild.yaml @@ -6,5 +6,5 @@ category: CI/CD description: View resources related to AWS CodeBuild for your components in Backstage. documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/plugins/codebuild#readme iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/aws-codebuild-logo.png -npmPackageName: 'https://www.npmjs.com/package/@aws/aws-codebuild-plugin-for-backstage' +npmPackageName: '@aws/aws-codebuild-plugin-for-backstage' addedDate: '2024-04-22' diff --git a/microsite/data/plugins/aws-codepipeline.yaml b/microsite/data/plugins/aws-codepipeline.yaml index 16986b6718..a2eb8de230 100644 --- a/microsite/data/plugins/aws-codepipeline.yaml +++ b/microsite/data/plugins/aws-codepipeline.yaml @@ -6,5 +6,5 @@ category: CI/CD description: View resources related to AWS CodePipeline for your components in Backstage. documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/plugins/codepipeline#readme iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/aws-codepipeline-logo.png -npmPackageName: 'https://www.npmjs.com/package/@aws/aws-codepipeline-plugin-for-backstage' +npmPackageName: '@aws/aws-codepipeline-plugin-for-backstage' addedDate: '2024-04-22' diff --git a/microsite/data/plugins/azure-pipelines.yaml b/microsite/data/plugins/azure-pipelines.yaml index e0e3534fdb..f9536eb192 100644 --- a/microsite/data/plugins/azure-pipelines.yaml +++ b/microsite/data/plugins/azure-pipelines.yaml @@ -6,5 +6,5 @@ category: CI/CD description: Easily view your Azure Pipelines within the Software Catalog documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/azure-devops/plugins/azure-devops/README.md iconUrl: /img/azure-pipelines.svg -npmPackageName: '@backstage/plugin-azure-devops' +npmPackageName: '@backstage-community/plugin-azure-devops' addedDate: '2021-12-22' diff --git a/microsite/data/plugins/badges.yaml b/microsite/data/plugins/badges.yaml index 789aaf155d..9452054810 100644 --- a/microsite/data/plugins/badges.yaml +++ b/microsite/data/plugins/badges.yaml @@ -6,5 +6,5 @@ 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/community-plugins/blob/main/workspaces/badges/plugins/badges/README.md iconUrl: /img/badges.svg -npmPackageName: '@backstage/plugin-badges' +npmPackageName: '@backstage-community/plugin-badges' addedDate: '2021-09-29' diff --git a/microsite/data/plugins/bazaar.yaml b/microsite/data/plugins/bazaar.yaml index 1130822e77..d14aaaecd8 100644 --- a/microsite/data/plugins/bazaar.yaml +++ b/microsite/data/plugins/bazaar.yaml @@ -5,5 +5,5 @@ category: Discovery description: A marketplace where engineers can propose projects suitable for inner sourcing documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/bazaar/plugins/bazaar/README.md iconUrl: /img/bazaar.svg -npmPackageName: '@backstage/plugin-bazaar' +npmPackageName: '@backstage-community/plugin-bazaar' addedDate: '2022-01-11' diff --git a/microsite/data/plugins/bitrise.yaml b/microsite/data/plugins/bitrise.yaml index 5cbfe5aabf..31f50a007c 100644 --- a/microsite/data/plugins/bitrise.yaml +++ b/microsite/data/plugins/bitrise.yaml @@ -6,5 +6,5 @@ category: CI/CD description: View Bitrise builds and download the build artifacts within Backstage. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/bitrise/plugins/bitrise/README.md iconUrl: https://avatars.githubusercontent.com/u/7174390?s=400&v=4 -npmPackageName: '@backstage/plugin-bitrise' +npmPackageName: '@backstage-community/plugin-bitrise' addedDate: '2021-03-01' diff --git a/microsite/data/plugins/conviso-platform.yaml b/microsite/data/plugins/conviso-platform.yaml new file mode 100644 index 0000000000..7017f74a3f --- /dev/null +++ b/microsite/data/plugins/conviso-platform.yaml @@ -0,0 +1,10 @@ +--- +title: Conviso Platform +author: Conviso Application Security +authorUrl: https://convisoappsec.com +category: Security +description: Import your Backstage catalog entities as security assets in Conviso Platform. +documentation: https://github.com/convisoappsec/backstage-plugin-conviso/blob/main/README.md +iconUrl: https://raw.githubusercontent.com/convisoappsec/backstage-plugin-conviso/main/assets/convisoappsec_logo.png +npmPackageName: 'backstage-plugin-conviso' +addedDate: '2025-11-14' diff --git a/microsite/data/plugins/digital.ai-deploy.yaml b/microsite/data/plugins/digital.ai-deploy.yaml index a36dd08ffb..62203a99ea 100644 --- a/microsite/data/plugins/digital.ai-deploy.yaml +++ b/microsite/data/plugins/digital.ai-deploy.yaml @@ -6,7 +6,7 @@ category: CI/CD description: The plugin offers integration with Digital.ai Deploy and backstage components and services. It provide access to deployments and reports. documentation: https://docs.digital.ai/deploy/docs/concept/xl-deploy-backstage-overview iconUrl: /img/digital.ai-deploy.svg -npmPackageName: '@digital.ai/plugin-dai-deploy' +npmPackageName: '@digital-ai/plugin-dai-deploy' tags: - ci - cd diff --git a/microsite/data/plugins/firehydrant.yaml b/microsite/data/plugins/firehydrant.yaml index 7aa00675b4..f5ccd3629d 100644 --- a/microsite/data/plugins/firehydrant.yaml +++ b/microsite/data/plugins/firehydrant.yaml @@ -6,5 +6,5 @@ category: Monitoring description: View service incidents information from FireHydrant, such as active incidents and incident metrics, directly within Backstage. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/firehydrant/plugins/firehydrant/README.md iconUrl: https://github.com/backstage/community-plugins/blob/main/workspaces/firehydrant/plugins/firehydrant/doc/firehydrant_logo.png -npmPackageName: '@backstage/plugin-firehydrant' +npmPackageName: '@backstage-community/plugin-firehydrant' addedDate: '2021-08-18' diff --git a/microsite/data/plugins/fossa.yaml b/microsite/data/plugins/fossa.yaml index a1a66eb7d6..34610c64e4 100644 --- a/microsite/data/plugins/fossa.yaml +++ b/microsite/data/plugins/fossa.yaml @@ -6,5 +6,5 @@ category: Quality description: View FOSSA license compliance of your components in Backstage. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/fossa/plugins/fossa/README.md iconUrl: https://avatars0.githubusercontent.com/u/9543448?s=400&v=4 -npmPackageName: '@backstage/plugin-fossa' +npmPackageName: '@backstage-community/plugin-fossa' addedDate: '2020-12-10' diff --git a/microsite/data/plugins/kubernetes-provider.yaml b/microsite/data/plugins/kubernetes-provider.yaml index e0971d55be..b23ab03caa 100644 --- a/microsite/data/plugins/kubernetes-provider.yaml +++ b/microsite/data/plugins/kubernetes-provider.yaml @@ -6,5 +6,5 @@ category: Kubernetes description: Import Kubernetes resources into Backstage Components documentation: https://github.com/AntoineDao/backstage-provider-kubernetes#readme iconUrl: https://avatars.githubusercontent.com/u/13629408 -npmPackageName: https://www.npmjs.com/package/@antoinedao/backstage-provider-kubernetes +npmPackageName: '@antoinedao/backstage-provider-kubernetes' addedDate: '2023-04-10' diff --git a/microsite/data/plugins/linguist.yaml b/microsite/data/plugins/linguist.yaml index fb8e88b747..15b79b3682 100644 --- a/microsite/data/plugins/linguist.yaml +++ b/microsite/data/plugins/linguist.yaml @@ -6,5 +6,5 @@ category: Metadata description: View the programming language break down for your entities within the Software Catalog documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/linguist/plugins/linguist/README.md iconUrl: /img/linguist.svg -npmPackageName: '@backstage/plugin-linguist' +npmPackageName: '@backstage-community/plugin-linguist' addedDate: '2023-06-17' diff --git a/microsite/data/plugins/octopus-deploy.yaml b/microsite/data/plugins/octopus-deploy.yaml index cfc5e2c9e5..eb19f60e72 100644 --- a/microsite/data/plugins/octopus-deploy.yaml +++ b/microsite/data/plugins/octopus-deploy.yaml @@ -6,5 +6,5 @@ category: CI/CD description: Easily view your Octopus Deploy releases within the Software Catalog documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/octopus-deploy/plugins/octopus-deploy/README.md iconUrl: /img/octopus-deploy.svg -npmPackageName: '@backstage/plugin-octopus-deploy' +npmPackageName: '@backstage-community/plugin-octopus-deploy' addedDate: '2023-02-24' diff --git a/microsite/data/plugins/opencost.yaml b/microsite/data/plugins/opencost.yaml index 95973e3b0c..9e98c85bcb 100644 --- a/microsite/data/plugins/opencost.yaml +++ b/microsite/data/plugins/opencost.yaml @@ -6,5 +6,5 @@ category: Monitoring description: OpenCost provides cloud cost monitoring for your cloud native environments. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/opencost/plugins/opencost/README.md iconUrl: /img/opencost.png -npmPackageName: '@backstage/plugin-opencost' +npmPackageName: '@backstage-community/plugin-opencost' addedDate: '2023-10-26' diff --git a/microsite/data/plugins/puppetdb.yaml b/microsite/data/plugins/puppetdb.yaml index 650d8318d3..26e8bbd3ef 100644 --- a/microsite/data/plugins/puppetdb.yaml +++ b/microsite/data/plugins/puppetdb.yaml @@ -6,7 +6,7 @@ category: Configuration Management description: Visualize resource information and Puppet facts from PuppetDB. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/puppetdb/plugins/puppetdb/README.md iconUrl: /img/puppet.png -npmPackageName: '@backstage/plugin-puppetdb' +npmPackageName: '@backstage-community/plugin-puppetdb' tags: - puppet - puppetdb diff --git a/microsite/data/plugins/renovate-hoster.yaml b/microsite/data/plugins/renovate-hoster.yaml index 4cf4f65956..06187581bf 100644 --- a/microsite/data/plugins/renovate-hoster.yaml +++ b/microsite/data/plugins/renovate-hoster.yaml @@ -5,5 +5,5 @@ authorUrl: https://github.com/secustor category: Quality description: This plugin enables you to host Renovate CLI yourself inside of Backstage and extract data from it. documentation: https://github.com/secustor/backstage-plugins/tree/main/plugins/renovate -npmPackageName: '@backstage/backstage-plugin-renovate' +npmPackageName: '@secustor/backstage-plugin-renovate' addedDate: '2025-03-31' diff --git a/microsite/data/plugins/sds_workspace.yaml b/microsite/data/plugins/sds_workspace.yaml new file mode 100644 index 0000000000..43d9a0f20e --- /dev/null +++ b/microsite/data/plugins/sds_workspace.yaml @@ -0,0 +1,10 @@ +--- +title: Citrix Secure Developer Spaces (SDS) +author: Citrix +authorUrl: https://github.com/strong-network +category: Development +description: Connect Backstage with Citrix Secure Developer Spaces (SDS) to view and create developer workspaces. +documentation: https://www.npmjs.com/package/@citrixcloud/backstage-sds-workspaces +iconUrl: /img/citrix_sds.png +npmPackageName: '@citrixcloud/backstage-sds-workspaces' +addedDate: '2025-10-07' diff --git a/microsite/data/plugins/shortcuts.yaml b/microsite/data/plugins/shortcuts.yaml index b420e28e2f..7d4e050df0 100644 --- a/microsite/data/plugins/shortcuts.yaml +++ b/microsite/data/plugins/shortcuts.yaml @@ -6,5 +6,5 @@ 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/community-plugins/blob/main/workspaces/shortcuts/plugins/shortcuts/README.md iconUrl: /img/shortcuts.svg -npmPackageName: '@backstage/plugin-shortcuts' +npmPackageName: '@backstage-community/plugin-shortcuts' addedDate: '2021-10-06' diff --git a/microsite/data/plugins/sonarqube.yaml b/microsite/data/plugins/sonarqube.yaml index 72c662e2fa..48793adea9 100644 --- a/microsite/data/plugins/sonarqube.yaml +++ b/microsite/data/plugins/sonarqube.yaml @@ -6,5 +6,5 @@ category: Quality description: Components to display code quality metrics from SonarCloud and SonarQube. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/sonarqube/plugins/sonarqube/README.md iconUrl: /img/sonarqube-icon.svg -npmPackageName: '@backstage/plugin-sonarqube' +npmPackageName: '@backstage-community/plugin-sonarqube' addedDate: '2020-11-03' diff --git a/microsite/data/plugins/stack-overflow.yaml b/microsite/data/plugins/stack-overflow.yaml index b058ce90fe..373c08de8f 100644 --- a/microsite/data/plugins/stack-overflow.yaml +++ b/microsite/data/plugins/stack-overflow.yaml @@ -6,5 +6,5 @@ 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/community-plugins/blob/main/workspaces/stack-overflow/plugins/stack-overflow/README.md iconUrl: /img/stack-overflow-logo.svg -npmPackageName: '@backstage/plugin-stack-overflow' +npmPackageName: '@backstage-community/plugin-stack-overflow' addedDate: '2022-06-14' diff --git a/microsite/data/plugins/stackoverflow-teams.yaml b/microsite/data/plugins/stackoverflow-teams.yaml index c2fc1f5480..262a6e9c69 100644 --- a/microsite/data/plugins/stackoverflow-teams.yaml +++ b/microsite/data/plugins/stackoverflow-teams.yaml @@ -6,5 +6,5 @@ category: Discovery description: Provide seamless access to Stack Overflow Teams most relevant data, allowing you to display the top users, top tags, and top questions directly within Backstage. It also allows to securely create SO Teams questions from Backstage. documentation: https://stackoverflowteams.help/en/articles/9692515-backstage-io-integration iconUrl: /img/stack-overflow-logo.svg -npmPackageName: 'backstage-plugin-stack-overflow-teams' +npmPackageName: '@stackoverflow/backstage-plugin-stack-overflow-teams' addedDate: '2025-06-10' diff --git a/microsite/data/plugins/targetboard.yaml b/microsite/data/plugins/targetboard.yaml new file mode 100644 index 0000000000..2cac367eec --- /dev/null +++ b/microsite/data/plugins/targetboard.yaml @@ -0,0 +1,10 @@ +--- +title: TargetBoard +author: TargetBoard +authorUrl: https://www.targetboard.ai +category: Metrics +description: Embed TargetBoard dashboards and metric cards inside Backstage to visualize real-time engineering performance, KPIs, and delivery metrics. +documentation: https://github.com/targetboard/backstage-plugin/blob/main/README.md +iconUrl: https://app.targetboard.ai/assets/TargetBoard-Backstage-logo.svg +npmPackageName: '@targetboard/backstage-plugin' +addedDate: '2025-11-02' diff --git a/microsite/data/plugins/template-designer.yaml b/microsite/data/plugins/template-designer.yaml new file mode 100644 index 0000000000..86aa8a97f0 --- /dev/null +++ b/microsite/data/plugins/template-designer.yaml @@ -0,0 +1,10 @@ +--- +title: Template Designer +author: tduniec +authorUrl: https://github.com/tduniec +category: Productivity +description: Template Designer turns blank Backstage YAML into a storyboard-like canvas, guiding anyone through drag-and-drop scaffolder authoring before ever touching code. Rally non-experts, broadcast best practices, and accelerate template launches directly inside Backstage. +documentation: https://github.com/tduniec/template-designer-plugin/blob/main/README.md +iconUrl: https://raw.githubusercontent.com/tduniec/template-designer-plugin/main/img/logo/templateDesignerLogo.png +npmPackageName: '@tduniec/plugin-template-designer' +addedDate: 2025-11-19 diff --git a/microsite/data/plugins/torque.yaml b/microsite/data/plugins/torque.yaml index 1480c3bad9..a469c455fc 100644 --- a/microsite/data/plugins/torque.yaml +++ b/microsite/data/plugins/torque.yaml @@ -8,5 +8,5 @@ description: | 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' +npmPackageName: '@qtorque/backstage-torque-plugin' addedDate: '2023-01-25' diff --git a/microsite/data/plugins/usage-statistics.yaml b/microsite/data/plugins/usage-statistics.yaml new file mode 100644 index 0000000000..d32fb54ac4 --- /dev/null +++ b/microsite/data/plugins/usage-statistics.yaml @@ -0,0 +1,9 @@ +--- +title: Usage Statistics +author: CodeVerse-GP +authorUrl: https://github.com/CodeVerse-GP +category: Monitoring +description: Shows usage statistics for scaffolder templates +documentation: https://github.com/CodeVerse-GP/usage-statistics/blob/main/README.md +npmPackageName: '@codeverse-gp/plugin-usage-statistics' +addedDate: '2025-11-05' diff --git a/microsite/data/plugins/xcmetrics.yaml b/microsite/data/plugins/xcmetrics.yaml index f02b195eb5..09a8b1547f 100644 --- a/microsite/data/plugins/xcmetrics.yaml +++ b/microsite/data/plugins/xcmetrics.yaml @@ -6,5 +6,5 @@ category: Monitoring description: Discover valuable insights hiding inside Xcode’s build logs. documentation: https://xcmetrics.io/ iconUrl: /img/xcmetrics-icon.png -npmPackageName: '@backstage/plugin-xcmetrics' +npmPackageName: '@backstage-community/plugin-xcmetrics' addedDate: '2021-08-06' diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 850e6058af..a08f0085a5 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -273,6 +273,10 @@ const config: Config = { from: '/docs/getting-started/app-custom-theme', to: '/docs/conf/user-interface', }, + { + from: '/docs/plugins/existing-plugins', + to: '/docs/plugins/', + }, ], }), [ diff --git a/microsite/package.json b/microsite/package.json index a042a23286..edf07abb02 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -28,7 +28,7 @@ "@docusaurus/preset-classic": "^3.1.1", "@docusaurus/types": "^3.1.1", "@mdx-js/react": "^3.0.0", - "@swc/core": "^1.3.46", + "@swc/core": "^1.15.6", "clsx": "^2.0.0", "docusaurus-plugin-openapi-docs": "^4.3.0", "docusaurus-plugin-sass": "^0.2.3", @@ -44,11 +44,10 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^3.1.1", "@docusaurus/tsconfig": "^3.1.1", - "@types/luxon": "^3.0.0", "@types/webpack-env": "^1.18.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "prettier": "^2.6.2", - "typescript": "~5.2.0", + "typescript": "~5.7.0", "yaml-loader": "^0.8.0" } } diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 9a4f03620f..fed5de8015 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -18,664 +18,742 @@ const scaffolderSidebar = tryToLoadCustomSidebar( '../docs/features/software-templates/api/sidebar.ts', ); +function sidebarElementWithIndex( + element: { + description?: string; + differentiator?: string; + label: string; + docId?: string; + }, + children: Array, +) { + const { label, description, differentiator = '' } = element; + return { + type: 'category', + label, + description, + link: { + type: 'generated-index', + title: label, + slug: `/${differentiator}${label + .toLowerCase() + .replace(/[^a-z0-9]/g, '-')}/generated-index`, + }, + items: children, + }; +} + export default { - docs: { - Overview: [ + docs: [ + sidebarElementWithIndex({ label: 'Overview' }, [ 'overview/what-is-backstage', 'overview/technical-overview', 'overview/architecture-overview', 'overview/roadmap', 'overview/threat-model', 'overview/versioning-policy', - ], - 'Getting Started': [ + ]), + sidebarElementWithIndex({ label: 'Getting Started' }, [ 'getting-started/index', - { - type: 'category', - label: 'Configuring Backstage', - items: [ - 'getting-started/config/database', - 'getting-started/config/authentication', - 'getting-started/configure-app-with-plugins', - 'getting-started/homepage', - ], - }, - { - type: 'category', - label: 'Deploying Backstage', - items: [ - 'deployment/index', - 'deployment/scaling', - 'deployment/docker', - 'deployment/k8s', - ], - }, - { - type: 'category', - label: 'Using Backstage', - items: [ - 'getting-started/logging-in', - 'getting-started/register-a-component', - 'getting-started/create-a-component', - ], - }, + sidebarElementWithIndex({ label: 'Configuring Backstage' }, [ + 'getting-started/config/database', + 'getting-started/config/authentication', + 'getting-started/configure-app-with-plugins', + 'getting-started/homepage', + ]), + sidebarElementWithIndex({ label: 'Deploying Backstage' }, [ + 'deployment/index', + 'deployment/scaling', + 'deployment/docker', + 'deployment/k8s', + ]), + sidebarElementWithIndex({ label: 'Using Backstage' }, [ + 'getting-started/logging-in', + 'getting-started/register-a-component', + 'getting-started/create-a-component', + ]), 'overview/support', 'getting-started/keeping-backstage-updated', - ], + ]), ...(process.env.GOLDEN_PATH - ? { - 'Golden Paths': [ + ? [ + sidebarElementWithIndex({ label: 'Golden Paths' }, [ + sidebarElementWithIndex({ label: '001 - Create an App' }, [ + 'golden-path/create-app/index', + 'golden-path/create-app/npx-create-app', + 'golden-path/create-app/local-development', + 'golden-path/create-app/installing-plugins', + 'golden-path/create-app/logging-in', + 'golden-path/create-app/custom-theme', + 'golden-path/create-app/keeping-backstage-updated', + ]), + sidebarElementWithIndex({ label: '002 - Plugins' }, [ + 'golden-path/plugins/index', + 'golden-path/plugins/why-build-plugins', + 'golden-path/plugins/sustainable-plugin-development', + sidebarElementWithIndex({ label: 'Backend Plugins' }, [ + 'golden-path/plugins/backend/001-first-steps', + 'golden-path/plugins/backend/002-poking-around', + ]), + ]), + ]), + ] + : []), + sidebarElementWithIndex( + { + label: 'Core Features', + description: 'Features powering the core of Backstage.', + }, + [ + sidebarElementWithIndex( + { + label: 'Auth and Identity', + description: 'Authentication and identity management features.', + }, + [ + 'auth/index', + sidebarElementWithIndex( + { + label: 'Included providers', + description: + 'Pre-configured authentication providers included with Backstage.', + }, + [ + 'auth/auth0/provider', + 'auth/atlassian/provider', + 'auth/aws-alb/provider', + 'auth/microsoft/provider', + 'auth/microsoft/easy-auth', + 'auth/bitbucket/provider', + 'auth/bitbucketServer/provider', + 'auth/cloudflare/provider', + 'auth/github/provider', + 'auth/gitlab/provider', + 'auth/google/provider', + 'auth/google/gcp-iap-auth', + 'auth/guest/provider', + 'auth/okta/provider', + 'auth/oauth2-proxy/provider', + 'auth/onelogin/provider', + 'auth/vmware-cloud/provider', + ], + ), + 'auth/identity-resolver', + 'auth/oauth', + 'auth/oidc', + 'auth/add-auth-provider', + 'auth/service-to-service-auth', + 'auth/autologout', + 'auth/troubleshooting', + ], + ), + sidebarElementWithIndex( + { + label: 'Kubernetes', + description: 'Use Kubernetes right from Backstage.', + }, + [ + 'features/kubernetes/overview', + 'features/kubernetes/installation', + 'features/kubernetes/configuration', + 'features/kubernetes/authentication', + 'features/kubernetes/authentication-strategies', + 'features/kubernetes/troubleshooting', + 'features/kubernetes/proxy', + ], + ), + sidebarElementWithIndex( + { + label: 'Notifications', + description: 'Know when important events happen in Backstage.', + }, + [ + 'notifications/index', + 'notifications/processors', + 'notifications/usage', + ], + ), + sidebarElementWithIndex( + { + label: 'Permissions', + description: 'Access control for your Backstage instance.', + }, + [ + 'permissions/overview', + 'permissions/concepts', + 'permissions/getting-started', + 'permissions/writing-a-policy', + 'permissions/frontend-integration', + 'permissions/custom-rules', + sidebarElementWithIndex( + { + label: 'Tutorial: using Permissions in your plugin', + description: + 'Step-by-step guide for plugin authors on using Permissions.', + }, + [ + 'permissions/plugin-authors/01-setup', + 'permissions/plugin-authors/02-adding-a-basic-permission-check', + 'permissions/plugin-authors/03-adding-a-resource-permission-check', + 'permissions/plugin-authors/04-authorizing-access-to-paginated-data', + 'permissions/plugin-authors/05-frontend-authorization', + ], + ), + ], + ), + sidebarElementWithIndex( + { label: 'Search', description: 'Using Search within Backstage.' }, + [ + 'features/search/search-overview', + 'features/search/getting-started', + 'features/search/concepts', { type: 'category', - label: '001 - create-app', - items: [ - 'golden-path/create-app/index', - 'golden-path/create-app/npx-create-app', - 'golden-path/create-app/local-development', - 'golden-path/create-app/installing-plugins', - 'golden-path/create-app/logging-in', - 'golden-path/create-app/custom-theme', - 'golden-path/create-app/keeping-backstage-updated', - ], + label: 'API', + link: + searchSidebar.length > 0 + ? { + type: 'generated-index', + title: 'Search API', + slug: '/category/search-api', + } + : { + type: 'doc', + id: 'openapi/generated-docs/404', + }, + items: searchSidebar, }, + 'features/search/architecture', + 'features/search/search-engines', + 'features/search/collators', + 'features/search/how-to-guides', + ], + ), + sidebarElementWithIndex( + { + label: 'Software Catalog', + description: 'Manage and explore your software components.', + }, + [ + 'features/software-catalog/software-catalog-overview', + 'features/software-catalog/life-of-an-entity', + 'features/software-catalog/configuration', + 'features/software-catalog/system-model', + 'features/software-catalog/descriptor-format', + 'features/software-catalog/references', + 'features/software-catalog/well-known-annotations', + 'features/software-catalog/well-known-relations', + 'features/software-catalog/well-known-statuses', + 'features/software-catalog/extending-the-model', + 'features/software-catalog/external-integrations', + 'features/software-catalog/catalog-customization', { type: 'category', - label: '002 - Plugins', - items: [ - 'golden-path/plugins/index', - 'golden-path/plugins/why-build-plugins', - 'golden-path/plugins/sustainable-plugin-development', - { - type: 'category', - label: 'Backend Plugins', - items: [ - 'golden-path/plugins/backend/001-first-steps', - 'golden-path/plugins/backend/002-poking-around', - ], - }, - ], + label: 'API', + link: + catalogSidebar.length > 0 + ? { + type: 'generated-index', + title: 'Catalog API', + slug: '/category/catalog-api', + } + : { + type: 'doc', + id: 'openapi/generated-docs/404', + }, + items: catalogSidebar, + }, + 'features/software-catalog/creating-the-catalog-graph', + 'features/software-catalog/faq', + ], + ), + sidebarElementWithIndex( + { + label: 'Software Templates', + description: 'Create and manage software templates.', + }, + [ + 'features/software-templates/software-templates-index', + 'features/software-templates/configuration', + 'features/software-templates/adding-templates', + 'features/software-templates/writing-templates', + 'features/software-templates/input-examples', + 'features/software-templates/builtin-actions', + 'features/software-templates/writing-custom-actions', + 'features/software-templates/writing-tests-for-actions', + 'features/software-templates/writing-custom-field-extensions', + 'features/software-templates/writing-custom-step-layouts', + 'features/software-templates/authorizing-scaffolder-template-details', + 'features/software-templates/migrating-to-rjsf-v5', + 'features/software-templates/migrating-from-v1beta2-to-v1beta3', + 'features/software-templates/dry-run-testing', + 'features/software-templates/experimental', + 'features/software-templates/templating-extensions', + { + type: 'category', + label: 'API', + link: + scaffolderSidebar.length > 0 + ? { + type: 'generated-index', + title: 'Scaffolder API', + slug: '/category/scaffolder-api', + } + : { + type: 'doc', + id: 'openapi/generated-docs/404', + }, + items: scaffolderSidebar, }, ], - } - : {}), - 'Core Features': [ - { - type: 'category', - label: 'Auth and Identity', - items: [ - 'auth/index', + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Included providers', - items: [ - 'auth/auth0/provider', - 'auth/atlassian/provider', - 'auth/aws-alb/provider', - 'auth/microsoft/provider', - 'auth/microsoft/easy-auth', - 'auth/bitbucket/provider', - 'auth/bitbucketServer/provider', - 'auth/cloudflare/provider', - 'auth/github/provider', - 'auth/gitlab/provider', - 'auth/google/provider', - 'auth/google/gcp-iap-auth', - 'auth/guest/provider', - 'auth/okta/provider', - 'auth/oauth2-proxy/provider', - 'auth/onelogin/provider', - 'auth/vmware-cloud/provider', - ], + label: 'TechDocs', + description: 'Documentation as code for your software.', }, - 'auth/identity-resolver', - 'auth/oauth', - 'auth/oidc', - 'auth/add-auth-provider', - 'auth/service-to-service-auth', - 'auth/autologout', - 'auth/troubleshooting', - ], - }, + [ + 'features/techdocs/techdocs-overview', + 'features/techdocs/getting-started', + 'features/techdocs/concepts', + 'features/techdocs/addons', + 'features/techdocs/architecture', + 'features/techdocs/extensions', + 'features/techdocs/creating-and-publishing', + 'features/techdocs/configuration', + 'features/techdocs/using-cloud-storage', + 'features/techdocs/configuring-ci-cd', + 'features/techdocs/cli', + 'features/techdocs/how-to-guides', + 'features/techdocs/troubleshooting', + 'features/techdocs/faqs', + ], + ), + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Kubernetes', - items: [ - 'features/kubernetes/overview', - 'features/kubernetes/installation', - 'features/kubernetes/configuration', - 'features/kubernetes/authentication', - 'features/kubernetes/authentication-strategies', - 'features/kubernetes/troubleshooting', - 'features/kubernetes/proxy', - ], + label: 'Integrations', + description: 'Connect and integrate with external services.', }, - { - type: 'category', - label: 'Notifications', - items: [ - 'notifications/index', - 'notifications/processors', - 'notifications/usage', - ], - }, - { - type: 'category', - label: 'Permissions', - items: [ - 'permissions/overview', - 'permissions/concepts', - 'permissions/getting-started', - 'permissions/writing-a-policy', - 'permissions/frontend-integration', - 'permissions/custom-rules', - { - type: 'category', - label: 'Tutorial: using Permissions in your plugin', - items: [ - 'permissions/plugin-authors/01-setup', - 'permissions/plugin-authors/02-adding-a-basic-permission-check', - 'permissions/plugin-authors/03-adding-a-resource-permission-check', - 'permissions/plugin-authors/04-authorizing-access-to-paginated-data', - 'permissions/plugin-authors/05-frontend-authorization', - ], - }, - ], - }, - { - type: 'category', - label: 'Search', - items: [ - 'features/search/search-overview', - 'features/search/getting-started', - 'features/search/concepts', - { - type: 'category', - label: 'API', - link: - searchSidebar.length > 0 - ? { - type: 'generated-index', - title: 'Search API', - slug: '/category/search-api', - } - : { - type: 'doc', - id: 'openapi/generated-docs/404', - }, - items: searchSidebar, - }, - 'features/search/architecture', - 'features/search/search-engines', - 'features/search/collators', - 'features/search/how-to-guides', - ], - }, - { - type: 'category', - label: 'Software Catalog', - items: [ - 'features/software-catalog/software-catalog-overview', - 'features/software-catalog/life-of-an-entity', - 'features/software-catalog/configuration', - 'features/software-catalog/system-model', - 'features/software-catalog/descriptor-format', - 'features/software-catalog/references', - 'features/software-catalog/well-known-annotations', - 'features/software-catalog/well-known-relations', - 'features/software-catalog/well-known-statuses', - 'features/software-catalog/extending-the-model', - 'features/software-catalog/external-integrations', - 'features/software-catalog/catalog-customization', - { - type: 'category', - label: 'API', - link: - catalogSidebar.length > 0 - ? { - type: 'generated-index', - title: 'Catalog API', - slug: '/category/catalog-api', - } - : { - type: 'doc', - id: 'openapi/generated-docs/404', - }, - items: catalogSidebar, - }, - 'features/software-catalog/creating-the-catalog-graph', - 'features/software-catalog/faq', - ], - }, - { - type: 'category', - label: 'Software Templates', - items: [ - 'features/software-templates/software-templates-index', - 'features/software-templates/configuration', - 'features/software-templates/adding-templates', - 'features/software-templates/writing-templates', - 'features/software-templates/input-examples', - 'features/software-templates/builtin-actions', - 'features/software-templates/writing-custom-actions', - 'features/software-templates/writing-tests-for-actions', - 'features/software-templates/writing-custom-field-extensions', - 'features/software-templates/writing-custom-step-layouts', - 'features/software-templates/authorizing-scaffolder-template-details', - 'features/software-templates/migrating-to-rjsf-v5', - 'features/software-templates/migrating-from-v1beta2-to-v1beta3', - 'features/software-templates/dry-run-testing', - 'features/software-templates/experimental', - 'features/software-templates/templating-extensions', - { - type: 'category', - label: 'API', - link: - scaffolderSidebar.length > 0 - ? { - type: 'generated-index', - title: 'Scaffolder API', - slug: '/category/scaffolder-api', - } - : { - type: 'doc', - id: 'openapi/generated-docs/404', - }, - items: scaffolderSidebar, - }, - ], - }, - { - type: 'category', - label: 'TechDocs', - items: [ - 'features/techdocs/techdocs-overview', - 'features/techdocs/getting-started', - 'features/techdocs/concepts', - 'features/techdocs/addons', - 'features/techdocs/architecture', - 'features/techdocs/extensions', - 'features/techdocs/creating-and-publishing', - 'features/techdocs/configuration', - 'features/techdocs/using-cloud-storage', - 'features/techdocs/configuring-ci-cd', - 'features/techdocs/cli', - 'features/techdocs/how-to-guides', - 'features/techdocs/troubleshooting', - 'features/techdocs/faqs', - ], - }, - ], - Integrations: [ - 'integrations/index', - { - type: 'category', - label: 'AWS S3', - items: [ + [ + 'integrations/index', + sidebarElementWithIndex({ label: 'AWS S3' }, [ 'integrations/aws-s3/locations', 'integrations/aws-s3/discovery', - ], - }, - { - type: 'category', - label: 'Azure Blob Storage', - items: [ + ]), + sidebarElementWithIndex({ label: 'Azure Blob Storage' }, [ 'integrations/azure-blobStorage/locations', 'integrations/azure-blobStorage/discovery', - ], - }, - { - type: 'category', - label: 'Azure', - items: [ + ]), + sidebarElementWithIndex({ label: 'Azure' }, [ 'integrations/azure/locations', 'integrations/azure/discovery', 'integrations/azure/org', - ], - }, - { - type: 'category', - label: 'Bitbucket Cloud', - items: [ + ]), + sidebarElementWithIndex({ label: 'Bitbucket Cloud' }, [ 'integrations/bitbucketCloud/locations', 'integrations/bitbucketCloud/discovery', - ], - }, - { - type: 'category', - label: 'Bitbucket Server', - items: [ + ]), + sidebarElementWithIndex({ label: 'Bitbucket Server' }, [ 'integrations/bitbucketServer/locations', 'integrations/bitbucketServer/discovery', - ], - }, - { - type: 'category', - label: 'Datadog', - items: ['integrations/datadog-rum/installation'], - }, - { - type: 'category', - label: 'Gerrit', - items: [ + ]), + sidebarElementWithIndex({ label: 'Datadog' }, [ + 'integrations/datadog-rum/installation', + ]), + sidebarElementWithIndex({ label: 'Gerrit' }, [ 'integrations/gerrit/locations', 'integrations/gerrit/discovery', - ], - }, - { - type: 'category', - label: 'GitHub', - items: [ + ]), + sidebarElementWithIndex({ label: 'Github' }, [ 'integrations/github/locations', 'integrations/github/discovery', 'integrations/github/org', 'integrations/github/github-apps', - ], - }, - { - type: 'category', - label: 'GitLab', - items: [ + ]), + sidebarElementWithIndex({ label: 'GitLab' }, [ 'integrations/gitlab/locations', 'integrations/gitlab/discovery', 'integrations/gitlab/org', - ], - }, + ]), + sidebarElementWithIndex({ label: 'Gitea' }, [ + 'integrations/gitea/locations', + 'integrations/gitea/discovery', + ]), + sidebarElementWithIndex({ label: 'Harness' }, [ + 'integrations/harness/locations', + ]), + sidebarElementWithIndex({ label: 'Google GCS' }, [ + 'integrations/google-cloud-storage/locations', + ]), + sidebarElementWithIndex({ label: 'LDAP' }, ['integrations/ldap/org']), + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Gitea', - items: ['integrations/gitea/locations', 'integrations/gitea/discovery'], + label: 'Plugins', + description: 'Extend Backstage with custom functionality.', }, - { - type: 'category', - label: 'Harness', - items: ['integrations/harness/locations'], - }, - { - type: 'category', - label: 'Google GCS', - items: ['integrations/google-cloud-storage/locations'], - }, - { - type: 'category', - label: 'LDAP', - items: ['integrations/ldap/org'], - }, - ], - Plugins: [ - 'plugins/index', - 'plugins/existing-plugins', - 'plugins/create-a-plugin', - 'plugins/plugin-development', - 'plugins/structure-of-a-plugin', - 'plugins/integrating-plugin-into-software-catalog', - 'plugins/integrating-search-into-plugins', - 'plugins/composability', - 'plugins/internationalization', - 'plugins/analytics', - 'plugins/feature-flags', - { - type: 'category', - label: 'OpenAPI', - items: [ - 'openapi/01-getting-started', - 'openapi/generate-client', - 'openapi/test-case-validation', - ], - }, - { - type: 'category', - label: 'Backends and APIs', - items: [ - 'plugins/proxying', - 'plugins/backend-plugin', - 'plugins/call-existing-api', - ], - }, - { - type: 'category', - label: 'Testing', - items: ['plugins/testing'], - }, - { - type: 'category', - label: 'Publishing', - items: [ - 'plugins/publish-private', - 'plugins/add-to-directory', - 'plugins/observability', - ], - }, - ], - Configuration: [ - 'conf/index', - 'conf/reading', - 'conf/writing', - 'conf/defining', - ], - Framework: [ - { - type: 'category', - label: 'Backend System', - items: [ - 'backend-system/index', + [ + 'plugins/index', + 'plugins/create-a-plugin', + 'plugins/plugin-development', + 'plugins/structure-of-a-plugin', + 'plugins/integrating-plugin-into-software-catalog', + 'plugins/integrating-search-into-plugins', + 'plugins/composability', + 'plugins/internationalization', + 'plugins/analytics', + 'plugins/feature-flags', + sidebarElementWithIndex( { - 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/feature-loaders', - 'backend-system/architecture/naming-patterns', - ], + label: 'OpenAPI', + description: + 'Work with OpenAPI specifications and generate clients.', }, + [ + 'openapi/01-getting-started', + 'openapi/generate-client', + 'openapi/test-case-validation', + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Building Backends', - items: [ - 'backend-system/building-backends/index', - 'backend-system/building-backends/migrating', - ], + label: 'Backends and APIs', + description: 'Build and manage backend services and APIs.', }, - { - 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', - 'backend-system/core-services/auditor', - 'backend-system/core-services/auth', - 'backend-system/core-services/cache', - 'backend-system/core-services/database', - 'backend-system/core-services/discovery', - 'backend-system/core-services/http-auth', - 'backend-system/core-services/http-router', - 'backend-system/core-services/identity', - 'backend-system/core-services/lifecycle', - 'backend-system/core-services/logger', - 'backend-system/core-services/permissions', - 'backend-system/core-services/permissions-registry', - 'backend-system/core-services/plugin-metadata', - 'backend-system/core-services/root-config', - 'backend-system/core-services/root-health', - 'backend-system/core-services/root-http-router', - 'backend-system/core-services/root-lifecycle', - 'backend-system/core-services/root-logger', - 'backend-system/core-services/scheduler', - 'backend-system/core-services/token-manager', - 'backend-system/core-services/url-reader', - 'backend-system/core-services/user-info', - 'backend-system/core-services/actions-registry', - 'backend-system/core-services/actions', - ], - }, - ], - }, + [ + 'plugins/proxying', + 'plugins/backend-plugin', + 'plugins/call-existing-api', + ], + ), + sidebarElementWithIndex( + { label: 'Testing', description: 'Testing plugins and modules.' }, + ['plugins/testing'], + ), + sidebarElementWithIndex( + { label: 'Publishing', description: 'Publishing your plugins.' }, + ['plugins/publish-private', 'plugins/add-to-directory'], + ), + 'plugins/observability', + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'New Frontend System', - items: [ - 'frontend-system/index', + label: 'Configuration', + description: 'Manage static configuration files and settings.', + }, + ['conf/index', 'conf/reading', 'conf/writing', 'conf/defining'], + ), + sidebarElementWithIndex( + { + label: 'Framework', + description: 'Core framework concepts and architecture.', + }, + [ + sidebarElementWithIndex( { - type: 'category', - label: 'Architecture', - items: [ - 'frontend-system/architecture/index', - 'frontend-system/architecture/app', - 'frontend-system/architecture/plugins', - 'frontend-system/architecture/extensions', - 'frontend-system/architecture/extension-blueprints', - 'frontend-system/architecture/extension-overrides', - 'frontend-system/architecture/references', - 'frontend-system/architecture/utility-apis', - 'frontend-system/architecture/routes', - 'frontend-system/architecture/naming-patterns', - 'frontend-system/architecture/migrations', - ], + label: 'Backend System', + description: 'Backend system components and architecture.', }, + [ + 'backend-system/index', + sidebarElementWithIndex( + { + label: 'Architecture', + description: 'Architecture of the backend system.', + }, + [ + 'backend-system/architecture/index', + 'backend-system/architecture/services', + 'backend-system/architecture/plugins', + 'backend-system/architecture/extension-points', + 'backend-system/architecture/modules', + 'backend-system/architecture/feature-loaders', + 'backend-system/architecture/naming-patterns', + ], + ), + sidebarElementWithIndex( + { + label: 'Building Backends', + description: 'Guides on building backend systems.', + }, + [ + 'backend-system/building-backends/index', + 'backend-system/building-backends/migrating', + ], + ), + sidebarElementWithIndex( + { + label: 'Building Plugins & Modules', + description: 'Guides on building plugins and modules.', + }, + [ + 'backend-system/building-plugins-and-modules/index', + 'backend-system/building-plugins-and-modules/testing', + 'backend-system/building-plugins-and-modules/migrating', + ], + ), + sidebarElementWithIndex( + { + label: 'Core Services', + description: 'Core services of the backend system.', + }, + [ + 'backend-system/core-services/index', + 'backend-system/core-services/auditor', + 'backend-system/core-services/auth', + 'backend-system/core-services/cache', + 'backend-system/core-services/database', + 'backend-system/core-services/discovery', + 'backend-system/core-services/http-auth', + 'backend-system/core-services/http-router', + 'backend-system/core-services/identity', + 'backend-system/core-services/lifecycle', + 'backend-system/core-services/logger', + 'backend-system/core-services/permissions', + 'backend-system/core-services/permissions-registry', + 'backend-system/core-services/plugin-metadata', + 'backend-system/core-services/root-config', + 'backend-system/core-services/root-health', + 'backend-system/core-services/root-http-router', + 'backend-system/core-services/root-instance-metadata', + 'backend-system/core-services/root-lifecycle', + 'backend-system/core-services/root-logger', + 'backend-system/core-services/scheduler', + 'backend-system/core-services/token-manager', + 'backend-system/core-services/url-reader', + 'backend-system/core-services/user-info', + 'backend-system/core-services/actions-registry', + 'backend-system/core-services/actions', + ], + ), + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Building Plugins', - items: [ - 'frontend-system/building-plugins/index', - 'frontend-system/building-plugins/testing', - 'frontend-system/building-plugins/common-extension-blueprints', - 'frontend-system/building-plugins/built-in-data-refs', - 'frontend-system/building-plugins/migrating', - ], + label: 'New Frontend System', + description: 'New frontend system components and architecture.', }, + [ + 'frontend-system/index', + sidebarElementWithIndex( + { + label: 'Architecture', + description: 'Architecture of the new frontend system.', + }, + [ + 'frontend-system/architecture/index', + 'frontend-system/architecture/app', + 'frontend-system/architecture/plugins', + 'frontend-system/architecture/extensions', + 'frontend-system/architecture/extension-blueprints', + 'frontend-system/architecture/extension-overrides', + 'frontend-system/architecture/references', + 'frontend-system/architecture/utility-apis', + 'frontend-system/architecture/routes', + 'frontend-system/architecture/naming-patterns', + 'frontend-system/architecture/migrations', + ], + ), + sidebarElementWithIndex( + { + label: 'Building Plugins', + description: 'Guides on building frontend plugins.', + }, + [ + 'frontend-system/building-plugins/index', + 'frontend-system/building-plugins/testing', + 'frontend-system/building-plugins/common-extension-blueprints', + 'frontend-system/building-plugins/built-in-data-refs', + 'frontend-system/building-plugins/migrating', + ], + ), + sidebarElementWithIndex( + { + label: 'Building Apps', + description: 'Guides on building frontend applications.', + }, + [ + 'frontend-system/building-apps/index', + 'frontend-system/building-apps/configuring-extensions', + 'frontend-system/building-apps/built-in-extensions', + 'frontend-system/building-apps/plugin-conversion', + 'frontend-system/building-apps/migrating', + ], + ), + sidebarElementWithIndex( + { + label: 'Utility APIs', + description: 'Utility APIs for the frontend system.', + }, + [ + 'frontend-system/utility-apis/index', + 'frontend-system/utility-apis/creating', + 'frontend-system/utility-apis/consuming', + 'frontend-system/utility-apis/configuring', + ], + ), + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Building Apps', - items: [ - 'frontend-system/building-apps/index', - 'frontend-system/building-apps/configuring-extensions', - 'frontend-system/building-apps/built-in-extensions', - 'frontend-system/building-apps/plugin-conversion', - 'frontend-system/building-apps/migrating', - ], + label: 'Backstage CLI', + description: 'Command-line interface for Backstage.', }, + [ + 'tooling/cli/overview', + 'tooling/cli/build-system', + 'tooling/cli/commands', + 'tooling/cli/templates', + sidebarElementWithIndex( + { + label: 'Local Development', + description: + 'Guides for local development using Backstage CLI.', + }, + [ + 'tooling/local-dev/linking-local-packages', + 'tooling/local-dev/debugging', + 'tooling/local-dev/profiling', + ], + ), + 'tooling/package-metadata', + ], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'Utility APIs', - items: [ - 'frontend-system/utility-apis/index', - 'frontend-system/utility-apis/creating', - 'frontend-system/utility-apis/consuming', - 'frontend-system/utility-apis/configuring', - ], + label: 'User Interface', + description: 'User interface customization and configuration.', }, - ], - }, + [ + 'conf/user-interface/index', + 'conf/user-interface/logo', + 'conf/user-interface/icons', + 'conf/user-interface/sidebar', + ], + ), + ], + ), + sidebarElementWithIndex( { - 'Backstage CLI': [ - 'tooling/cli/overview', - 'tooling/cli/build-system', - 'tooling/cli/commands', - 'tooling/cli/templates', + label: 'Tutorials', + description: + 'Step-by-step tutorials for various Backstage features and use cases.', + }, + [ + sidebarElementWithIndex( { - type: 'category', - label: 'Local Development', - items: [ - 'tooling/local-dev/linking-local-packages', - 'tooling/local-dev/debugging', - 'tooling/local-dev/profiling', - ], + label: 'Non-technical', + description: 'Non-technical tutorials and guides.', }, - 'tooling/package-metadata', - ], - }, + ['overview/adopting'], + ), + sidebarElementWithIndex( + { + label: 'Technical', + description: 'Technical tutorials and guides.', + }, + [ + 'tutorials/quickstart-app-plugin', + 'tutorials/configuring-plugin-databases', + 'tutorials/manual-knex-rollback', + 'tutorials/switching-sqlite-postgres', + 'tutorials/using-backstage-proxy-within-plugin', + 'tutorials/enable-public-entry', + 'tutorials/setup-opentelemetry', + 'tutorials/integrating-event-driven-updates-with-entity-providers', + 'accessibility/index', + ], + ), + sidebarElementWithIndex( + { + label: 'Migrations', + description: + 'Guides for migrating between different versions and systems.', + }, + [ + 'tutorials/jest30-migration', + 'tutorials/react-router-stable-migration', + 'tutorials/react18-migration', + 'tutorials/package-role-migration', + 'tutorials/migrating-away-from-core', + 'tutorials/yarn-migration', + 'tutorials/migrate-to-mui5', + 'tutorials/auth-service-migration', + 'tutorials/jsx-transform-migration', + ], + ), + ], + ), + sidebarElementWithIndex( + { label: 'FAQ', description: 'Frequently asked questions and answers.' }, + ['faq/index', 'faq/product', 'faq/technical'], + ), + sidebarElementWithIndex( { - type: 'category', - label: 'User Interface', - items: [ - 'conf/user-interface/index', - 'conf/user-interface/logo', - 'conf/user-interface/icons', - 'conf/user-interface/sidebar', - ], + label: 'Contribute', + description: 'Information on contributing to Backstage.', }, - ], - Tutorials: [ - { 'Non-technical': ['overview/adopting'] }, + [ + 'contribute/index', + 'contribute/getting-involved', + 'contribute/project-structure', + ], + ), + sidebarElementWithIndex( { - Technical: [ - 'tutorials/quickstart-app-plugin', - 'tutorials/configuring-plugin-databases', - 'tutorials/manual-knex-rollback', - 'tutorials/switching-sqlite-postgres', - 'tutorials/using-backstage-proxy-within-plugin', - 'tutorials/enable-public-entry', - 'tutorials/setup-opentelemetry', - 'tutorials/integrating-event-driven-updates-with-entity-providers', - 'accessibility/index', - ], + label: 'References', + description: 'Reference materials and documentation.', }, - { - Migrations: [ - 'tutorials/react-router-stable-migration', - 'tutorials/react18-migration', - 'tutorials/package-role-migration', - 'tutorials/migrating-away-from-core', - 'tutorials/yarn-migration', - 'tutorials/migrate-to-mui5', - 'tutorials/auth-service-migration', - 'tutorials/jsx-transform-migration', - ], - }, - ], - FAQ: ['faq/index', 'faq/product', 'faq/technical'], - Contribute: [ - 'contribute/index', - 'contribute/getting-involved', - 'contribute/project-structure', - ], - References: [ - { - 'Designing for Backstage': [ - 'dls/design', - 'dls/component-design-guidelines', - 'dls/contributing-to-storybook', - 'dls/figma', - ], - }, - { - type: 'category', - label: 'Architecture Decision Records (ADRs)', - items: [ - 'architecture-decisions/adrs-overview', - 'architecture-decisions/adrs-adr001', - 'architecture-decisions/adrs-adr002', - 'architecture-decisions/adrs-adr003', - 'architecture-decisions/adrs-adr004', - 'architecture-decisions/adrs-adr005', - 'architecture-decisions/adrs-adr006', - 'architecture-decisions/adrs-adr007', - 'architecture-decisions/adrs-adr008', - 'architecture-decisions/adrs-adr009', - 'architecture-decisions/adrs-adr010', - 'architecture-decisions/adrs-adr011', - 'architecture-decisions/adrs-adr012', - 'architecture-decisions/adrs-adr013', - 'architecture-decisions/adrs-adr014', - ], - }, - 'api/deprecations', - 'references/glossary', - 'api/utility-apis', - 'reference/index', - ], - }, + [ + sidebarElementWithIndex( + { + label: 'Designing for Backstage', + description: + 'Guidelines and resources for designing Backstage components.', + }, + [ + 'dls/design', + 'dls/component-design-guidelines', + 'dls/contributing-to-storybook', + 'dls/figma', + ], + ), + sidebarElementWithIndex( + { + label: 'Architecture Decision Records (ADRs)', + description: + 'Documentation of architectural decisions made in Backstage.', + }, + [ + 'architecture-decisions/adrs-overview', + 'architecture-decisions/adrs-adr001', + 'architecture-decisions/adrs-adr002', + 'architecture-decisions/adrs-adr003', + 'architecture-decisions/adrs-adr004', + 'architecture-decisions/adrs-adr005', + 'architecture-decisions/adrs-adr006', + 'architecture-decisions/adrs-adr007', + 'architecture-decisions/adrs-adr008', + 'architecture-decisions/adrs-adr009', + 'architecture-decisions/adrs-adr010', + 'architecture-decisions/adrs-adr011', + 'architecture-decisions/adrs-adr012', + 'architecture-decisions/adrs-adr013', + 'architecture-decisions/adrs-adr014', + 'architecture-decisions/adrs-adr015', + ], + ), + 'api/deprecations', + 'references/glossary', + 'api/utility-apis', + 'reference/index', + ], + ), + ], releases: { 'Release Notes': releases.map(release => `releases/${release}`), }, diff --git a/microsite/static/img/citrix_sds.png b/microsite/static/img/citrix_sds.png new file mode 100644 index 0000000000..fac58411a1 Binary files /dev/null and b/microsite/static/img/citrix_sds.png differ diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg index c5e3b2af9a..652249b8d8 100644 --- a/microsite/static/img/cncf-white.svg +++ b/microsite/static/img/cncf-white.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 9040e75d86..2af94aec1f 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -2460,14 +2460,14 @@ __metadata: linkType: hard "@mdx-js/react@npm:^3.0.0": - version: 3.1.0 - resolution: "@mdx-js/react@npm:3.1.0" + version: 3.1.1 + resolution: "@mdx-js/react@npm:3.1.1" dependencies: "@types/mdx": "npm:^2.0.0" peerDependencies: "@types/react": ">=16" react: ">=16" - checksum: 10/cf89d6392c76091622fb647f205e1ab5cbdf5edd4401dde7092138cefc9fbb6d61428aa63557de0bccca3695d5a8854dd4a93b34a27cb8e27369da7eaeaa3e73 + checksum: 10/52a740e2f37761694fa94d4704b7825084b4055616a95c8b8f4c1676190d399ddc5cdbb399ffc45b550beecd30497a7224c2e5b05bf43ecb668c7473641037d1 languageName: node linkType: hard @@ -2961,92 +2961,92 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-darwin-arm64@npm:1.11.24" +"@swc/core-darwin-arm64@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-darwin-arm64@npm:1.15.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-darwin-x64@npm:1.11.24" +"@swc/core-darwin-x64@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-darwin-x64@npm:1.15.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.11.24" +"@swc/core-linux-arm-gnueabihf@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-linux-arm64-gnu@npm:1.11.24" +"@swc/core-linux-arm64-gnu@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.6" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-linux-arm64-musl@npm:1.11.24" +"@swc/core-linux-arm64-musl@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.6" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-linux-x64-gnu@npm:1.11.24" +"@swc/core-linux-x64-gnu@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.6" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-linux-x64-musl@npm:1.11.24" +"@swc/core-linux-x64-musl@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-linux-x64-musl@npm:1.15.6" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-win32-arm64-msvc@npm:1.11.24" +"@swc/core-win32-arm64-msvc@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-win32-ia32-msvc@npm:1.11.24" +"@swc/core-win32-ia32-msvc@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.11.24": - version: 1.11.24 - resolution: "@swc/core-win32-x64-msvc@npm:1.11.24" +"@swc/core-win32-x64-msvc@npm:1.15.6": + version: 1.15.6 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:^1.3.46": - version: 1.11.24 - resolution: "@swc/core@npm:1.11.24" +"@swc/core@npm:^1.15.6": + version: 1.15.6 + resolution: "@swc/core@npm:1.15.6" dependencies: - "@swc/core-darwin-arm64": "npm:1.11.24" - "@swc/core-darwin-x64": "npm:1.11.24" - "@swc/core-linux-arm-gnueabihf": "npm:1.11.24" - "@swc/core-linux-arm64-gnu": "npm:1.11.24" - "@swc/core-linux-arm64-musl": "npm:1.11.24" - "@swc/core-linux-x64-gnu": "npm:1.11.24" - "@swc/core-linux-x64-musl": "npm:1.11.24" - "@swc/core-win32-arm64-msvc": "npm:1.11.24" - "@swc/core-win32-ia32-msvc": "npm:1.11.24" - "@swc/core-win32-x64-msvc": "npm:1.11.24" + "@swc/core-darwin-arm64": "npm:1.15.6" + "@swc/core-darwin-x64": "npm:1.15.6" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.6" + "@swc/core-linux-arm64-gnu": "npm:1.15.6" + "@swc/core-linux-arm64-musl": "npm:1.15.6" + "@swc/core-linux-x64-gnu": "npm:1.15.6" + "@swc/core-linux-x64-musl": "npm:1.15.6" + "@swc/core-win32-arm64-msvc": "npm:1.15.6" + "@swc/core-win32-ia32-msvc": "npm:1.15.6" + "@swc/core-win32-x64-msvc": "npm:1.15.6" "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.21" + "@swc/types": "npm:^0.1.25" peerDependencies: "@swc/helpers": ">=0.5.17" dependenciesMeta: @@ -3073,7 +3073,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/0b3e883f8a5652a7ab221a777386ccc8a65fc5b53d533bad15b703b22984eb3b449efd907b1872263f1a9990a9a50612f3c6deb619894a43f03cd974ec9bd1b7 + checksum: 10/d17d1b5e6e81f8c262b668f237b863dd612eaa19fc35e74067b2a27ba3998c0c511ec7f49cd91c02f61fa77dd1b5d7e65d293736f65ec45d6c13989904b1c7eb languageName: node linkType: hard @@ -3084,12 +3084,12 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.21": - version: 0.1.21 - resolution: "@swc/types@npm:0.1.21" +"@swc/types@npm:^0.1.25": + version: 0.1.25 + resolution: "@swc/types@npm:0.1.25" dependencies: "@swc/counter": "npm:^0.1.3" - checksum: 10/6554bf5c78519f49099a2ba448d170191a14b1c7a35df848f10ee4d6c03ecd681e5213884905187de1d1d221589ec8b5cb77f477d099dc1627c3ec9d7f2fcdb0 + checksum: 10/f6741450224892d12df43e5ca7f3cc0287df644dcd672626eb0cc2a3a8e3e875f4b29eb11336f37c7240cf6e010ba59eb3a79f4fb8bee5cbd168dfc1326ff369 languageName: node linkType: hard @@ -3328,13 +3328,6 @@ __metadata: languageName: node linkType: hard -"@types/luxon@npm:^3.0.0": - version: 3.4.2 - resolution: "@types/luxon@npm:3.4.2" - checksum: 10/fd89566e3026559f2bc4ddcc1e70a2c16161905ed50be9473ec0cfbbbe919165041408c4f6e06c4bcf095445535052e2c099087c76b1b38e368127e618fc968d - languageName: node - linkType: hard - "@types/mdast@npm:^3.0.0": version: 3.0.15 resolution: "@types/mdast@npm:3.0.15" @@ -4281,15 +4274,14 @@ __metadata: "@docusaurus/tsconfig": "npm:^3.1.1" "@docusaurus/types": "npm:^3.1.1" "@mdx-js/react": "npm:^3.0.0" - "@swc/core": "npm:^1.3.46" - "@types/luxon": "npm:^3.0.0" + "@swc/core": "npm:^1.15.6" "@types/webpack-env": "npm:^1.18.0" clsx: "npm:^2.0.0" docusaurus-plugin-openapi-docs: "npm:^4.3.0" docusaurus-plugin-sass: "npm:^0.2.3" docusaurus-pushfeedback: "npm:^1.0.0" docusaurus-theme-openapi-docs: "npm:^4.3.0" - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" luxon: "npm:^3.0.0" prettier: "npm:^2.6.2" prism-react-renderer: "npm:^2.1.0" @@ -4297,7 +4289,7 @@ __metadata: react-dom: "npm:^18.0.2" sass: "npm:^1.57.1" swc-loader: "npm:^0.2.3" - typescript: "npm:~5.2.0" + typescript: "npm:~5.7.0" yaml-loader: "npm:^0.8.0" languageName: unknown linkType: soft @@ -4358,23 +4350,23 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.3": - version: 1.20.3 - resolution: "body-parser@npm:1.20.3" +"body-parser@npm:~1.20.3": + version: 1.20.4 + resolution: "body-parser@npm:1.20.4" dependencies: - bytes: "npm:3.1.2" + bytes: "npm:~3.1.2" content-type: "npm:~1.0.5" debug: "npm:2.6.9" depd: "npm:2.0.0" - destroy: "npm:1.2.0" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.4.24" - on-finished: "npm:2.4.1" - qs: "npm:6.13.0" - raw-body: "npm:2.5.2" + destroy: "npm:~1.2.0" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.4.24" + on-finished: "npm:~2.4.1" + qs: "npm:~6.14.0" + raw-body: "npm:~2.5.3" type-is: "npm:~1.6.18" - unpipe: "npm:1.0.0" - checksum: 10/8723e3d7a672eb50854327453bed85ac48d045f4958e81e7d470c56bf111f835b97e5b73ae9f6393d0011cc9e252771f46fd281bbabc57d33d3986edf1e6aeca + unpipe: "npm:~1.0.0" + checksum: 10/ff67e28d3f426707be8697a75fdf8d564dc50c341b41f054264d8ab6e2924e519c7ce8acc9d0de05328fdc41e1d9f3f200aec9c1cfb1867d6b676a410d97c689 languageName: node linkType: hard @@ -4591,7 +4583,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2": +"bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388 @@ -4729,9 +4721,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001695 - resolution: "caniuse-lite@npm:1.0.30001695" - checksum: 10/8107c5e89b86c7a2fd506b93c658ff945c98c6518260c3b28af9f02bd83bf83939696241f0b413545c5b9895c86bcae64c9370388576440e74e9b848f04170d3 + version: 1.0.30001760 + resolution: "caniuse-lite@npm:1.0.30001760" + checksum: 10/ac5c13d00b946c3ace331d25379ed9d85743b1028aba79ae80402cb128b4b491122585144898fefb836d4d9879c13c717a081ae241a00420cbbc7e1b934ec685 languageName: node linkType: hard @@ -5223,7 +5215,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4": +"content-disposition@npm:~0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -5246,17 +5238,17 @@ __metadata: languageName: node linkType: hard -"cookie-signature@npm:1.0.6": - version: 1.0.6 - resolution: "cookie-signature@npm:1.0.6" - checksum: 10/f4e1b0a98a27a0e6e66fd7ea4e4e9d8e038f624058371bf4499cfcd8f3980be9a121486995202ba3fca74fbed93a407d6d54d43a43f96fd28d0bd7a06761591a +"cookie-signature@npm:~1.0.6": + version: 1.0.7 + resolution: "cookie-signature@npm:1.0.7" + checksum: 10/1a62808cd30d15fb43b70e19829b64d04b0802d8ef00275b57d152de4ae6a3208ca05c197b6668d104c4d9de389e53ccc2d3bc6bcaaffd9602461417d8c40710 languageName: node linkType: hard -"cookie@npm:0.6.0": - version: 0.6.0 - resolution: "cookie@npm:0.6.0" - checksum: 10/c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583 +"cookie@npm:~0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f languageName: node linkType: hard @@ -5772,7 +5764,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0": +"depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10/c0c8ff36079ce5ada64f46cc9d6fd47ebcf38241105b6e0c98f412e8ad91f084bcf906ff644cc3a4bd876ca27a62accb8b0fff72ea6ed1a414b89d8506f4a5ca @@ -5803,7 +5795,7 @@ __metadata: languageName: node linkType: hard -"destroy@npm:1.2.0": +"destroy@npm:1.2.0, destroy@npm:~1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 @@ -5914,8 +5906,8 @@ __metadata: linkType: hard "docusaurus-plugin-openapi-docs@npm:^4.3.0": - version: 4.3.7 - resolution: "docusaurus-plugin-openapi-docs@npm:4.3.7" + version: 4.5.1 + resolution: "docusaurus-plugin-openapi-docs@npm:4.5.1" dependencies: "@apidevtools/json-schema-ref-parser": "npm:^11.5.4" "@redocly/openapi-core": "npm:^1.10.5" @@ -5937,7 +5929,7 @@ __metadata: "@docusaurus/utils": ^3.5.0 "@docusaurus/utils-validation": ^3.5.0 react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/ad011358b13fd5e574e33ad3e1b2bb002226e2dcab72e6a8c98c4ad5436a81e28815afe6f14252a48bb52f0c088b03dfd93a5955a8a6883fbda794bebf7d1792 + checksum: 10/496ce49d494f06803a61eaa6c6810782668269574c12a1a55315742974884a7ffd6e2c75875d8fd9909611bdccaa4de42419e0debbf47def0db6974617b9e5ba languageName: node linkType: hard @@ -6541,41 +6533,41 @@ __metadata: linkType: hard "express@npm:^4.17.3": - version: 4.21.0 - resolution: "express@npm:4.21.0" + version: 4.22.1 + resolution: "express@npm:4.22.1" dependencies: accepts: "npm:~1.3.8" array-flatten: "npm:1.1.1" - body-parser: "npm:1.20.3" - content-disposition: "npm:0.5.4" + body-parser: "npm:~1.20.3" + content-disposition: "npm:~0.5.4" content-type: "npm:~1.0.4" - cookie: "npm:0.6.0" - cookie-signature: "npm:1.0.6" + cookie: "npm:~0.7.1" + cookie-signature: "npm:~1.0.6" debug: "npm:2.6.9" depd: "npm:2.0.0" encodeurl: "npm:~2.0.0" escape-html: "npm:~1.0.3" etag: "npm:~1.8.1" - finalhandler: "npm:1.3.1" - fresh: "npm:0.5.2" - http-errors: "npm:2.0.0" + finalhandler: "npm:~1.3.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.0" merge-descriptors: "npm:1.0.3" methods: "npm:~1.1.2" - on-finished: "npm:2.4.1" + on-finished: "npm:~2.4.1" parseurl: "npm:~1.3.3" - path-to-regexp: "npm:0.1.10" + path-to-regexp: "npm:~0.1.12" proxy-addr: "npm:~2.0.7" - qs: "npm:6.13.0" + qs: "npm:~6.14.0" range-parser: "npm:~1.2.1" safe-buffer: "npm:5.2.1" - send: "npm:0.19.0" - serve-static: "npm:1.16.2" + send: "npm:~0.19.0" + serve-static: "npm:~1.16.2" setprototypeof: "npm:1.2.0" - statuses: "npm:2.0.1" + statuses: "npm:~2.0.1" type-is: "npm:~1.6.18" utils-merge: "npm:1.0.1" vary: "npm:~1.1.2" - checksum: 10/3b1ee5bc5b1bd996f688702519cebc9b63a24e506965f6e1773268238cfa2c24ffdb38cc3fcb4fde66f77de1c0bebd9ee058dad06bb9c6f084b525f3c09164d3 + checksum: 10/f33c1bd0c7d36e2a1f18de9cdc176469d32f68e20258d2941b8d296ab9a4fd9011872c246391bf87714f009fac5114c832ec5ac65cbee39421f1258801eb8470 languageName: node linkType: hard @@ -6723,18 +6715,18 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:1.3.1": - version: 1.3.1 - resolution: "finalhandler@npm:1.3.1" +"finalhandler@npm:~1.3.1": + version: 1.3.2 + resolution: "finalhandler@npm:1.3.2" dependencies: debug: "npm:2.6.9" encodeurl: "npm:~2.0.0" escape-html: "npm:~1.0.3" - on-finished: "npm:2.4.1" + on-finished: "npm:~2.4.1" parseurl: "npm:~1.3.3" - statuses: "npm:2.0.1" + statuses: "npm:~2.0.2" unpipe: "npm:~1.0.0" - checksum: 10/4babe72969b7373b5842bc9f75c3a641a4d0f8eb53af6b89fa714d4460ce03fb92b28de751d12ba415e96e7e02870c436d67412120555e2b382640535697305b + checksum: 10/6cb4f9f80eaeb5a0fac4fdbd27a65d39271f040a0034df16556d896bfd855fd42f09da886781b3102117ea8fceba97b903c1f8b08df1fb5740576d5e0f481eed languageName: node linkType: hard @@ -6872,7 +6864,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2": +"fresh@npm:0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 10/64c88e489b5d08e2f29664eb3c79c705ff9a8eb15d3e597198ef76546d4ade295897a44abb0abd2700e7ef784b2e3cbf1161e4fbf16f59129193fd1030d16da1 @@ -7751,6 +7743,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:~2.0.0, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10/9fe31bc0edf36566c87048aed1d3d0cbe03552564adc3541626a0613f542d753fbcb13bdfcec0a3a530dbe1714bb566c89d46244616b66bddd26ac413b06a207 + languageName: node + linkType: hard + "http-parser-js@npm:>=0.5.1": version: 0.5.8 resolution: "http-parser-js@npm:0.5.8" @@ -7865,15 +7870,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24": - version: 0.4.24 - resolution: "iconv-lite@npm:0.4.24" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3" - checksum: 10/6d3a2dac6e5d1fb126d25645c25c3a1209f70cceecc68b8ef51ae0da3cdc078c151fade7524a30b12a3094926336831fca09c666ef55b37e2c69638b5d6bd2e3 - languageName: node - linkType: hard - "iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -7883,6 +7879,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:~0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: 10/6d3a2dac6e5d1fb126d25645c25c3a1209f70cceecc68b8ef51ae0da3cdc078c151fade7524a30b12a3094926336831fca09c666ef55b37e2c69638b5d6bd2e3 + languageName: node + linkType: hard + "icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": version: 5.1.0 resolution: "icss-utils@npm:5.1.0" @@ -8508,7 +8513,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": +"js-yaml@npm:4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -8520,14 +8525,25 @@ __metadata: linkType: hard "js-yaml@npm:^3.13.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" dependencies: argparse: "npm:^1.0.7" esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 + checksum: 10/172e0b6007b0bf0fc8d2469c94424f7dd765c64a047d2b790831fecef2204a4054eabf4d911eb73ab8c9a3256ab8ba1ee8d655b789bf24bf059c772acc2075a1 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 languageName: node linkType: hard @@ -8893,9 +8909,9 @@ __metadata: linkType: hard "luxon@npm:^3.0.0": - version: 3.5.0 - resolution: "luxon@npm:3.5.0" - checksum: 10/48f86e6c1c96815139f8559456a3354a276ba79bcef0ae0d4f2172f7652f3ba2be2237b0e103b8ea0b79b47715354ac9fac04eb1db3485dcc72d5110491dd47f + version: 3.7.2 + resolution: "luxon@npm:3.7.2" + checksum: 10/b24cd205ed306ce7415991687897dcc4027921ae413c9116590bc33a95f93b86ce52cf74ba72b4f5c5ab1c10090517f54ac8edfb127c049e0bf55b90dc2260be languageName: node linkType: hard @@ -10620,9 +10636,9 @@ __metadata: linkType: hard "node-forge@npm:^1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d + version: 1.3.2 + resolution: "node-forge@npm:1.3.2" + checksum: 10/dcc54aaffe0cf52367214a20c0032aa9b209d9095dd14526504f1972d1900a07e96046b3684cb0c8d0cc3d48744dd18e02b7b447ab28fac615ffb850beeabf18 languageName: node linkType: hard @@ -10898,7 +10914,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:2.4.1": +"on-finished@npm:2.4.1, on-finished@npm:~2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -11276,13 +11292,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:0.1.10": - version: 0.1.10 - resolution: "path-to-regexp@npm:0.1.10" - checksum: 10/894e31f1b20e592732a87db61fff5b95c892a3fe430f9ab18455ebe69ee88ef86f8eb49912e261f9926fc53da9f93b46521523e33aefd9cb0a7b0d85d7096006 - languageName: node - linkType: hard - "path-to-regexp@npm:2.2.1": version: 2.2.1 resolution: "path-to-regexp@npm:2.2.1" @@ -11299,6 +11308,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:~0.1.12": + version: 0.1.12 + resolution: "path-to-regexp@npm:0.1.12" + checksum: 10/2e30f6a0144679c1f95c98e166b96e6acd1e72be9417830fefc8de7ac1992147eb9a4c7acaa59119fb1b3c34eec393b2129ef27e24b2054a3906fc4fb0d1398e + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -12038,21 +12054,12 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0": - version: 6.13.0 - resolution: "qs@npm:6.13.0" - dependencies: - side-channel: "npm:^1.0.6" - checksum: 10/f548b376e685553d12e461409f0d6e5c59ec7c7d76f308e2a888fd9db3e0c5e89902bedd0754db3a9038eda5f27da2331a6f019c8517dc5e0a16b3c9a6e9cef8 - languageName: node - linkType: hard - -"qs@npm:^6.12.3": - version: 6.14.0 - resolution: "qs@npm:6.14.0" +"qs@npm:^6.12.3, qs@npm:~6.14.0": + version: 6.14.1 + resolution: "qs@npm:6.14.1" dependencies: side-channel: "npm:^1.1.0" - checksum: 10/a60e49bbd51c935a8a4759e7505677b122e23bf392d6535b8fc31c1e447acba2c901235ecb192764013cd2781723dc1f61978b5fdd93cc31d7043d31cdc01974 + checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5 languageName: node linkType: hard @@ -12119,15 +12126,15 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.2": - version: 2.5.2 - resolution: "raw-body@npm:2.5.2" +"raw-body@npm:~2.5.3": + version: 2.5.3 + resolution: "raw-body@npm:2.5.3" dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.4.24" - unpipe: "npm:1.0.0" - checksum: 10/863b5171e140546a4d99f349b720abac4410338e23df5e409cfcc3752538c9caf947ce382c89129ba976f71894bd38b5806c774edac35ebf168d02aa1ac11a95 + bytes: "npm:~3.1.2" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.4.24" + unpipe: "npm:~1.0.0" + checksum: 10/f35759fe5a6548e7c529121ead1de4dd163f899749a5896c42e278479df2d9d7f98b5bb17312737c03617765e5a1433e586f717616e5cfbebc13b4738b820601 languageName: node linkType: hard @@ -13048,8 +13055,8 @@ __metadata: linkType: hard "sass@npm:^1.57.1, sass@npm:^1.80.4": - version: 1.83.4 - resolution: "sass@npm:1.83.4" + version: 1.94.2 + resolution: "sass@npm:1.94.2" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -13060,7 +13067,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10/9a7d1c6be1a9e711a1c561d189b9816aa7715f6d0ec0b2ec181f64163788d0caaf4741924eeadce558720b58b1de0e9b21b9dae6a0d14489c4d2a142d3f3b12e + checksum: 10/e60c214ea93677740c9ddfad55c77fd433255bbfdd9faba137acf1215bed5ba6ad9d83efea81feb87a89283931d01f0435227e3fff37c65c263e0ee05f885328 languageName: node linkType: hard @@ -13188,6 +13195,27 @@ __metadata: languageName: node linkType: hard +"send@npm:~0.19.0": + version: 0.19.1 + resolution: "send@npm:0.19.1" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10/360bf50a839c7bbc181f67c3a0f3424a7ad8016dfebcd9eb90891f4b762b4377da14414c32250d67b53872e884171c27469110626f6c22765caa7c38c207ee1d + languageName: node + linkType: hard + "serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.1": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" @@ -13228,7 +13256,7 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.16.2": +"serve-static@npm:~1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" dependencies: @@ -13275,7 +13303,7 @@ __metadata: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10/fde1630422502fbbc19e6844346778f99d449986b2f9cdcceb8326730d2f3d9964dbcb03c02aaadaefffecd0f2c063315ebea8b3ad895914bf1afc1747fc172e @@ -13438,7 +13466,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": +"side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -13685,6 +13713,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:~2.0.1, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 + languageName: node + linkType: hard + "std-env@npm:^3.0.1": version: 3.3.1 resolution: "std-env@npm:3.3.1" @@ -14108,7 +14143,7 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -14215,23 +14250,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.2.0": - version: 5.2.2 - resolution: "typescript@npm:5.2.2" +"typescript@npm:~5.7.0": + version: 5.7.3 + resolution: "typescript@npm:5.7.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/d65e50eb849bd21ff8677e5b9447f9c6e74777e346afd67754934264dcbf4bd59e7d2473f6062d9a015d66bd573311166357e3eb07fea0b52859cf9bb2b58555 + checksum: 10/6a7e556de91db3d34dc51cd2600e8e91f4c312acd8e52792f243c7818dfadb27bae677175fad6947f9c81efb6c57eb6b2d0c736f196a6ee2f1f7d57b74fc92fa languageName: node linkType: hard -"typescript@patch:typescript@npm%3A~5.2.0#optional!builtin": - version: 5.2.2 - resolution: "typescript@patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441" +"typescript@patch:typescript@npm%3A~5.7.0#optional!builtin": + version: 5.7.3 + resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/f79cc2ba802c94c2b78dbb00d767a10adb67368ae764709737dc277273ec148aa4558033a03ce901406b35fddf4eac46dabc94a1e1d12d2587e2b9cfe5707b4a + checksum: 10/dc58d777eb4c01973f7fbf1fd808aad49a0efdf545528dab9b07d94fdcb65b8751742804c3057e9619a4627f2d9cc85547fdd49d9f4326992ad0181b49e61d81 languageName: node linkType: hard @@ -14459,7 +14494,7 @@ __metadata: languageName: node linkType: hard -"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": +"unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" checksum: 10/4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 diff --git a/mkdocs.yml b/mkdocs.yml index 45fdd2c9d6..c07ff5afc4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -135,7 +135,6 @@ nav: - Org Data: 'integrations/ldap/org.md' - Plugins: - Intro to plugins: 'plugins/index.md' - - Existing plugins: 'plugins/existing-plugins.md' - Create a Backstage Plugin: 'plugins/create-a-plugin.md' - Plugin Development: 'plugins/plugin-development.md' - Structure of a plugin: 'plugins/structure-of-a-plugin.md' diff --git a/package.json b/package.json index 69b9e1025a..90b8cb7ad0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.45.0-next.0", + "version": "1.47.0", "backstage": { "cli": { "new": { @@ -47,6 +47,7 @@ "lint:type-deps": "backstage-repo-tools type-deps", "mui-to-bui": "node scripts/mui-to-bui/backstage-migration-analytics.js", "new": "backstage-cli new", + "patch-pr": "node .patches/create-pr-patch.js", "prepare": "husky", "prettier:check": "prettier --check .", "prettier:fix": "prettier --write .", @@ -65,8 +66,8 @@ "test": "NODE_OPTIONS='--no-node-snapshot --experimental-vm-modules' backstage-cli repo test", "test:all": "NODE_OPTIONS='--no-node-snapshot --experimental-vm-modules' backstage-cli repo test --coverage", "test:e2e": "NODE_OPTIONS=--no-node-snapshot playwright test", - "tsc": "tsc", - "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false" + "tsc": "NODE_OPTIONS='--max-old-space-size=8192' tsc", + "tsc:full": "backstage-cli repo clean && NODE_OPTIONS='--max-old-space-size=8192' tsc --skipLibCheck false --incremental false" }, "lint-staged": { "*.{js,jsx,ts,tsx,mjs,cjs}": [ @@ -116,7 +117,6 @@ "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", - "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { @@ -134,29 +134,37 @@ "@backstage/eslint-plugin": "workspace:*", "@backstage/repo-tools": "workspace:*", "@changesets/cli": "^2.14.0", + "@jest/environment-jsdom-abstract": "^30", "@octokit/rest": "^19.0.3", "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^15.0.0", - "@storybook/addon-a11y": "^9.1.7", - "@storybook/addon-docs": "^9.1.7", - "@storybook/addon-links": "^9.1.7", - "@storybook/addon-themes": "^9.1.7", - "@storybook/react-vite": "^9.1.7", + "@storybook/addon-a11y": "^10.2.0-alpha.7", + "@storybook/addon-docs": "^10.2.0-alpha.7", + "@storybook/addon-links": "^10.2.0-alpha.7", + "@storybook/addon-mcp": "0.1.8", + "@storybook/addon-themes": "^10.2.0-alpha.7", + "@storybook/addon-vitest": "^10.2.0-alpha.7", + "@storybook/react-vite": "^10.2.0-alpha.7", "@techdocs/cli": "workspace:*", "@types/cacheable-request": "^8.3.6", + "@types/jest": "^30", + "@types/jsdom": "^27", "@types/memjs": "^1.3.3", - "@types/node": "^20.16.0", + "@types/node": "^22.13.14", "@types/webpack": "^5.28.0", "array-to-table": "^1.0.1", "command-exists": "^1.2.9", - "cross-env": "^7.0.0", + "cross-env": "^10.0.0", "e2e-test": "workspace:*", "eslint": "^8.6.0", "eslint-plugin-notice": "^1.0.0", "eslint-plugin-react": "^7.28.0", - "eslint-plugin-testing-library": "^6.0.0", + "eslint-plugin-testing-library": "^7.0.0", "fs-extra": "^11.2.0", "husky": "^9.0.0", + "jest": "^30", + "js-yaml": "^4.1.1", + "jsdom": "^27", "lint-staged": "^15.0.0", "madge": "^8.0.0", "minimist": "^1.2.5", @@ -166,7 +174,7 @@ "shx": "^0.4.0", "sloc": "^0.3.1", "sort-package-json": "^2.8.0", - "storybook": "^9.1.7", + "storybook": "^10.2.0-alpha.7", "ts-morph": "^24.0.0", "typedoc": "^0.28.0", "typescript": "~5.7.0", @@ -174,7 +182,7 @@ }, "packageManager": "yarn@4.8.1", "engines": { - "node": "20 || 22" + "node": "22 || 24" }, "madge": { "fileExtensions": [ diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index fbbbdf6569..04838f53c4 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/app-defaults +## 1.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## 1.7.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## 1.7.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-permission-react@0.4.39 + +## 1.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + +## 1.7.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-permission-react@0.4.38 + ## 1.7.2-next.0 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 8cd48112b2..c2625983ac 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.7.2-next.0", + "version": "1.7.4", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 74ba940174..0bd762ac16 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,44 @@ # app-next-example-plugin +## 0.0.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## 0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## 0.0.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + +## 0.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-components@0.18.4-next.0 + +## 0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + ## 0.0.29-next.0 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 5415d793b3..45a5cc82bf 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.29-next.0", + "version": "0.0.31", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next-example-plugin/report.api.md b/packages/app-next-example-plugin/report.api.md index 12346d908c..bfcafbe2d2 100644 --- a/packages/app-next-example-plugin/report.api.md +++ b/packages/app-next-example-plugin/report.api.md @@ -5,9 +5,9 @@ ```ts import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSX as JSX_3 } from 'react/jsx-runtime'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; @@ -16,7 +16,7 @@ const examplePlugin: OverridableFrontendPlugin< {}, {}, { - 'page:example': ExtensionDefinition<{ + 'page:example': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; config: { diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index ffa195a96c..32e2dd696f 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,405 @@ # example-app-next +## 0.0.31 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-catalog@1.32.2 + - @backstage/plugin-home@0.9.0 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-org@0.6.48 + - @backstage/plugin-app@0.3.4 + - @backstage/plugin-notifications@0.5.13 + - @backstage/frontend-app-api@0.14.0 + - @backstage/plugin-api-docs@0.13.3 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25 + - @backstage/plugin-devtools@0.1.35 + - @backstage/plugin-scaffolder@1.35.1 + - @backstage/plugin-catalog-graph@0.5.6 + - @backstage/plugin-search@1.5.3 + - @backstage/plugin-scaffolder-react@1.19.5 + - @backstage/core-compat-api@0.5.6 + - @backstage/frontend-defaults@0.3.5 + - @backstage/plugin-search-react@1.10.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-app-visualizer@0.1.27 + - @backstage/plugin-auth@0.1.4 + - @backstage/plugin-auth-react@0.1.23 + - @backstage/plugin-catalog-import@0.13.9 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes@0.12.15 + - @backstage/plugin-kubernetes-cluster@0.0.33 + - @backstage/plugin-signals@0.0.27 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32 + - @backstage/plugin-techdocs-react@1.3.7 + - @backstage/plugin-user-settings@0.8.31 + +## 0.0.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.1 + - @backstage/plugin-catalog@1.32.2-next.2 + - @backstage/plugin-home@0.9.0-next.2 + - @backstage/plugin-api-docs@0.13.3-next.2 + - @backstage/plugin-scaffolder@1.35.1-next.2 + - @backstage/cli@0.35.2-next.1 + +## 0.0.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.1 + - @backstage/plugin-search@1.5.2-next.1 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/frontend-defaults@0.3.5-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-api-docs@0.13.3-next.1 + - @backstage/plugin-app@0.3.4-next.1 + - @backstage/plugin-app-visualizer@0.1.27-next.1 + - @backstage/plugin-auth@0.1.4-next.0 + - @backstage/plugin-auth-react@0.1.23-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-import@0.13.9-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25-next.0 + - @backstage/plugin-home@0.8.16-next.1 + - @backstage/plugin-kubernetes@0.12.15-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.1 + - @backstage/plugin-notifications@0.5.13-next.0 + - @backstage/plugin-org@0.6.48-next.1 + - @backstage/plugin-scaffolder@1.35.1-next.1 + - @backstage/plugin-scaffolder-react@1.19.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-signals@0.0.27-next.0 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + - @backstage/plugin-user-settings@0.8.31-next.1 + +## 0.0.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-import@0.13.9-next.0 + - @backstage/plugin-scaffolder@1.35.1-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.0 + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-defaults@0.3.5-next.0 + - @backstage/core-compat-api@0.5.6-next.0 + - @backstage/plugin-api-docs@0.13.3-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.0 + - @backstage/plugin-org@0.6.48-next.0 + - @backstage/plugin-user-settings@0.8.31-next.0 + - @backstage/plugin-home@0.8.16-next.0 + - @backstage/plugin-kubernetes@0.12.15-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.0 + - @backstage/plugin-search@1.5.2-next.0 + - @backstage/plugin-app-visualizer@0.1.27-next.0 + - @backstage/plugin-auth@0.1.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-search-react@1.10.1 + +## 0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/plugin-app-visualizer@0.1.27-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-compat-api@0.5.5 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-app-api@0.13.3 + - @backstage/frontend-defaults@0.3.4 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-app@0.3.3 + - @backstage/plugin-auth@0.1.3 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-home@0.8.15 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-techdocs-react@1.3.6 + - @backstage/plugin-user-settings@0.8.30 + +## 0.0.30 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-app@0.3.3 + - @backstage/plugin-org@0.6.47 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/core-components@0.18.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-app-visualizer@0.1.26 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-user-settings@0.8.30 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-home@0.8.15 + - @backstage/theme@0.7.1 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/app-defaults@1.7.3 + - @backstage/core-compat-api@0.5.5 + - @backstage/frontend-app-api@0.13.3 + - @backstage/frontend-defaults@0.3.4 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-auth@0.1.3 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs-react@1.3.6 + +## 0.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-catalog-graph@0.5.4-next.1 + - @backstage/plugin-api-docs@0.13.2-next.1 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/plugin-search@1.5.1-next.1 + - @backstage/plugin-app@0.3.3-next.1 + - @backstage/plugin-org@0.6.47-next.1 + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.2 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home@0.8.15-next.1 + - @backstage/plugin-scaffolder@1.34.4-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-app-visualizer@0.1.26-next.1 + - @backstage/plugin-auth@0.1.3-next.0 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + +## 0.0.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + - @backstage/plugin-app-visualizer@0.1.26-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.1 + +## 0.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.5.4-next.0 + - @backstage/ui@0.9.1-next.0 + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.0 + - @backstage/plugin-app-visualizer@0.1.26-next.0 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + - @backstage/plugin-scaffolder@1.34.4-next.0 + - @backstage/plugin-api-docs@0.13.2-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/plugin-search@1.5.1-next.0 + - @backstage/plugin-home@0.8.15-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/plugin-org@0.6.47-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-compat-api@0.5.5-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/plugin-auth@0.1.3-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## 0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-api-docs@0.13.1 + - @backstage/ui@0.9.0 + - @backstage/plugin-org@0.6.46 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-signals@0.0.25 + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-search@1.5.0 + - @backstage/plugin-home@0.8.14 + - @backstage/plugin-app-visualizer@0.1.25 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30 + - @backstage/plugin-scaffolder-react@1.19.3 + - @backstage/plugin-catalog-graph@0.5.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-user-settings@0.8.29 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-auth@0.1.2 + - @backstage/plugin-catalog-import@0.13.7 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/plugin-scaffolder@1.34.3 + - @backstage/plugin-catalog-unprocessed-entities@0.2.23 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/frontend-defaults@0.3.3 + - @backstage/plugin-app@0.3.2 + - @backstage/plugin-kubernetes@0.12.13 + - @backstage/plugin-notifications@0.5.11 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-auth-react@0.1.21 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-kubernetes-cluster@0.0.31 + - @backstage/plugin-search-common@1.2.21 + +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-signals@0.0.25-next.1 + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-search@1.5.0-next.1 + - @backstage/plugin-app-visualizer@0.1.25-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + - @backstage/plugin-catalog-graph@0.5.3-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.1 + - @backstage/plugin-scaffolder@1.34.3-next.0 + - @backstage/core-compat-api@0.5.4-next.0 + - @backstage/plugin-api-docs@0.13.1-next.1 + - @backstage/plugin-catalog-import@0.13.7-next.0 + - @backstage/plugin-org@0.6.46-next.0 + - @backstage/plugin-scaffolder-react@1.19.3-next.0 + - @backstage/plugin-user-settings@0.8.29-next.0 + +## 0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + ## 0.0.29-next.0 ### Patch Changes diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index dcaddfb9bf..d21e19f0de 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -21,10 +21,18 @@ app: ownerEntityRefs: [cubic-belugas] extensions: + # set availableLanguages example + - api:app/app-language: + config: + availableLanguages: ['en', 'es', 'fr', 'de', 'ja'] + defaultLanguage: 'en' - entity-card:org/members-list: config: showAggregateMembersToggle: true initialRelationAggregation: aggregated + - entity-card:org/ownership: + config: + ownedKinds: ['Component', 'API', 'System'] # - apis.plugin.graphiql.browse.gitlab: true # - graphiql-endpoint:graphiql/gitlab: true @@ -106,6 +114,11 @@ app: # - entity-content:azure-devops/pull-requests # - entity-content:azure-devops/git-tags + # Enable the catalog-unprocessed-entities tab in devtools + - devtools-content:catalog-unprocessed-entities: true + # Disable the catalog-unprocessed-entities element outside devtools + - page:catalog-unprocessed-entities: false + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 746b415db7..46fcabbf5a 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.29-next.0", + "version": "0.0.31", "backstage": { "role": "frontend" }, @@ -57,6 +57,7 @@ "@backstage/plugin-catalog-import": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", "@backstage/plugin-home": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^", @@ -97,7 +98,7 @@ "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", - "cross-env": "^7.0.0" + "cross-env": "^10.0.0" }, "bundled": true } diff --git a/packages/app-next/public/index.html b/packages/app-next/public/index.html index 63ab0bec0c..74387edea3 100644 --- a/packages/app-next/public/index.html +++ b/packages/app-next/public/index.html @@ -4,6 +4,7 @@ + } /> @@ -133,6 +140,8 @@ const app = createApp({ notFoundErrorPageModule, appModuleNav, customHomePageModule, + devtoolsPlugin, + devtoolsPluginUnprocessed, ...collectedLegacyPlugins, ], advanced: { diff --git a/packages/app-next/src/modules/appModuleNav.tsx b/packages/app-next/src/modules/appModuleNav.tsx index aa7d92bd1b..ff89b2d1de 100644 --- a/packages/app-next/src/modules/appModuleNav.tsx +++ b/packages/app-next/src/modules/appModuleNav.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { compatWrapper } from '@backstage/core-compat-api'; import { Link, Sidebar, @@ -106,36 +105,34 @@ export const appModuleNav = createFrontendModule({ extensions: [ NavContentBlueprint.make({ params: { - component: ({ items }) => { - return compatWrapper( - - - } to="/search"> - - - - }> - - {items.map((item, index) => ( - - ))} - - - - - - } - to="/settings" - > - - - - - , - ); - }, + component: ({ items }) => ( + + + } to="/search"> + + + + }> + + {items.map((item, index) => ( + + ))} + + + + + + } + to="/settings" + > + + + + + + ), }, }), ], diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index f524bfc95d..87581cb9aa 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,370 @@ # example-app +## 0.2.117 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/plugin-catalog@1.32.2 + - @backstage/plugin-home@0.9.0 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-org@0.6.48 + - @backstage/plugin-mui-to-bui@0.2.3 + - @backstage/plugin-notifications@0.5.13 + - @backstage/frontend-app-api@0.14.0 + - @backstage/plugin-api-docs@0.13.3 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25 + - @backstage/plugin-devtools@0.1.35 + - @backstage/plugin-scaffolder@1.35.1 + - @backstage/plugin-catalog-graph@0.5.6 + - @backstage/plugin-search@1.5.3 + - @backstage/plugin-scaffolder-react@1.19.5 + - @backstage/plugin-search-react@1.10.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-auth-react@0.1.23 + - @backstage/plugin-catalog-import@0.13.9 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes@0.12.15 + - @backstage/plugin-kubernetes-cluster@0.0.33 + - @backstage/plugin-signals@0.0.27 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32 + - @backstage/plugin-techdocs-react@1.3.7 + - @backstage/plugin-user-settings@0.8.31 + +## 0.2.117-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.1 + - @backstage/plugin-catalog@1.32.2-next.2 + - @backstage/plugin-home@0.9.0-next.2 + - @backstage/plugin-api-docs@0.13.3-next.2 + - @backstage/plugin-scaffolder@1.35.1-next.2 + - @backstage/cli@0.35.2-next.1 + +## 0.2.117-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.1 + - @backstage/plugin-search@1.5.2-next.1 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-api-docs@0.13.3-next.1 + - @backstage/plugin-auth-react@0.1.23-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-catalog-import@0.13.9-next.1 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.25-next.0 + - @backstage/plugin-devtools@0.1.35-next.1 + - @backstage/plugin-home@0.8.16-next.1 + - @backstage/plugin-kubernetes@0.12.15-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.1 + - @backstage/plugin-notifications@0.5.13-next.0 + - @backstage/plugin-org@0.6.48-next.1 + - @backstage/plugin-scaffolder@1.35.1-next.1 + - @backstage/plugin-scaffolder-react@1.19.5-next.1 + - @backstage/plugin-search-react@1.10.2-next.0 + - @backstage/plugin-signals@0.0.27-next.0 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + - @backstage/plugin-user-settings@0.8.31-next.1 + +## 0.2.117-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-import@0.13.9-next.0 + - @backstage/plugin-scaffolder@1.35.1-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.32-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/plugin-api-docs@0.13.3-next.0 + - @backstage/plugin-catalog-graph@0.5.5-next.0 + - @backstage/plugin-org@0.6.48-next.0 + - @backstage/plugin-user-settings@0.8.31-next.0 + - @backstage/plugin-home@0.8.16-next.0 + - @backstage/plugin-kubernetes@0.12.15-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.33-next.0 + - @backstage/plugin-search@1.5.2-next.0 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-devtools@0.1.35-next.0 + +## 0.2.117-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/plugin-mui-to-bui@0.2.3-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-app-api@0.13.3 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-devtools@0.1.34 + - @backstage/plugin-home@0.8.15 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-techdocs-react@1.3.6 + - @backstage/plugin-user-settings@0.8.30 + +## 0.2.116 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-catalog-graph@0.5.4 + - @backstage/plugin-api-docs@0.13.2 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-search@1.5.1 + - @backstage/plugin-org@0.6.47 + - @backstage/plugin-scaffolder-react@1.19.4 + - @backstage/core-components@0.18.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes@0.12.14 + - @backstage/plugin-search-react@1.10.1 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24 + - @backstage/plugin-catalog-import@0.13.8 + - @backstage/plugin-notifications@0.5.12 + - @backstage/plugin-user-settings@0.8.30 + - @backstage/plugin-scaffolder@1.35.0 + - @backstage/plugin-devtools@0.1.34 + - @backstage/plugin-home@0.8.15 + - @backstage/theme@0.7.1 + - @backstage/plugin-kubernetes-cluster@0.0.32 + - @backstage/app-defaults@1.7.3 + - @backstage/frontend-app-api@0.13.3 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-auth-react@0.1.22 + - @backstage/plugin-mui-to-bui@0.2.2 + - @backstage/plugin-permission-react@0.4.39 + - @backstage/plugin-signals@0.0.26 + - @backstage/plugin-techdocs-react@1.3.6 + +## 0.2.116-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-catalog-graph@0.5.4-next.1 + - @backstage/plugin-api-docs@0.13.2-next.1 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/plugin-search@1.5.1-next.1 + - @backstage/plugin-org@0.6.47-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.2 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home@0.8.15-next.1 + - @backstage/plugin-scaffolder@1.34.4-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/plugin-devtools@0.1.34-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-mui-to-bui@0.2.2-next.1 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/plugin-search-common@1.2.21 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + +## 0.2.116-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-scaffolder-react@1.19.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + - @backstage/plugin-mui-to-bui@0.2.2-next.1 + - @backstage/plugin-kubernetes@0.12.14-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.1 + +## 0.2.116-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.5.4-next.0 + - @backstage/ui@0.9.1-next.0 + - @backstage/plugin-kubernetes@0.12.14-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.24-next.0 + - @backstage/plugin-catalog-import@0.13.8-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/plugin-notifications@0.5.12-next.0 + - @backstage/plugin-user-settings@0.8.30-next.0 + - @backstage/plugin-scaffolder@1.34.4-next.0 + - @backstage/plugin-api-docs@0.13.2-next.0 + - @backstage/plugin-devtools@0.1.34-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/plugin-search@1.5.1-next.0 + - @backstage/plugin-home@0.8.15-next.0 + - @backstage/plugin-org@0.6.47-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.2-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + - @backstage/plugin-search-react@1.10.1-next.0 + - @backstage/plugin-signals@0.0.26-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.31-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.32-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-auth-react@0.1.22-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-search-common@1.2.21 + +## 0.2.115 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-api-docs@0.13.1 + - @backstage/ui@0.9.0 + - @backstage/plugin-org@0.6.46 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-signals@0.0.25 + - @backstage/frontend-app-api@0.13.2 + - @backstage/plugin-search-react@1.10.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/plugin-search@1.5.0 + - @backstage/plugin-home@0.8.14 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30 + - @backstage/plugin-scaffolder-react@1.19.3 + - @backstage/plugin-catalog-graph@0.5.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-user-settings@0.8.29 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-catalog-import@0.13.7 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/plugin-scaffolder@1.34.3 + - @backstage/plugin-catalog-unprocessed-entities@0.2.23 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-mui-to-bui@0.2.1 + - @backstage/plugin-devtools@0.1.33 + - @backstage/plugin-kubernetes@0.12.13 + - @backstage/plugin-notifications@0.5.11 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/plugin-auth-react@0.1.21 + - @backstage/plugin-catalog-common@1.1.7 + - @backstage/plugin-kubernetes-cluster@0.0.31 + - @backstage/plugin-search-common@1.2.21 + +## 0.2.115-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-signals@0.0.25-next.1 + - @backstage/plugin-search-react@1.10.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/plugin-search@1.5.0-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.30-next.1 + - @backstage/plugin-catalog-react@1.21.3-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + - @backstage/plugin-catalog-graph@0.5.3-next.1 + - @backstage/plugin-scaffolder@1.34.3-next.0 + - @backstage/plugin-api-docs@0.13.1-next.1 + - @backstage/plugin-catalog-import@0.13.7-next.0 + - @backstage/plugin-org@0.6.46-next.0 + - @backstage/plugin-scaffolder-react@1.19.3-next.0 + - @backstage/plugin-user-settings@0.8.29-next.0 + +## 0.2.115-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + - @backstage/plugin-mui-to-bui@0.2.1-next.1 + ## 0.2.115-next.0 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 6766f1fca1..25f5ff443d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.115-next.0", + "version": "0.2.117", "backstage": { "role": "frontend" }, @@ -95,8 +95,8 @@ "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", - "axios": "^1.12.0", - "cross-env": "^7.0.0", + "axios": "^1.13.0", + "cross-env": "^10.0.0", "msw": "^1.0.0" }, "bundled": true diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 9a8267e98e..9b67673b2e 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -4,6 +4,7 @@ + { + + + ) => wrapInTestApp( diff --git a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx index 9ac8bdaaf8..d01c6aabdf 100644 --- a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx +++ b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx @@ -86,6 +86,7 @@ starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4'); export default { title: 'Plugins/Home/Templates', + tags: ['!manifest'], decorators: [ (Story: ComponentType>) => wrapInTestApp( diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index 05738a467a..bb0fb2474e 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/backend-app-api +## 1.4.1 + +### Patch Changes + +- 04db26b: Clean up process event listeners on backend stop to prevent leaks +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## 1.4.0 + +### Minor Changes + +- 5a2d538: Introduced backend startup result tracking and error handling. The `Backend.start()` method now returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed. + + This also improves the default error message when backend startup fails, and of course makes it possible to craft your own custom error reporting based on the startup results. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## 1.4.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.4.0-next.0 + +### Minor Changes + +- 5a2d538: Introduced backend startup result tracking and error handling. The `Backend.start()` method now returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed. + + This also improves the default error message when backend startup fails, and of course makes it possible to craft your own custom error reporting based on the startup results. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.3.0 + +### Minor Changes + +- a17d9df: Updates API for `instanceMetadata` service to return a list of plugins not features. + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config@1.3.6 + +## 1.3.0-next.1 + +### Minor Changes + +- a17d9df: Updates API for `instanceMetadata` service to return a list of plugins not features. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 1.2.9-next.0 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 6defc5bd88..43976588b0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.2.9-next.0", + "version": "1.4.1", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-app-api/report.api.md b/packages/backend-app-api/report.api.md index 88fa34e6e4..62b46b93e6 100644 --- a/packages/backend-app-api/report.api.md +++ b/packages/backend-app-api/report.api.md @@ -4,6 +4,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CustomErrorBase } from '@backstage/errors'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -17,11 +18,30 @@ export interface Backend { }>, ): void; // (undocumented) - start(): Promise; + start(): Promise<{ + result: BackendStartupResult; + }>; // (undocumented) stop(): Promise; } +// @public +export class BackendStartupError extends CustomErrorBase { + constructor(startupResult: BackendStartupResult); + // (undocumented) + name: 'BackendStartupError'; + // (undocumented) + get result(): BackendStartupResult; +} + +// @public +export interface BackendStartupResult { + beginAt: Date; + outcome: 'success' | 'failure'; + plugins: PluginStartupResult[]; + resultAt: Date; +} + // @public (undocumented) export function createSpecializedBackend( options: CreateSpecializedBackendOptions, @@ -32,4 +52,25 @@ export interface CreateSpecializedBackendOptions { // (undocumented) defaultServiceFactories: ServiceFactory[]; } + +// @public +export interface ModuleStartupResult { + failure?: { + error: Error; + allowed: boolean; + }; + moduleId: string; + resultAt: Date; +} + +// @public +export interface PluginStartupResult { + failure?: { + error: Error; + allowed: boolean; + }; + modules: ModuleStartupResult[]; + pluginId: string; + resultAt: Date; +} ``` diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 10d3cceba0..1c0bf5a0ef 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -22,10 +22,11 @@ import { createExtensionPoint, createBackendFeatureLoader, ServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; import { mockServices } from '@backstage/backend-test-utils'; +import { BackendStartupError } from './BackendStartupError'; const baseFactories = [ mockServices.rootLifecycle.factory(), @@ -806,49 +807,6 @@ describe('BackendInitializer', () => { ); }); - it('should forward errors when multiple plugins fail to start', async () => { - const init = new BackendInitializer([]); - init.add( - createBackendPlugin({ - pluginId: 'test-1', - register(reg) { - reg.registerInit({ - deps: {}, - async init() { - throw new Error('NOPE A'); - }, - }); - }, - }), - ); - init.add( - createBackendPlugin({ - pluginId: 'test-2', - register(reg) { - reg.registerInit({ - deps: {}, - async init() { - throw new Error('NOPE B'); - }, - }); - }, - }), - ); - const result = init.start(); - - await expect(result).rejects.toThrow('Backend startup failed'); - await expect(result).rejects.toMatchObject({ - errors: [ - expect.objectContaining({ - message: "Plugin 'test-1' startup failed; caused by Error: NOPE A", - }), - expect.objectContaining({ - message: "Plugin 'test-2' startup failed; caused by Error: NOPE B", - }), - ], - }); - }); - it('should forward errors when modules fail to start', async () => { const init = new BackendInitializer([]); init.add(testPlugin); @@ -1110,7 +1068,7 @@ describe('BackendInitializer', () => { }); it('should properly add plugins + modules to the instance metadata service', async () => { - expect.assertions(2); + expect.assertions(1); const backend = new BackendInitializer(baseFactories); const plugin = createBackendPlugin({ pluginId: 'test', @@ -1126,31 +1084,25 @@ describe('BackendInitializer', () => { register(reg) { reg.registerInit({ deps: { - instanceMetadata: instanceMetadataServiceRef, + instanceMetadata: coreServices.rootInstanceMetadata, }, async init({ instanceMetadata }) { - expect(instanceMetadata.getInstalledFeatures()).toEqual([ + await expect( + instanceMetadata.getInstalledPlugins(), + ).resolves.toEqual([ { pluginId: 'test', - type: 'plugin', - }, - { - pluginId: 'test', - moduleId: 'test', - type: 'module', + modules: [ + { + moduleId: 'test', + }, + ], }, { pluginId: 'instance-metadata', - type: 'plugin', + modules: [], }, ]); - expect(instanceMetadata.getInstalledFeatures().map(String)).toEqual( - [ - 'plugin{pluginId=test}', - 'module{moduleId=test,pluginId=test}', - 'plugin{pluginId=instance-metadata}', - ], - ); }, }); }, @@ -1171,6 +1123,67 @@ describe('BackendInitializer', () => { await backend.start(); }); + it('should ignore modules that do not have a matching plugin', async () => { + expect.assertions(1); + const backend = new BackendInitializer(baseFactories); + const instanceMetadataPlugin = createBackendPlugin({ + pluginId: 'instance-metadata', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async init({ instanceMetadata }) { + await expect( + instanceMetadata.getInstalledPlugins(), + ).resolves.toEqual([ + { + pluginId: 'instance-metadata', + modules: [], + }, + ]); + }, + }); + }, + }); + const module = createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }); + backend.add(module); + backend.add(instanceMetadataPlugin); + await backend.start(); + }); + + it('should prevent writes to the instance metadata service', async () => { + expect.assertions(1); + const backend = new BackendInitializer(baseFactories); + const plugin = createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async init({ instanceMetadata }) { + const plugins = await instanceMetadata.getInstalledPlugins(); + await expect(() => { + (plugins[0] as any).pluginId = 'foo'; + }).toThrow(/Cannot assign to read only property/); + }, + }); + }, + }); + backend.add(plugin); + await backend.start(); + }); + it('should properly wait for all modules that consume an extension point to really finish, before starting the module that provides that extension point', async () => { expect.assertions(3); const backend = new BackendInitializer(baseFactories); @@ -1234,4 +1247,594 @@ describe('BackendInitializer', () => { await backend.add(slowConsumerModule); await backend.start(); }); + + describe('startup results', () => { + it('should return successful startup result when all plugins and modules start successfully', async () => { + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin2', + moduleId: 'module2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins).toHaveLength(2); + expect(result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + modules: [ + { + moduleId: 'module1', + }, + ], + }); + expect(result.plugins[0].failure).toBeUndefined(); + expect(result.plugins[0].modules[0].failure).toBeUndefined(); + expect(result.plugins[1]).toMatchObject({ + pluginId: 'plugin2', + modules: [ + { + moduleId: 'module2', + }, + ], + }); + expect(result.plugins[1].failure).toBeUndefined(); + expect(result.plugins[1].modules[0].failure).toBeUndefined(); + }); + + it('should throw BackendStartupError with results when plugin fails to start', async () => { + const init = new BackendInitializer(baseFactories); + const error = new Error('Plugin failed'); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error; + }, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + expect(err?.result.plugins).toHaveLength(2); + expect(err?.result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + failure: { error, allowed: false }, + }); + expect(err?.result.plugins[1]).toMatchObject({ + pluginId: 'plugin2', + }); + expect(err?.result.plugins[1].failure).toBeUndefined(); + }); + + it('should throw BackendStartupError with results when module fails to start', async () => { + const init = new BackendInitializer(baseFactories); + const error = new Error('Module failed'); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error; + }, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + expect(err?.result.plugins).toHaveLength(1); + expect(err?.result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + modules: [ + { + moduleId: 'module1', + failure: { error, allowed: false }, + }, + ], + }); + expect(err?.result.plugins[0].failure).toBeUndefined(); + }); + + it('should throw BackendStartupError with results when multiple plugins fail', async () => { + const init = new BackendInitializer(baseFactories); + const error1 = new Error('Plugin1 failed'); + const error2 = new Error('Plugin2 failed'); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error1; + }, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error2; + }, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + expect(err?.result.plugins).toHaveLength(2); + expect(err?.result.plugins[0].failure?.error).toBe(error1); + expect(err?.result.plugins[1].failure?.error).toBe(error2); + }); + + it('should return results with failure status when plugin boot failure is permitted', async () => { + const error = new Error('Plugin failed'); + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { plugin1: { onPluginBootFailure: 'continue' } }, + }, + }, + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error; + }, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins).toHaveLength(2); + expect(result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + failure: { error, allowed: true }, + }); + expect(result.plugins[1]).toMatchObject({ + pluginId: 'plugin2', + }); + expect(result.plugins[1].failure).toBeUndefined(); + }); + + it('should return results with failure status when module boot failure is permitted', async () => { + const error = new Error('Module failed'); + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { + plugin1: { + modules: { + module1: { onPluginModuleBootFailure: 'continue' }, + }, + }, + }, + }, + }, + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error; + }, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins).toHaveLength(1); + expect(result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + modules: [ + { + moduleId: 'module1', + failure: { error, allowed: true }, + }, + { + moduleId: 'module2', + }, + ], + }); + expect(result.plugins[0].failure).toBeUndefined(); + expect(result.plugins[0].modules[1].failure).toBeUndefined(); + }); + + it('should include all module results even when some modules fail', async () => { + const error1 = new Error('Module1 failed'); + const error2 = new Error('Module2 failed'); + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { + plugin1: { + modules: { + module1: { onPluginModuleBootFailure: 'continue' }, + module2: { onPluginModuleBootFailure: 'continue' }, + }, + }, + }, + }, + }, + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error1; + }, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module2', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error2; + }, + }); + }, + }), + ); + init.add( + createBackendModule({ + pluginId: 'plugin1', + moduleId: 'module3', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins[0].modules).toHaveLength(3); + expect(result.plugins[0].modules[0]).toMatchObject({ + moduleId: 'module1', + failure: { error: error1, allowed: true }, + }); + expect(result.plugins[0].modules[1]).toMatchObject({ + moduleId: 'module2', + failure: { error: error2, allowed: true }, + }); + expect(result.plugins[0].modules[2]).toMatchObject({ + moduleId: 'module3', + }); + expect(result.plugins[0].modules[2].failure).toBeUndefined(); + }); + + it('should return results for plugins without modules', async () => { + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins).toHaveLength(1); + expect(result.plugins[0]).toMatchObject({ + pluginId: 'plugin1', + modules: [], + }); + expect(result.plugins[0].failure).toBeUndefined(); + }); + + it('should include error information in BackendStartupError', async () => { + const error = new Error('Test error'); + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw error; + }, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + expect(err?.message).toBe( + "Backend startup failed due to the following errors:\n Plugin 'plugin1' startup failed; caused by Error: Test error", + ); + expect(err?.result).toBeDefined(); + expect(err?.result.plugins[0].failure?.error).toBe(error); + }); + + it('should handle plugin scoped service factory failures', async () => { + const serviceFactoryError = new Error('Service factory failed'); + const ref = createServiceRef<{ value: string }>({ + id: 'failing-service', + }); + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref, + deps: {}, + factory: () => { + throw serviceFactoryError; + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: { service: ref }, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + expect(err?.result.plugins).toHaveLength(1); + expect(err?.result.plugins[0].pluginId).toBe('plugin1'); + expect(err?.result.plugins[0].failure).toBeDefined(); + expect(err?.result.plugins[0].failure?.allowed).toBe(false); + expect(err?.result.plugins[0].failure?.error.message).toContain( + "Failed to instantiate service 'failing-service'", + ); + expect(err?.result.plugins[0].failure?.error.message).toContain( + 'Service factory failed', + ); + }); + + it('should handle plugin scoped service factory failures with allowed boot failure', async () => { + const serviceFactoryError = new Error('Service factory failed'); + const ref = createServiceRef<{ value: string }>({ + id: 'failing-service', + }); + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { plugin1: { onPluginBootFailure: 'continue' } }, + }, + }, + }, + }), + createServiceFactory({ + service: ref, + deps: {}, + factory: () => { + throw serviceFactoryError; + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'plugin1', + register(reg) { + reg.registerInit({ + deps: { service: ref }, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + + expect(result.plugins).toHaveLength(1); + expect(result.plugins[0].pluginId).toBe('plugin1'); + expect(result.plugins[0].failure).toBeDefined(); + expect(result.plugins[0].failure?.allowed).toBe(true); + expect(result.plugins[0].failure?.error.message).toContain( + "Failed to instantiate service 'failing-service'", + ); + expect(result.plugins[0].failure?.error.message).toContain( + 'Service factory failed', + ); + }); + }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 09b22e3a7a..2d32306385 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -24,7 +24,6 @@ import { RootLifecycleService, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -36,14 +35,14 @@ import type { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; -import { - instanceMetadataServiceRef, - BackendFeatureMeta, -} from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; -import { createInitializationLogger } from './createInitializationLogger'; -import { unwrapFeature } from './helpers'; +import { createInitializationResultCollector } from './createInitializationResultCollector'; +import { deepFreeze, unwrapFeature } from './helpers'; +import type { RootInstanceMetadataServicePluginInfo } from '@backstage/backend-plugin-api'; +import { BackendStartupResult } from './types'; +import { BackendStartupError } from './BackendStartupError'; +import { createAllowBootFailurePredicate } from './createAllowBootFailurePredicate'; export interface BackendRegisterInit { consumes: Set; @@ -101,67 +100,65 @@ const instanceRegistry = new (class InstanceRegistry { }; })(); -function createInstanceMetadataServiceFactory( - registrations: InternalBackendRegistrations[], +function createRootInstanceMetadataServiceFactory( + rawRegistrations: InternalBackendRegistrations[], ) { - const installedFeatures = registrations - .map(registration => { - if (registration.featureType === 'registrations') { - return registration - .getRegistrations() - .map(feature => { - if (feature.type === 'plugin') { - return Object.defineProperty( - { - type: 'plugin', - pluginId: feature.pluginId, - }, - 'toString', - { - enumerable: false, - configurable: true, - value: () => `plugin{pluginId=${feature.pluginId}}`, - }, - ); - } else if (feature.type === 'module') { - return Object.defineProperty( - { - type: 'module', - pluginId: feature.pluginId, - moduleId: feature.moduleId, - }, - 'toString', - { - enumerable: false, - configurable: true, - value: () => - `module{moduleId=${feature.moduleId},pluginId=${feature.pluginId}}`, - }, - ); - } - // Ignore unknown feature types. - return undefined; - }) - .filter(Boolean) as BackendFeatureMeta[]; - } - return []; - }) - .flat(); + const installedPlugins: Map = + new Map(); + const registrations = rawRegistrations + .filter(registration => registration.featureType === 'registrations') + .flatMap(registration => registration.getRegistrations()); + const plugins = registrations.filter( + registration => registration.type === 'plugin', + ); + const modules = registrations.filter( + registration => registration.type === 'module', + ); + for (const plugin of plugins) { + const { pluginId } = plugin; + if (!installedPlugins.get(pluginId)) { + installedPlugins.set(pluginId, { + pluginId, + modules: [], + }); + } + } + for (const module of modules) { + const { pluginId, moduleId } = module; + const installedPlugin = installedPlugins.get(pluginId); + if (installedPlugin) { + (installedPlugin.modules as Array<{ moduleId: string }>).push({ + moduleId, + }); + } + } + return createServiceFactory({ - service: instanceMetadataServiceRef, + service: coreServices.rootInstanceMetadata, deps: {}, - factory: async () => ({ getInstalledFeatures: () => installedFeatures }), + factory: async () => { + const readonlyInstalledPlugins = deepFreeze([ + ...installedPlugins.values(), + ]); + const instanceMetadata = { + getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins), + }; + + return instanceMetadata; + }, }); } export class BackendInitializer { - #startPromise?: Promise; + #startPromise?: Promise<{ result: BackendStartupResult }>; #stopPromise?: Promise; #registrations = new Array(); #extensionPoints = new Map(); #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); #registeredFeatureLoaders = new Array(); + #unhandledRejectionHandler?: (reason: Error) => void; + #uncaughtExceptionHandler?: (error: Error) => void; constructor(defaultApiFactories: ServiceFactory[]) { this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); @@ -231,7 +228,7 @@ export class BackendInitializer { } } - async start(): Promise { + async start(): Promise<{ result: BackendStartupResult }> { if (this.#startPromise) { throw new Error('Backend has already started'); } @@ -242,10 +239,10 @@ export class BackendInitializer { instanceRegistry.register(this); this.#startPromise = this.#doStart(); - await this.#startPromise; + return await this.#startPromise; } - async #doStart(): Promise { + async #doStart(): Promise<{ result: BackendStartupResult }> { this.#serviceRegistry.checkForCircularDeps(); for (const feature of this.#registeredFeatures) { @@ -255,7 +252,7 @@ export class BackendInitializer { await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); this.#serviceRegistry.add( - createInstanceMetadataServiceFactory(this.#registrations), + createRootInstanceMetadataServiceFactory(this.#registrations), ); // This makes sure that any uncaught errors or unhandled rejections are @@ -270,16 +267,18 @@ export class BackendInitializer { coreServices.rootLogger, 'root', ); - process.on('unhandledRejection', (reason: Error) => { + this.#unhandledRejectionHandler = (reason: Error) => { rootLogger ?.child({ type: 'unhandledRejection' }) ?.error('Unhandled rejection', reason); - }); - process.on('uncaughtException', error => { + }; + this.#uncaughtExceptionHandler = (error: Error) => { rootLogger ?.child({ type: 'uncaughtException' }) ?.error('Uncaught exception', error); - }); + }; + process.on('unhandledRejection', this.#unhandledRejectionHandler); + process.on('uncaughtException', this.#uncaughtExceptionHandler); } // Initialize all root scoped services @@ -339,26 +338,26 @@ export class BackendInitializer { } } - const allPluginIds = [...pluginInits.keys()]; - - const initLogger = createInitializationLogger( - allPluginIds, - await this.#serviceRegistry.get(coreServices.rootLogger, 'root'), - ); + const pluginIds = [...pluginInits.keys()]; const rootConfig = await this.#serviceRegistry.get( coreServices.rootConfig, 'root', ); + const rootLogger = await this.#serviceRegistry.get( + coreServices.rootLogger, + 'root', + ); + + const resultCollector = createInitializationResultCollector({ + pluginIds, + logger: rootLogger, + allowBootFailurePredicate: createAllowBootFailurePredicate(rootConfig), + }); // All plugins are initialized in parallel - const results = await Promise.allSettled( - allPluginIds.map(async pluginId => { - const isBootFailurePermitted = this.#getPluginBootFailurePredicate( - pluginId, - rootConfig, - ); - + await Promise.all( + pluginIds.map(async pluginId => { try { // Initialize all eager services await this.#serviceRegistry.initializeEagerServicesWithScope( @@ -389,37 +388,21 @@ export class BackendInitializer { } await tree.parallelTopologicalTraversal( async ({ moduleId, moduleInit }) => { - const isModuleBootFailurePermitted = - this.#getPluginModuleBootFailurePredicate( - pluginId, - moduleId, - rootConfig, - ); - try { const moduleDeps = await this.#getInitDeps( moduleInit.init.deps, pluginId, moduleId, ); - await moduleInit.init.func(moduleDeps).catch(error => { - throw new ForwardedError( - `Module '${moduleId}' for plugin '${pluginId}' startup failed`, - error, - ); - }); + await moduleInit.init.func(moduleDeps); + resultCollector.onPluginModuleResult(pluginId, moduleId); } catch (error: unknown) { assertError(error); - if (isModuleBootFailurePermitted) { - initLogger.onPermittedPluginModuleFailure( - pluginId, - moduleId, - error, - ); - } else { - initLogger.onPluginModuleFailed(pluginId, moduleId, error); - throw error; - } + resultCollector.onPluginModuleResult( + pluginId, + moduleId, + error, + ); } }, ); @@ -433,46 +416,36 @@ export class BackendInitializer { pluginInit.init.deps, pluginId, ); - await pluginInit.init.func(pluginDeps).catch(error => { - throw new ForwardedError( - `Plugin '${pluginId}' startup failed`, - error, - ); - }); + await pluginInit.init.func(pluginDeps); } - initLogger.onPluginStarted(pluginId); + resultCollector.onPluginResult(pluginId); // Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully const lifecycleService = await this.#getPluginLifecycleImpl(pluginId); await lifecycleService.startup(); } catch (error: unknown) { assertError(error); - if (isBootFailurePermitted) { - initLogger.onPermittedPluginFailure(pluginId, error); - } else { - initLogger.onPluginFailed(pluginId, error); - throw error; - } + resultCollector.onPluginResult(pluginId, error); } }), - ); + ).catch(error => { + throw new ForwardedError( + 'Unexpected uncaught backend startup error', + error, + ); + }); - const initErrors = results.flatMap(r => - r.status === 'rejected' ? [r.reason] : [], - ); - if (initErrors.length === 1) { - throw initErrors[0]; - } else if (initErrors.length > 1) { - // TODO(Rugvip): Seems like there aren't proper types for AggregateError yet - throw new (AggregateError as any)(initErrors, 'Backend startup failed'); + const result = resultCollector.finalize(); + if (result.outcome === 'failure') { + throw new BackendStartupError(result); } // Once all plugins and modules have been initialized, we can signal that the backend has started up successfully const lifecycleService = await this.#getRootLifecycleImpl(); await lifecycleService.startup(); - initLogger.onAllStarted(); + return { result }; } // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit @@ -521,6 +494,16 @@ export class BackendInitializer { // Once all plugin shutdown hooks are done, run root shutdown hooks. await rootLifecycleService.shutdown(); + + // Clean up process event listeners to prevent memory leaks and duplicate logging + if (this.#unhandledRejectionHandler) { + process.off('unhandledRejection', this.#unhandledRejectionHandler); + this.#unhandledRejectionHandler = undefined; + } + if (this.#uncaughtExceptionHandler) { + process.off('uncaughtException', this.#uncaughtExceptionHandler); + this.#uncaughtExceptionHandler = undefined; + } } // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this @@ -661,38 +644,6 @@ export class BackendInitializer { } } } - - #getPluginBootFailurePredicate(pluginId: string, config?: Config): boolean { - const defaultStartupBootFailureValue = - config?.getOptionalString( - 'backend.startup.default.onPluginBootFailure', - ) ?? 'abort'; - - const pluginStartupBootFailureValue = - config?.getOptionalString( - `backend.startup.plugins.${pluginId}.onPluginBootFailure`, - ) ?? defaultStartupBootFailureValue; - - return pluginStartupBootFailureValue === 'continue'; - } - - #getPluginModuleBootFailurePredicate( - pluginId: string, - moduleId: string, - config?: Config, - ): boolean { - const defaultStartupBootFailureValue = - config?.getOptionalString( - 'backend.startup.default.onPluginModuleBootFailure', - ) ?? 'abort'; - - const pluginModuleStartupBootFailureValue = - config?.getOptionalString( - `backend.startup.plugins.${pluginId}.modules.${moduleId}.onPluginModuleBootFailure`, - ) ?? defaultStartupBootFailureValue; - - return pluginModuleStartupBootFailureValue === 'continue'; - } } function toInternalBackendFeature( diff --git a/packages/backend-app-api/src/wiring/BackendStartupError.ts b/packages/backend-app-api/src/wiring/BackendStartupError.ts new file mode 100644 index 0000000000..7876b2c236 --- /dev/null +++ b/packages/backend-app-api/src/wiring/BackendStartupError.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CustomErrorBase } from '@backstage/errors'; +import { BackendStartupResult } from './types'; + +function formatMessage(startupResult: BackendStartupResult): string { + const parts: string[] = [ + 'Backend startup failed due to the following errors:', + ]; + const failures: string[] = []; + + for (const plugin of startupResult.plugins) { + if (plugin.failure && !plugin.failure.allowed) { + failures.push( + ` Plugin '${plugin.pluginId}' startup failed; caused by ${plugin.failure.error}`, + ); + } + + for (const mod of plugin.modules) { + if (mod.failure && !mod.failure.allowed) { + failures.push( + ` Module '${mod.moduleId}' for plugin '${plugin.pluginId}' startup failed; caused by ${mod.failure.error}`, + ); + } + } + } + + if (failures.length > 0) { + parts.push(...failures); + } + + return parts.join('\n'); +} + +/** + * Error thrown when backend startup fails. + * Includes detailed startup results for all plugins and modules. + * + * @public + */ +export class BackendStartupError extends CustomErrorBase { + name = 'BackendStartupError' as const; + + /** + * The startup results for all plugins and modules. + */ + readonly #startupResult: BackendStartupResult; + + constructor(startupResult: BackendStartupResult) { + super(formatMessage(startupResult)); + this.#startupResult = startupResult; + } + + get result(): BackendStartupResult { + return this.#startupResult; + } +} diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index 49a8bbbbfb..79ee55780f 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -17,7 +17,7 @@ import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; import { unwrapFeature } from './helpers'; -import { Backend } from './types'; +import { Backend, BackendStartupResult } from './types'; export class BackstageBackend implements Backend { #initializer: BackendInitializer; @@ -34,8 +34,8 @@ export class BackstageBackend implements Backend { } } - async start(): Promise { - await this.#initializer.start(); + async start(): Promise<{ result: BackendStartupResult }> { + return await this.#initializer.start(); } async stop(): Promise { diff --git a/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.test.ts b/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.test.ts new file mode 100644 index 0000000000..f12998aa1d --- /dev/null +++ b/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.test.ts @@ -0,0 +1,520 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices } from '@backstage/backend-test-utils'; +import { createAllowBootFailurePredicate } from './createAllowBootFailurePredicate'; + +describe('createAllowBootFailurePredicate', () => { + describe('when no config is provided', () => { + it('should default to abort for plugins', () => { + const predicate = createAllowBootFailurePredicate(); + expect(predicate('test-plugin')).toBe(false); + }); + + it('should default to abort for modules', () => { + const predicate = createAllowBootFailurePredicate(); + expect(predicate('test-plugin', 'test-module')).toBe(false); + }); + }); + + describe('default plugin boot failure configuration', () => { + it('should use abort as default when not configured', () => { + const config = mockServices.rootConfig(); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(false); + }); + + it('should use continue when default is set to continue', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'continue', + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + }); + + it('should use abort when default is set to abort', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'abort', + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(false); + }); + }); + + describe('default module boot failure configuration', () => { + it('should use abort as default when not configured', () => { + const config = mockServices.rootConfig(); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(false); + }); + + it('should use continue when default is set to continue', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'continue', + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(true); + }); + + it('should use abort when default is set to abort', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(false); + }); + }); + + describe('plugin-specific overrides', () => { + it('should override default with plugin-specific continue', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'abort', + }, + plugins: { + 'test-plugin': { + onPluginBootFailure: 'continue', + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + }); + + it('should override default with plugin-specific abort', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'continue', + }, + plugins: { + 'test-plugin': { + onPluginBootFailure: 'abort', + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(false); + }); + + it('should use default when plugin-specific config is not set', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'continue', + }, + plugins: { + 'other-plugin': { + onPluginBootFailure: 'abort', + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + }); + + it('should handle multiple plugins independently', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'abort', + }, + plugins: { + 'plugin-a': { + onPluginBootFailure: 'continue', + }, + 'plugin-b': { + onPluginBootFailure: 'abort', + }, + 'plugin-c': { + // No override, uses default + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('plugin-a')).toBe(true); + expect(predicate('plugin-b')).toBe(false); + expect(predicate('plugin-c')).toBe(false); + }); + }); + + describe('module-specific overrides', () => { + it('should override default with module-specific continue', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'abort', + }, + plugins: { + 'test-plugin': { + modules: { + 'test-module': { + onPluginModuleBootFailure: 'continue', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(true); + }); + + it('should override default with module-specific abort', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'continue', + }, + plugins: { + 'test-plugin': { + modules: { + 'test-module': { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(false); + }); + + it('should use default when module-specific config is not set', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'continue', + }, + plugins: { + 'test-plugin': { + modules: { + 'other-module': { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(true); + }); + + it('should handle multiple modules independently', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'abort', + }, + plugins: { + 'test-plugin': { + modules: { + 'module-a': { + onPluginModuleBootFailure: 'continue', + }, + 'module-b': { + onPluginModuleBootFailure: 'abort', + }, + 'module-c': { + // No override, uses default + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'module-a')).toBe(true); + expect(predicate('test-plugin', 'module-b')).toBe(false); + expect(predicate('test-plugin', 'module-c')).toBe(false); + }); + + it('should handle modules across different plugins', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'abort', + }, + plugins: { + 'plugin-a': { + modules: { + 'module-x': { + onPluginModuleBootFailure: 'continue', + }, + }, + }, + 'plugin-b': { + modules: { + 'module-y': { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('plugin-a', 'module-x')).toBe(true); + expect(predicate('plugin-b', 'module-y')).toBe(false); + }); + }); + + describe('combined plugin and module configurations', () => { + it('should use module config when both plugin and module configs exist', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'abort', + onPluginModuleBootFailure: 'abort', + }, + plugins: { + 'test-plugin': { + onPluginBootFailure: 'continue', + modules: { + 'test-module': { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + expect(predicate('test-plugin', 'test-module')).toBe(false); + }); + + it('should use plugin default when module config does not exist', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'abort', + onPluginModuleBootFailure: 'abort', + }, + plugins: { + 'test-plugin': { + onPluginBootFailure: 'continue', + modules: { + 'other-module': { + onPluginModuleBootFailure: 'abort', + }, + }, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + expect(predicate('test-plugin', 'test-module')).toBe(false); // Uses default module config + }); + }); + + describe('edge cases', () => { + it('should handle empty plugins config', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginBootFailure: 'continue', + }, + plugins: {}, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin')).toBe(true); + }); + + it('should handle plugin with empty modules config', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'continue', + }, + plugins: { + 'test-plugin': { + modules: {}, + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(true); + }); + + it('should handle plugin without modules config', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + default: { + onPluginModuleBootFailure: 'continue', + }, + plugins: { + 'test-plugin': { + onPluginBootFailure: 'abort', + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('test-plugin', 'test-module')).toBe(true); + }); + + it('should handle case sensitivity correctly', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + startup: { + plugins: { + 'Test-Plugin': { + onPluginBootFailure: 'continue', + }, + }, + }, + }, + }, + }); + const predicate = createAllowBootFailurePredicate(config); + expect(predicate('Test-Plugin')).toBe(true); + expect(predicate('test-plugin')).toBe(false); // Different case + }); + }); + + describe('performance - configuration read upfront', () => { + it('should read configuration only once', () => { + const configData = { + backend: { + startup: { + default: { + onPluginBootFailure: 'continue', + }, + plugins: { + 'plugin-a': { + onPluginBootFailure: 'abort', + modules: { + 'module-x': { + onPluginModuleBootFailure: 'continue', + }, + }, + }, + }, + }, + }, + }; + const config = mockServices.rootConfig({ data: configData }); + const predicate = createAllowBootFailurePredicate(config); + + // Call predicate multiple times - should use cached values + expect(predicate('plugin-a')).toBe(false); + expect(predicate('plugin-a')).toBe(false); + expect(predicate('plugin-a', 'module-x')).toBe(true); + expect(predicate('plugin-a', 'module-x')).toBe(true); + expect(predicate('plugin-b')).toBe(true); // Uses default + }); + }); +}); diff --git a/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.ts b/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.ts new file mode 100644 index 0000000000..15b73cb8ed --- /dev/null +++ b/packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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'; + +export type AllowBootFailurePredicate = ( + pluginId: string, + moduleId?: string, +) => boolean; + +/** + * Creates a predicate function that determines whether a boot failure should be + * allowed for a given plugin or module based on configuration. + * + * @param config - The configuration object to read boot failure settings from + * @returns A predicate function that accepts a pluginId and optional moduleId, + * and returns true if boot failures should be allowed, false otherwise. + */ +export function createAllowBootFailurePredicate( + config?: Config, +): AllowBootFailurePredicate { + // Read default values upfront + const defaultPluginBootFailure = + config?.getOptionalString('backend.startup.default.onPluginBootFailure') ?? + 'abort'; + const defaultModuleBootFailure = + config?.getOptionalString( + 'backend.startup.default.onPluginModuleBootFailure', + ) ?? 'abort'; + + // Read plugin-specific overrides upfront + const pluginOverrides = new Map(); + const moduleOverrides = new Map>(); + + const pluginsConfig = config?.getOptionalConfig('backend.startup.plugins'); + if (pluginsConfig) { + for (const pluginId of pluginsConfig.keys()) { + const pluginConfig = pluginsConfig.getConfig(pluginId); + const pluginBootFailure = pluginConfig.getOptionalString( + 'onPluginBootFailure', + ); + if (pluginBootFailure) { + pluginOverrides.set(pluginId, pluginBootFailure); + } + + // Read module-specific overrides + const modulesConfig = pluginConfig.getOptionalConfig('modules'); + if (modulesConfig) { + const moduleMap = new Map(); + for (const moduleId of modulesConfig.keys()) { + const moduleConfig = modulesConfig.getConfig(moduleId); + const moduleBootFailure = moduleConfig.getOptionalString( + 'onPluginModuleBootFailure', + ); + if (moduleBootFailure) { + moduleMap.set(moduleId, moduleBootFailure); + } + } + if (moduleMap.size > 0) { + moduleOverrides.set(pluginId, moduleMap); + } + } + } + } + + return (pluginId: string, moduleId?: string): boolean => { + if (moduleId !== undefined) { + // Module-specific boot failure predicate + const moduleMap = moduleOverrides.get(pluginId); + const moduleBootFailure = + moduleMap?.get(moduleId) ?? defaultModuleBootFailure; + return moduleBootFailure === 'continue'; + } + // Plugin-specific boot failure predicate + const pluginBootFailure = + pluginOverrides.get(pluginId) ?? defaultPluginBootFailure; + return pluginBootFailure === 'continue'; + }; +} diff --git a/packages/backend-app-api/src/wiring/createInitializationLogger.ts b/packages/backend-app-api/src/wiring/createInitializationLogger.ts deleted file mode 100644 index 600826a435..0000000000 --- a/packages/backend-app-api/src/wiring/createInitializationLogger.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RootLoggerService } from '@backstage/backend-plugin-api'; - -const LOGGER_INTERVAL_MAX = 60_000; - -function joinIds(ids: Iterable): string { - return [...ids].map(id => `'${id}'`).join(', '); -} - -export function createInitializationLogger( - pluginIds: string[], - rootLogger?: RootLoggerService, -): { - onPluginStarted(pluginId: string): void; - onPluginFailed(pluginId: string, error: Error): void; - onPermittedPluginFailure(pluginId: string, error: Error): void; - onPluginModuleFailed(pluginId: string, moduleId: string, error: Error): void; - onPermittedPluginModuleFailure( - pluginId: string, - moduleId: string, - error: Error, - ): void; - onAllStarted(): void; -} { - const logger = rootLogger?.child({ type: 'initialization' }); - const starting = new Set(pluginIds); - const started = new Set(); - - logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`); - - const getInitStatus = () => { - let status = ''; - if (started.size > 0) { - status = `, newly initialized: ${joinIds(started)}`; - started.clear(); - } - if (starting.size > 0) { - status += `, still initializing: ${joinIds(starting)}`; - } - return status; - }; - - // Periodically log the initialization status with a fibonacci backoff - let interval = 1000; - let prevInterval = 0; - let timeout: NodeJS.Timeout | undefined; - const onTimeout = () => { - logger?.info(`Plugin initialization in progress${getInitStatus()}`); - - const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX); - prevInterval = interval; - interval = nextInterval; - - timeout = setTimeout(onTimeout, nextInterval); - }; - timeout = setTimeout(onTimeout, interval); - - return { - onPluginStarted(pluginId: string) { - starting.delete(pluginId); - started.add(pluginId); - }, - onPluginFailed(pluginId: string, error: Error) { - starting.delete(pluginId); - const status = - starting.size > 0 - ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` - : ''; - logger?.error( - `Plugin '${pluginId}' threw an error during startup${status}.`, - error, - ); - }, - onPermittedPluginFailure(pluginId: string, error: Error) { - starting.delete(pluginId); - logger?.error( - `Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`, - error, - ); - }, - onPluginModuleFailed(pluginId: string, moduleId: string, error: Error) { - const status = - starting.size > 0 - ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` - : ''; - logger?.error( - `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup${status}.`, - error, - ); - }, - onPermittedPluginModuleFailure( - pluginId: string, - moduleId: string, - error: Error, - ) { - logger?.error( - `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin module so startup will continue.`, - error, - ); - }, - onAllStarted() { - logger?.info(`Plugin initialization complete${getInitStatus()}`); - - if (timeout) { - clearTimeout(timeout); - timeout = undefined; - } - }, - }; -} diff --git a/packages/backend-app-api/src/wiring/createInitializationResultCollector.ts b/packages/backend-app-api/src/wiring/createInitializationResultCollector.ts new file mode 100644 index 0000000000..b9ee4c227d --- /dev/null +++ b/packages/backend-app-api/src/wiring/createInitializationResultCollector.ts @@ -0,0 +1,178 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RootLoggerService } from '@backstage/backend-plugin-api'; +import { AllowBootFailurePredicate } from './createAllowBootFailurePredicate'; +import { + BackendStartupResult, + PluginStartupResult, + ModuleStartupResult, +} from './types'; + +const LOGGER_INTERVAL_MAX = 60_000; + +function joinIds(ids: Iterable): string { + return [...ids].map(id => `'${id}'`).join(', '); +} + +export function createInitializationResultCollector(options: { + pluginIds: string[]; + logger?: RootLoggerService; + allowBootFailurePredicate: AllowBootFailurePredicate; +}): { + onPluginResult(pluginId: string, error?: Error): void; + onPluginModuleResult(pluginId: string, moduleId: string, error?: Error): void; + finalize(): BackendStartupResult; +} { + const logger = options.logger?.child({ type: 'initialization' }); + const beginAt = new Date(); + const starting = new Set(options.pluginIds.toSorted()); + const started = new Set(); + + let hasDisallowedFailures = false; + + const pluginResults: PluginStartupResult[] = []; + const moduleResultsByPlugin: Map = new Map( + Array.from(starting).map(pluginId => [pluginId, []]), + ); + + logger?.info(`Plugin initialization started: ${joinIds(starting)}`); + + const getInitStatus = () => { + let status = ''; + if (started.size > 0) { + status = `, newly initialized: ${joinIds(started)}`; + started.clear(); + } + if (starting.size > 0) { + status += `, still initializing: ${joinIds(starting)}`; + } + return status; + }; + + // Periodically log the initialization status with a fibonacci backoff + let interval = 1000; + let prevInterval = 0; + let timeout: NodeJS.Timeout | undefined; + const onTimeout = () => { + logger?.info(`Plugin initialization in progress${getInitStatus()}`); + + const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX); + prevInterval = interval; + interval = nextInterval; + + timeout = setTimeout(onTimeout, nextInterval); + }; + timeout = setTimeout(onTimeout, interval); + + return { + onPluginResult(pluginId: string, error?: Error) { + starting.delete(pluginId); + started.add(pluginId); + + const modules = moduleResultsByPlugin.get(pluginId); + if (!modules) { + throw new Error( + `Failed to push plugin result for nonexistent plugin '${pluginId}'`, + ); + } + + if (!error) { + pluginResults.push({ + pluginId, + resultAt: new Date(), + modules, + }); + } else { + const allowed = options.allowBootFailurePredicate(pluginId); + pluginResults.push({ + pluginId, + resultAt: new Date(), + modules, + failure: { + error, + allowed, + }, + }); + if (allowed) { + logger?.error( + `Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`, + error, + ); + } else { + hasDisallowedFailures = true; + const status = + starting.size > 0 + ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` + : ''; + logger?.error( + `Plugin '${pluginId}' threw an error during startup${status}.`, + error, + ); + } + } + }, + onPluginModuleResult(pluginId: string, moduleId: string, error?: Error) { + const moduleResults = moduleResultsByPlugin.get(pluginId); + if (!moduleResults) { + throw new Error( + `Failed to push module result for nonexistent plugin '${pluginId}'`, + ); + } + + if (!error) { + moduleResults.push({ moduleId, resultAt: new Date() }); + } else { + const allowed = options.allowBootFailurePredicate(pluginId, moduleId); + moduleResults.push({ + moduleId, + resultAt: new Date(), + failure: { error, allowed }, + }); + if (allowed) { + logger?.error( + `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin module so startup will continue.`, + error, + ); + } else { + hasDisallowedFailures = true; + const status = + starting.size > 0 + ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` + : ''; + logger?.error( + `Module ${moduleId} in Plugin '${pluginId}' threw an error during startup${status}.`, + error, + ); + } + } + }, + finalize() { + logger?.info(`Plugin initialization complete${getInitStatus()}`); + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + return { + beginAt, + resultAt: new Date(), + outcome: hasDisallowedFailures ? 'failure' : 'success', + plugins: pluginResults, + }; + }, + }; +} diff --git a/packages/backend-app-api/src/wiring/helpers.ts b/packages/backend-app-api/src/wiring/helpers.ts index ffb7e3b079..1be7f62da2 100644 --- a/packages/backend-app-api/src/wiring/helpers.ts +++ b/packages/backend-app-api/src/wiring/helpers.ts @@ -34,3 +34,22 @@ export function unwrapFeature( return feature; } + +/** @internal */ +export type DeepReadonly = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; +}; + +/** + * Deeply freezes an object by recursively freezing all of its properties. + * From https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 + + * https://stackoverflow.com/a/69656011 + */ +export function deepFreeze(obj: T) { + // Can cause: "Type instantiation is excessively deep and possibly infinite." + // @ts-expect-error + Object.values(obj).forEach( + value => Object.isFrozen(value) || deepFreeze(value), + ); + return Object.freeze(obj) as DeepReadonly; +} diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts index 00e4a9d4b4..cc084e5f34 100644 --- a/packages/backend-app-api/src/wiring/index.ts +++ b/packages/backend-app-api/src/wiring/index.ts @@ -14,5 +14,12 @@ * limitations under the License. */ -export type { Backend, CreateSpecializedBackendOptions } from './types'; +export type { + Backend, + CreateSpecializedBackendOptions, + BackendStartupResult, + PluginStartupResult, + ModuleStartupResult, +} from './types'; export { createSpecializedBackend } from './createSpecializedBackend'; +export { BackendStartupError } from './BackendStartupError'; diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index bd761ff3c8..5782021656 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -26,7 +26,7 @@ import { */ export interface Backend { add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void; - start(): Promise; + start(): Promise<{ result: BackendStartupResult }>; stop(): Promise; } @@ -43,3 +43,88 @@ export interface CreateSpecializedBackendOptions { export type ServiceOrExtensionPoint = | ExtensionPoint | ServiceRef; + +/** + * Result of a module startup attempt. + * @public + */ +export interface ModuleStartupResult { + /** + * The time the module startup was completed. + */ + resultAt: Date; + + /** + * The ID of the module. + */ + moduleId: string; + + /** + * If the startup failed, this contains information about the failure. + */ + failure?: { + /** + * The error that occurred during startup, if any. + */ + error: Error; + /** + * Whether the failure was allowed. + */ + allowed: boolean; + }; +} + +/** + * Result of a plugin startup attempt. + * @public + */ +export interface PluginStartupResult { + /** + * The time the plugin startup was completed. + */ + resultAt: Date; + /** + * If the startup failed, this contains information about the failure. + */ + failure?: { + /** + * The error that occurred during startup, if any. + */ + error: Error; + /** + * Whether the failure was allowed. + */ + allowed: boolean; + }; + /** + * The ID of the plugin. + */ + pluginId: string; + /** + * Results for all modules belonging to this plugin. + */ + modules: ModuleStartupResult[]; +} + +/** + * Result of a backend startup attempt. + * @public + */ +export interface BackendStartupResult { + /** + * The time the backend startup started. + */ + beginAt: Date; + /** + * The time the backend startup was completed. + */ + resultAt: Date; + /** + * Results for all plugins that were attempted to start. + */ + plugins: PluginStartupResult[]; + /** + * The outcome of the backend startup. + */ + outcome: 'success' | 'failure'; +} diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 62c478f6f7..cf9640969f 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,296 @@ # @backstage/backend-defaults +## 0.15.0 + +### Minor Changes + +- 6fc00e6: Added action filtering support with glob patterns and attribute constraints. + + The `ActionsService` now supports filtering actions based on configuration. This allows controlling which actions are exposed to consumers like the MCP backend. + + Configuration example: + + ```yaml + backend: + actions: + pluginSources: + - catalog + - scaffolder + filter: + include: + - id: 'catalog:*' + attributes: + destructive: false + - id: 'scaffolder:*' + exclude: + - id: '*:delete-*' + - attributes: + readOnly: false + ``` + + Filtering logic: + + - `include`: Rules for actions to include. Each rule can specify an `id` glob pattern and/or `attributes` constraints. An action must match at least one rule to be included. If no include rules are specified, all actions are included by default. + - `exclude`: Rules for actions to exclude. Takes precedence over include rules. + - Each rule combines `id` and `attributes` with AND logic (both must match if specified). + +- 27f9061: **BREAKING**: The constructor for `FetchUrlReader` is now private. If you have to construct an instance of it, please use `FetchUrlReader.fromConfig` instead. +- 27f9061: **BREAKING**: `coreServices.urlReader` now validates that redirect chains are subject to the allow list in `reading.allow` of your app config. If you were relying on redirects that pointed to URLs that were not allowlisted, you will now have to add those to your config as well. + + Example: + + ```diff + backend: + reading: + allow: + - host: example.com + + - host: storage-api.example.com + ``` + +### Patch Changes + +- 3afeab4: Implementing `readTree` for `GoogleGcsReader` +- c641c14: Wrap some of the action logic with `resolveSafeChildPath` and improve symlink handling when fetching remote and local files +- 7126bf2: Fixed a spelling mistake in root health service shutdown response. +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-app-api@1.4.1 + - @backstage/integration@1.19.2 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-node@0.10.8 + +## 0.15.0-next.2 + +### Minor Changes + +- 6fc00e6: Added action filtering support with glob patterns and attribute constraints. + + The `ActionsService` now supports filtering actions based on configuration. This allows controlling which actions are exposed to consumers like the MCP backend. + + Configuration example: + + ```yaml + backend: + actions: + pluginSources: + - catalog + - scaffolder + filter: + include: + - id: 'catalog:*' + attributes: + destructive: false + - id: 'scaffolder:*' + exclude: + - id: '*:delete-*' + - attributes: + readOnly: false + ``` + + Filtering logic: + + - `include`: Rules for actions to include. Each rule can specify an `id` glob pattern and/or `attributes` constraints. An action must match at least one rule to be included. If no include rules are specified, all actions are included by default. + - `exclude`: Rules for actions to exclude. Takes precedence over include rules. + - Each rule combines `id` and `attributes` with AND logic (both must match if specified). + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + +## 0.14.1-next.1 + +### Patch Changes + +- 3afeab4: Implementing `readTree` for `GoogleGcsReader` +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## 0.14.1-next.0 + +### Patch Changes + +- 7126bf2: Fixed a spelling mistake in root health service shutdown response. +- Updated dependencies + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-dev-utils@0.1.6 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/integration@1.19.0 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + +## 0.14.0 + +### Minor Changes + +- fa43826: Move `better-sqlite3` from dependencies to peer dependencies +- 2bc4e02: **BREAKING** The correct configuration options for Valkey are now being used. + + These changes are **required** to `app-config.yaml`: + + ```diff + backend: + cache: + store: valkey + connection: ... + client: + - namespace: 'my-app' + - keyPrefixSeparator: ':' + + keyPrefix: 'my-app:' + - clearBatchSize: 1000 + - useUnlink: false + ``` + + In comparison to Redis, Valkey requires the full `keyPrefix` including the separator to be specified instead of separate `namespace` and `keyPrefixSeparator` options. Also, Valkey does not support the `clearBatchSize` and `useUnlink` options. + +### Patch Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- aa79251: build(deps): bump `node-forge` from 1.3.1 to 1.3.2 +- f96edff: Allow configuration of the `referrerPolicy` +- fb029b6: Updated luxon types +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- 847a330: Fix for `jose` types +- 25b560e: Internal change to support new versions of the `logform` library +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. +- 3016a79: Updated dependency `@types/archiver` to `^7.0.0`. +- 42db6a6: Don't warn when parsing `storeOptions` for `memory` cache +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/integration@1.19.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config-loader@1.10.7 + - @backstage/backend-dev-utils@0.1.6 + +## 0.14.0-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- aa79251: build(deps): bump `node-forge` from 1.3.1 to 1.3.2 +- f96edff: Allow configuration of the `referrerPolicy` +- fb029b6: Updated luxon types +- 847a330: Fix for `jose` types +- 25b560e: Internal change to support new versions of the `logform` library +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. +- 3016a79: Updated dependency `@types/archiver` to `^7.0.0`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/backend-dev-utils@0.1.6-next.0 + - @backstage/backend-app-api@1.4.0-next.1 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + +## 0.14.0-next.0 + +### Minor Changes + +- fa43826: Move `better-sqlite3` from dependencies to peer dependencies + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-app-api@1.4.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/backend-dev-utils@0.1.5 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.13.1 + +### Patch Changes + +- 9bcfa77: Adjusted the log line wording of task worker starting +- 91ab2eb: Fix a bug in the Gitlab URL reader where `search` did not handle multiple globs +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- b2f6a5a: Fix #31348 issue where BitbucketUrlReader ignored provided token and instead always used integration credentials +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/backend-app-api@1.3.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/integration-aws-node@0.1.19 + - @backstage/plugin-permission-node@0.10.6 + +## 0.13.1-next.1 + +### Patch Changes + +- 91ab2eb: Fix a bug in the Gitlab URL reader where `search` did not handle multiple globs +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-app-api@1.3.0-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + ## 0.13.1-next.0 ### Patch Changes diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index f0632a37bb..5a651ef6cd 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -154,6 +154,104 @@ export interface Config { * List of plugin sources to load actions from. */ pluginSources?: string[]; + + /** + * Filter configuration for actions. Allows controlling which actions + * are exposed to consumers based on patterns and attributes. + */ + filter?: { + /** + * Rules for actions to include. An action must match at least one rule to be included. + * Each rule can specify an id pattern and/or attribute constraints. + * If no include rules are specified, all actions are included by default. + * + * @example + * ```yaml + * include: + * - id: 'catalog:*' + * attributes: + * destructive: false + * - id: 'scaffolder:*' + * ``` + */ + include?: Array<{ + /** + * Glob pattern for action IDs to match. + * Action IDs have the format `{pluginId}:{actionName}`. + * @example 'catalog:*' + */ + id?: string; + + /** + * Attribute constraints. All specified attributes must match. + * Actions are compared against their resolved attributes (with defaults applied). + */ + attributes?: { + /** + * If specified, only match actions where destructive matches this value. + * Actions default to destructive: true if not explicitly set. + */ + destructive?: boolean; + + /** + * If specified, only match actions where readOnly matches this value. + * Actions default to readOnly: false if not explicitly set. + */ + readOnly?: boolean; + + /** + * If specified, only match actions where idempotent matches this value. + * Actions default to idempotent: false if not explicitly set. + */ + idempotent?: boolean; + }; + }>; + + /** + * Rules for actions to exclude. Exclusions take precedence over inclusions. + * Each rule can specify an id pattern and/or attribute constraints. + * + * @example + * ```yaml + * exclude: + * - id: '*:delete-*' + * - attributes: + * readOnly: false + * ``` + */ + exclude?: Array<{ + /** + * Glob pattern for action IDs to match. + * Action IDs have the format `{pluginId}:{actionName}`. + * @example '*:delete-*' + */ + id?: string; + + /** + * Attribute constraints. All specified attributes must match. + * Actions are compared against their resolved attributes (with defaults applied). + */ + attributes?: { + /** + * If specified, only match actions where destructive matches this value. + * Actions default to destructive: true if not explicitly set. + */ + destructive?: boolean; + + /** + * If specified, only match actions where readOnly matches this value. + * Actions default to readOnly: false if not explicitly set. + */ + readOnly?: boolean; + + /** + * If specified, only match actions where idempotent matches this value. + * Actions default to idempotent: false if not explicitly set. + */ + idempotent?: boolean; + }; + }>; + }; }; /** @@ -710,27 +808,9 @@ export interface Config { */ client?: { /** - * Namespace for the current instance. + * Namespace and separator used for prefixing keys. */ - namespace?: string; - /** - * Separator to use between namespace and key. - */ - keyPrefixSeparator?: string; - /** - * Number of keys to delete in a single batch. - */ - clearBatchSize?: number; - /** - * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions. - */ - useUnlink?: boolean; - /** - * Whether to allow clearing all keys when no namespace is set. - * If set to true and no namespace is set, iterate() will return all keys. - * Defaults to `false`. - */ - noNamespaceAffectsAll?: boolean; + keyPrefix?: string; }; /** * An optional Valkey cluster (redis cluster under the hood) configuration. @@ -993,6 +1073,13 @@ export interface Config { */ csp?: { [policyId: string]: string[] | false }; + /** + * Referrer Policy options + */ + referrer?: { + policy: string[]; + }; + /** * Options for the health check service and endpoint. */ diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 4884c9d30d..cb75e9ce5f 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.13.1-next.0", + "version": "0.15.0", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" @@ -156,13 +156,12 @@ "@types/express": "^4.17.6", "archiver": "^7.0.0", "base64-stream": "^1.0.0", - "better-sqlite3": "^12.0.0", "compression": "^1.7.4", "concat-stream": "^2.0.0", "cookie": "^0.7.0", "cors": "^2.8.5", "cron": "^3.0.0", - "express": "^4.17.1", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "express-rate-limit": "^7.5.0", "fs-extra": "^11.2.0", @@ -179,7 +178,7 @@ "minimatch": "^9.0.0", "mysql2": "^3.0.0", "node-fetch": "^2.7.0", - "node-forge": "^1.3.1", + "node-forge": "^1.3.2", "p-limit": "^3.1.0", "path-to-regexp": "^8.0.0", "pg": "^8.11.3", @@ -196,7 +195,7 @@ "yauzl": "^3.0.0", "yn": "^4.0.0", "zod": "^3.22.4", - "zod-to-json-schema": "^3.20.4" + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@aws-sdk/util-stream-node": "^3.350.0", @@ -204,7 +203,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@google-cloud/cloud-sql-connector": "^1.4.0", - "@types/archiver": "^6.0.0", + "@types/archiver": "^7.0.0", "@types/base64-stream": "^1.0.2", "@types/compression": "^1.7.5", "@types/concat-stream": "^2.0.0", @@ -214,6 +213,8 @@ "@types/pg-format": "^1.0.5", "@types/yauzl": "^2.10.0", "aws-sdk-client-mock": "^4.0.0", + "better-sqlite3": "^12.0.0", + "get-port": "^5.1.1", "http-errors": "^2.0.0", "msw": "^1.0.0", "node-mocks-http": "^1.0.0", @@ -221,11 +222,15 @@ "wait-for-expect": "^3.0.2" }, "peerDependencies": { - "@google-cloud/cloud-sql-connector": "^1.4.0" + "@google-cloud/cloud-sql-connector": "^1.4.0", + "better-sqlite3": "^12.0.0" }, "peerDependenciesMeta": { "@google-cloud/cloud-sql-connector": { "optional": true + }, + "better-sqlite3": { + "optional": true } }, "configSchema": "config.d.ts" diff --git a/packages/backend-defaults/report-alpha.api.md b/packages/backend-defaults/report-alpha.api.md index 58277b5e1e..c40517863a 100644 --- a/packages/backend-defaults/report-alpha.api.md +++ b/packages/backend-defaults/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { RootSystemMetadataService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -21,5 +22,12 @@ export const actionsServiceFactory: ServiceFactory< 'singleton' >; +// @alpha +export const rootSystemMetadataServiceFactory: ServiceFactory< + RootSystemMetadataService, + 'root', + 'singleton' +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/report-urlReader.api.md b/packages/backend-defaults/report-urlReader.api.md index 039d51808d..c162564747 100644 --- a/packages/backend-defaults/report-urlReader.api.md +++ b/packages/backend-defaults/report-urlReader.api.md @@ -12,6 +12,7 @@ import { AzureIntegration } from '@backstage/integration'; import { BitbucketCloudIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; +import { Config } from '@backstage/config'; import { GerritIntegration } from '@backstage/integration'; import { GiteaIntegration } from '@backstage/integration'; import { GithubCredentialsProvider } from '@backstage/integration'; @@ -225,6 +226,8 @@ export class BitbucketUrlReader implements UrlReaderService { export class FetchUrlReader implements UrlReaderService { static factory: ReaderFactory; // (undocumented) + static fromConfig(config: Config): FetchUrlReader; + // (undocumented) read(url: string): Promise; // (undocumented) readTree(): Promise; diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 4b201fded3..9f8116a996 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -39,6 +39,7 @@ import { actionsRegistryServiceFactory, actionsServiceFactory, } from '@backstage/backend-defaults/alpha'; +import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory'; export const defaultServiceFactories = [ auditorServiceFactory, @@ -65,6 +66,9 @@ export const defaultServiceFactories = [ // alpha services actionsRegistryServiceFactory, actionsServiceFactory, + + // Unexported alpha services kept around for compatibility reasons + instanceMetadataServiceFactory, ]; /** diff --git a/packages/backend-defaults/src/alpha/entrypoints/actions/DefaultActionsService.ts b/packages/backend-defaults/src/alpha/entrypoints/actions/DefaultActionsService.ts index 155bbf8b35..30aeb55227 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actions/DefaultActionsService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actions/DefaultActionsService.ts @@ -26,6 +26,8 @@ import { ActionsService, ActionsServiceAction, } from '@backstage/backend-plugin-api/alpha'; +import { Minimatch } from 'minimatch'; +import { Config } from '@backstage/config'; export class DefaultActionsService implements ActionsService { private readonly discovery: DiscoveryService; @@ -86,7 +88,7 @@ export class DefaultActionsService implements ActionsService { }), ); - return { actions: remoteActionsList.flat() }; + return { actions: this.applyFilters(remoteActionsList.flat()) }; } async invoke(opts: { @@ -148,4 +150,106 @@ export class DefaultActionsService implements ActionsService { } return id.substring(0, colonIndex); } + + private applyFilters( + actions: ActionsServiceAction[], + ): ActionsServiceAction[] { + const filterConfig = this.config.getOptionalConfig( + 'backend.actions.filter', + ); + + if (!filterConfig) { + return actions; + } + + const includeRules = this.parseFilterRules( + filterConfig.getOptionalConfigArray('include') ?? [], + ); + const excludeRules = this.parseFilterRules( + filterConfig.getOptionalConfigArray('exclude') ?? [], + ); + + return actions.filter(action => { + const excluded = excludeRules.some(rule => + this.matchesRule(action, rule), + ); + + if (excluded) { + return false; + } + + // If no include rules, include by default + if (includeRules.length === 0) { + return true; + } + + // Must match at least one include rule + return includeRules.some(rule => this.matchesRule(action, rule)); + }); + } + + private parseFilterRules(configArray: Array): Array<{ + idMatcher?: Minimatch; + attributes?: Partial< + Record<'destructive' | 'readOnly' | 'idempotent', boolean> + >; + }> { + return configArray.map(ruleConfig => { + const idPattern = ruleConfig.getOptionalString('id'); + const attributesConfig = ruleConfig.getOptionalConfig('attributes'); + + const rule: { + idMatcher?: Minimatch; + attributes?: Partial< + Record<'destructive' | 'readOnly' | 'idempotent', boolean> + >; + } = {}; + + if (idPattern) { + rule.idMatcher = new Minimatch(idPattern); + } + + if (attributesConfig) { + rule.attributes = {}; + for (const key of ['destructive', 'readOnly', 'idempotent'] as const) { + const value = attributesConfig.getOptionalBoolean(key); + if (value !== undefined) { + rule.attributes[key] = value; + } + } + } + + return rule; + }); + } + + private matchesRule( + action: ActionsServiceAction, + rule: { + idMatcher?: Minimatch; + attributes?: Partial< + Record<'destructive' | 'readOnly' | 'idempotent', boolean> + >; + }, + ): boolean { + // If id pattern is specified, it must match + if (rule.idMatcher && !rule.idMatcher.match(action.id)) { + return false; + } + + // If attributes are specified, all must match + if (rule.attributes) { + for (const [key, value] of Object.entries(rule.attributes)) { + if ( + action.attributes[ + key as 'destructive' | 'readOnly' | 'idempotent' + ] !== value + ) { + return false; + } + } + } + + return true; + } } diff --git a/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts index af2fcb0c12..dbce47bea4 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts @@ -115,6 +115,476 @@ describe('actionsServiceFactory', () => { expect(mockActionsListEndpoint).toHaveBeenCalledTimes(1); expect(mockNotFoundActionsListEndpoint).toHaveBeenCalledTimes(1); }); + + it('should filter actions based on include patterns', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:get-entity', + name: 'get-entity', + }, + { + ...mockActionsDefinition, + id: 'my-plugin:delete-entity', + name: 'delete-entity', + }, + { + ...mockActionsDefinition, + id: 'other-plugin:get-thing', + name: 'get-thing', + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => + res( + ctx.json({ + actions: multipleActions.filter(a => + a.id.startsWith('my-plugin:'), + ), + }), + ), + ), + rest.get( + 'http://localhost:0/api/other-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => + res( + ctx.json({ + actions: multipleActions.filter(a => + a.id.startsWith('other-plugin:'), + ), + }), + ), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin', 'other-plugin'], + filter: { + include: [{ id: 'my-plugin:*' }], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + expect(actions.map(a => a.id)).toEqual([ + 'my-plugin:get-entity', + 'my-plugin:delete-entity', + ]); + }); + + it('should filter actions based on exclude patterns', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:get-entity', + name: 'get-entity', + }, + { + ...mockActionsDefinition, + id: 'my-plugin:delete-entity', + name: 'delete-entity', + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => res(ctx.json({ actions: multipleActions })), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin'], + filter: { + exclude: [{ id: '*:delete-*' }], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + expect(actions.map(a => a.id)).toEqual(['my-plugin:get-entity']); + }); + + it('should have exclude take precedence over include', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:get-entity', + name: 'get-entity', + }, + { + ...mockActionsDefinition, + id: 'my-plugin:delete-entity', + name: 'delete-entity', + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => res(ctx.json({ actions: multipleActions })), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin'], + filter: { + include: [{ id: 'my-plugin:*' }], + exclude: [{ id: 'my-plugin:delete-entity' }], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + expect(actions.map(a => a.id)).toEqual(['my-plugin:get-entity']); + }); + + it('should always apply exclude rules even when action matches include', async () => { + // This tests that exclude is checked FIRST and always wins, + // regardless of whether the action would match an include rule + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:safe-action', + name: 'safe-action', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + }, + { + ...mockActionsDefinition, + id: 'my-plugin:dangerous-action', + name: 'dangerous-action', + attributes: { + destructive: true, + readOnly: false, + idempotent: false, + }, + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => res(ctx.json({ actions: multipleActions })), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin'], + filter: { + // Include all my-plugin actions + include: [{ id: 'my-plugin:*' }], + // But exclude any destructive ones + exclude: [{ attributes: { destructive: true } }], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + // dangerous-action matches include (my-plugin:*) but is excluded due to destructive: true + expect(actions.map(a => a.id)).toEqual(['my-plugin:safe-action']); + }); + + it('should filter actions based on attribute constraints', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:read-action', + name: 'read-action', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + }, + { + ...mockActionsDefinition, + id: 'my-plugin:write-action', + name: 'write-action', + attributes: { + destructive: true, + readOnly: false, + idempotent: false, + }, + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => res(ctx.json({ actions: multipleActions })), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin'], + filter: { + include: [ + { + attributes: { + readOnly: true, + }, + }, + ], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + expect(actions.map(a => a.id)).toEqual(['my-plugin:read-action']); + }); + + it('should combine pattern and attribute filtering with AND logic', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:safe-read', + name: 'safe-read', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + }, + { + ...mockActionsDefinition, + id: 'my-plugin:dangerous-read', + name: 'dangerous-read', + attributes: { + destructive: true, + readOnly: true, + idempotent: false, + }, + }, + { + ...mockActionsDefinition, + id: 'other-plugin:safe-read', + name: 'safe-read', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => + res( + ctx.json({ + actions: multipleActions.filter(a => + a.id.startsWith('my-plugin:'), + ), + }), + ), + ), + rest.get( + 'http://localhost:0/api/other-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => + res( + ctx.json({ + actions: multipleActions.filter(a => + a.id.startsWith('other-plugin:'), + ), + }), + ), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin', 'other-plugin'], + filter: { + include: [ + { + id: 'my-plugin:*', + attributes: { + destructive: false, + }, + }, + ], + }, + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + // Only my-plugin:safe-read matches: my-plugin:* pattern AND destructive: false + expect(actions.map(a => a.id)).toEqual(['my-plugin:safe-read']); + }); + + it('should return all actions when no filter config is provided', async () => { + const multipleActions: ActionsServiceAction[] = [ + { + ...mockActionsDefinition, + id: 'my-plugin:action-one', + name: 'action-one', + }, + { + ...mockActionsDefinition, + id: 'my-plugin:action-two', + name: 'action-two', + }, + ]; + + server.use( + rest.get( + 'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions', + (_req, res, ctx) => res(ctx.json({ actions: multipleActions })), + ), + ); + + const subject = await ServiceFactoryTester.from(actionsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['my-plugin'], + // No filter config + }, + }, + }, + }), + actionsServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.discovery.factory(), + actionsRegistryServiceFactory, + ], + }).getSubject(); + + const { actions } = await subject.list({ + credentials: mockCredentials.service('user:default/mock'), + }); + + expect(actions.map(a => a.id)).toEqual([ + 'my-plugin:action-one', + 'my-plugin:action-two', + ]); + }); }); describe('invoke', () => { diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.test.ts new file mode 100644 index 0000000000..73de60efdd --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + instanceMetadataServiceFactory, + instanceMetadataServiceRef, +} from './instanceMetadataServiceFactory'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import Router from 'express-promise-router'; +import request from 'supertest'; + +describe('instanceMetadataServiceFactory', () => { + it('should create an instance metadata service', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: instanceMetadataServiceRef, + httpRouter: coreServices.httpRouter, + }, + async init({ instanceMetadata, httpRouter }) { + const router = Router(); + router.get('/instance-metadata', async (_, res) => { + const metadata = await instanceMetadata.getInstalledFeatures(); + res.json(metadata); + }); + httpRouter.use(router); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [pluginSubject, instanceMetadataServiceFactory], + }); + + const response = await request(server) + .get('/api/my-plugin/instance-metadata') + .expect(200); + expect(response.body).toEqual([{ pluginId: 'my-plugin', type: 'plugin' }]); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts new file mode 100644 index 0000000000..eb50037511 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + createServiceRef, +} from '@backstage/backend-plugin-api'; + +/* + * NOTE(freben): We have moved over to coreServices.rootInstanceMetadata. This + * old alpha implementation is kept around for a little longer for backward + * compatibility reasons. + */ + +/** + * @internal + */ +export type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + +/** + * @internal + */ +export interface InstanceMetadataService { + getInstalledFeatures: () => BackendFeatureMeta[]; +} + +/** + * @internal + */ +export const instanceMetadataServiceRef = + createServiceRef({ + id: 'core.instanceMetadata', + }); + +/** + * @internal + */ +export const instanceMetadataServiceFactory = createServiceFactory({ + service: instanceMetadataServiceRef, + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + factory: async ({ instanceMetadata }) => { + const plugins = await instanceMetadata.getInstalledPlugins(); + const features: BackendFeatureMeta[] = []; + for (const plugin of plugins) { + features.push({ + type: 'plugin' as const, + pluginId: plugin.pluginId, + }); + for (const module of plugin.modules) { + features.push({ + type: 'module' as const, + pluginId: plugin.pluginId, + moduleId: module.moduleId, + }); + } + } + const service: InstanceMetadataService = { + getInstalledFeatures: () => features, + }; + + return service; + }, +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/RootSystemMetadataService.test.ts b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/RootSystemMetadataService.test.ts new file mode 100644 index 0000000000..13da5ae3c0 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/RootSystemMetadataService.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + coreServices, + createBackendPlugin, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import Router from 'express-promise-router'; +import request from 'supertest'; +import { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory'; +import { rootSystemMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; + +describe('SystemMetadataService', () => { + describe('returns plugins from config', () => { + const testPlugin = createBackendPlugin({ + pluginId: 'test-plugin', + register(reg) { + reg.registerInit({ + deps: { + systemMetadata: rootSystemMetadataServiceRef, + rootHttpRouter: coreServices.rootHttpRouter, + }, + init: async ({ systemMetadata, rootHttpRouter }) => { + const router = Router(); + router.use('/plugins', async (_, res) => { + res.json(await systemMetadata.getInstalledPlugins()); + }); + rootHttpRouter.use('/systemMetadata', router); + }, + }); + }, + }); + + it('should list all known instances', async () => { + const { server } = await startTestBackend({ + features: [ + testPlugin, + rootSystemMetadataServiceFactory, + mockServices.rootConfig.factory({ + data: { + backend: { + listen: { + port: 0, + }, + baseUrl: `http://localhost:0`, + }, + }, + }), + ], + }); + + const instanceResponse = await request(server).get( + `/systemMetadata/plugins`, + ); + + expect(instanceResponse.status).toBe(200); + expect(instanceResponse.body).toMatchObject([ + { + pluginId: 'test-plugin', + }, + ]); + }); + + it('should react to config updates', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + listen: { + port: 0, + }, + baseUrl: `http://localhost:0`, + }, + }, + }); + const configFactory = createServiceFactory({ + service: coreServices.rootConfig, + deps: {}, + factory: () => config, + }); + const { server } = await startTestBackend({ + features: [testPlugin, configFactory, rootSystemMetadataServiceFactory], + }); + + const initialResponse = await request(server).get( + `/systemMetadata/plugins`, + ); + + expect(initialResponse.status).toBe(200); + expect(initialResponse.body).toMatchObject([ + { + pluginId: 'test-plugin', + }, + ]); + + config.update({ + data: { + backend: { + listen: { + port: 0, + }, + baseUrl: `http://localhost:0`, + }, + discovery: { + endpoints: [ + { + target: `http://test.internal`, + plugins: ['your-new-plugin'], + }, + ], + }, + }, + }); + + const responseAfterUpdate = await request(server).get( + `/systemMetadata/plugins`, + ); + + expect(responseAfterUpdate.status).toBe(200); + expect(responseAfterUpdate.body).toMatchObject([ + { + pluginId: 'your-new-plugin', + }, + { + pluginId: 'test-plugin', + }, + ]); + }); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/index.ts b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/index.ts new file mode 100644 index 0000000000..cff4eccdbe --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory'; +export { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService'; diff --git a/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/lib/DefaultRootSystemMetadataService.ts b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/lib/DefaultRootSystemMetadataService.ts new file mode 100644 index 0000000000..edbffdfb0e --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/lib/DefaultRootSystemMetadataService.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + RootConfigService, + RootInstanceMetadataService, +} from '@backstage/backend-plugin-api'; +import { + RootSystemMetadataService, + RootSystemMetadataServicePluginInfo, +} from '@backstage/backend-plugin-api/alpha'; +import { getEndpoints } from '../../../../entrypoints/discovery/parsing'; +import { Config } from '@backstage/config'; + +function getPlugins(config: Config): string[] { + const endpoints = getEndpoints(config); + return Array.from(new Set(endpoints.flatMap(endpoint => endpoint.plugins))); +} + +/** + * @alpha + */ +export class DefaultRootSystemMetadataService + implements RootSystemMetadataService +{ + #plugins: string[]; + #instanceMetadata: RootInstanceMetadataService; + constructor(options: { + logger: LoggerService; + config: RootConfigService; + instanceMetadata: RootInstanceMetadataService; + }) { + const { config } = options; + this.#plugins = getPlugins(config); + config.subscribe?.(() => { + this.#plugins = getPlugins(config); + }); + this.#instanceMetadata = options.instanceMetadata; + } + + public static create(pluginEnv: { + logger: LoggerService; + config: RootConfigService; + instanceMetadata: RootInstanceMetadataService; + }) { + return new DefaultRootSystemMetadataService(pluginEnv); + } + + public async getInstalledPlugins(): Promise< + RootSystemMetadataServicePluginInfo[] + > { + const plugins = this.#plugins.map(pluginId => ({ pluginId })); + + for (const plugin of await this.#instanceMetadata.getInstalledPlugins()) { + plugins.push({ pluginId: plugin.pluginId }); + } + + return plugins; + } +} diff --git a/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/rootSystemMetadataServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/rootSystemMetadataServiceFactory.ts new file mode 100644 index 0000000000..5ee93d03d5 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/rootSystemMetadata/rootSystemMetadataServiceFactory.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, +} from '@backstage/backend-plugin-api'; +import { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService'; +import { rootSystemMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; + +/** + * Metadata about an entire Backstage system, a collection of Backstage instances. + * + * @alpha + */ +export const rootSystemMetadataServiceFactory = createServiceFactory({ + service: rootSystemMetadataServiceRef, + deps: { + logger: coreServices.rootLogger, + config: coreServices.rootConfig, + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async factory({ logger, config, instanceMetadata }) { + return DefaultRootSystemMetadataService.create({ + logger, + config, + instanceMetadata, + }); + }, +}); diff --git a/packages/backend-defaults/src/alpha/index.ts b/packages/backend-defaults/src/alpha/index.ts index 13bb439acd..c17e0e71cb 100644 --- a/packages/backend-defaults/src/alpha/index.ts +++ b/packages/backend-defaults/src/alpha/index.ts @@ -13,5 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry'; export { actionsServiceFactory } from './entrypoints/actions'; +export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata'; diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts index e6a7bbcfc6..562086a07f 100644 --- a/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts @@ -102,7 +102,7 @@ export class DatabasePluginKeySource implements PluginKeySource { await this.keyStore.addKey({ id: kid, - key: publicKey as InternalKey, + key: publicKey as unknown as InternalKey, expiresAt: new Date( Date.now() + this.keyDurationSeconds * diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index a308caa222..323ca881c5 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -336,11 +336,17 @@ describe('CacheManager store options', () => { it('correctly applies namespace configuration to redis and valkey stores', () => { const testCases = [ - { store: 'redis', namespace: 'test1', separator: ':' }, - { store: 'valkey', namespace: 'test2', separator: '!' }, + { + store: 'redis', + client: { + namespace: 'my-app', + keyPrefixSeparator: ':', + }, + }, + { store: 'valkey', client: { keyPrefix: 'my-app:' } }, ]; - testCases.forEach(({ store, namespace, separator }) => { + testCases.forEach(({ store, client }) => { const manager = CacheManager.fromConfig( mockServices.rootConfig({ data: { @@ -349,10 +355,7 @@ describe('CacheManager store options', () => { store, connection: 'redis://localhost:6379', [store]: { - client: { - namespace, - keyPrefixSeparator: separator, - }, + client, }, }, }, @@ -364,16 +367,16 @@ describe('CacheManager store options', () => { if (store === 'redis') { // eslint-disable-next-line jest/no-conditional-expect - expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { - namespace, - keyPrefixSeparator: separator, - }); + expect(KeyvRedis).toHaveBeenCalledWith( + 'redis://localhost:6379', + client, + ); } else if (store === 'valkey') { // eslint-disable-next-line jest/no-conditional-expect - expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', { - namespace, - keyPrefixSeparator: separator, - }); + expect(KeyvValkey).toHaveBeenCalledWith( + 'redis://localhost:6379', + client, + ); } }); }); @@ -408,21 +411,31 @@ describe('CacheManager store options', () => { expect(result).toBe('testPlugin'); }); - it('returns pluginId when store options have no namespace', () => { - const storeOptions = { - client: { - keyPrefixSeparator: ':', - }, - }; - const result = (CacheManager as any).constructNamespace( - 'testPlugin', - storeOptions, - ); - expect(result).toBe('testPlugin'); - }); + it.each([ + { + type: 'redis', + field: 'namespace', + client: { keyPrefixSeparator: ':' }, + }, + { type: 'valkey', field: 'keyPrefix', client: {} }, + ])( + 'returns pluginId when store options have no $field for $type', + ({ type, client }) => { + const storeOptions = { + type, + client, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }, + ); - it('combines namespace and pluginId with default separator', () => { + it('combines namespace and pluginId with default separator for redis', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', keyPrefixSeparator: ':', @@ -435,8 +448,9 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); - it('combines namespace and pluginId with custom separator', () => { + it('combines namespace and pluginId with custom separator for redis', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', keyPrefixSeparator: '-', @@ -449,8 +463,9 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app-testPlugin'); }); - it('uses default separator when keyPrefixSeparator is not provided', () => { + it('uses default separator when keyPrefixSeparator is not provided for redis', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', }, @@ -462,6 +477,20 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); + it('uses keyPrefix for valkey', () => { + const storeOptions = { + type: 'valkey', + client: { + keyPrefix: 'my-app:', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + it('handles empty namespace by falling back to pluginId', () => { const storeOptions = { client: { diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 06b2b0c57b..38c0c761ed 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -29,6 +29,7 @@ import { RedisCacheStoreOptions, InfinispanClientBehaviorOptions, InfinispanServerConfig, + ValkeyCacheStoreOptions, } from './types'; import { InfinispanOptionsMapper } from './providers/infinispan/InfinispanOptionsMapper'; import { durationToMilliseconds } from '@backstage/types'; @@ -134,43 +135,38 @@ export class CacheManager { ): CacheStoreOptions | undefined { const storeConfigPath = `backend.cache.${store}`; - if (!config.has(storeConfigPath)) { + if (store !== 'memory' && !config.has(storeConfigPath)) { logger?.warn( `No configuration found for cache store '${store}' at '${storeConfigPath}'.`, ); } - if (store === 'redis' || store === 'valkey') { - return CacheManager.parseRedisOptions( - store, - storeConfigPath, - config, - logger, - ); + switch (store) { + case 'redis': + return CacheManager.parseRedisOptions(storeConfigPath, config, logger); + case 'valkey': + return CacheManager.parseValkeyOptions(storeConfigPath, config, logger); + case 'infinispan': + return InfinispanOptionsMapper.parseInfinispanOptions( + storeConfigPath, + config, + logger, + ); + default: + return undefined; } - - if (store === 'infinispan') { - return InfinispanOptionsMapper.parseInfinispanOptions( - storeConfigPath, - config, - logger, - ); - } - - return undefined; } /** * Parse Redis-specific options from configuration. */ private static parseRedisOptions( - store: string, storeConfigPath: string, config: RootConfigService, logger?: LoggerService, ): RedisCacheStoreOptions { const redisOptions: RedisCacheStoreOptions = { - type: store as 'redis' | 'valkey', + type: 'redis', }; const redisConfig = @@ -213,6 +209,51 @@ export class CacheManager { return redisOptions; } + /** + * Parse Valkey-specific options from configuration. + */ + private static parseValkeyOptions( + storeConfigPath: string, + config: RootConfigService, + logger?: LoggerService, + ): ValkeyCacheStoreOptions { + const valkeyOptions: ValkeyCacheStoreOptions = { + type: 'valkey', + }; + + const valkeyConfig = + config.getOptionalConfig(storeConfigPath) ?? new ConfigReader({}); + + valkeyOptions.client = { + keyPrefix: valkeyConfig.getOptionalString('client.keyPrefix'), + }; + + if (valkeyConfig.has('cluster')) { + const clusterConfig = valkeyConfig.getConfig('cluster'); + + if (!clusterConfig.has('rootNodes')) { + logger?.warn( + `Valkey cluster config has no 'rootNodes' key, defaulting to non-clustered mode`, + ); + return valkeyOptions; + } + + valkeyOptions.cluster = { + rootNodes: clusterConfig.get('rootNodes'), + defaults: clusterConfig.getOptional('defaults'), + minimizeConnections: clusterConfig.getOptionalBoolean( + 'minimizeConnections', + ), + useReplicas: clusterConfig.getOptionalBoolean('useReplicas'), + maxCommandRedirections: clusterConfig.getOptionalNumber( + 'maxCommandRedirections', + ), + }; + } + + return valkeyOptions; + } + /** * Construct the full namespace based on the options and pluginId. * @@ -222,13 +263,23 @@ export class CacheManager { */ private static constructNamespace( pluginId: string, - storeOptions: RedisCacheStoreOptions | undefined, + storeOptions: RedisCacheStoreOptions | ValkeyCacheStoreOptions | undefined, ): string { - const prefix = storeOptions?.client?.namespace - ? `${storeOptions.client.namespace}${ - storeOptions.client.keyPrefixSeparator ?? ':' - }` - : ''; + let prefix: string; + switch (storeOptions?.type) { + case 'redis': + prefix = storeOptions?.client?.namespace + ? `${storeOptions.client.namespace}${ + storeOptions.client.keyPrefixSeparator ?? ':' + }` + : ''; + break; + case 'valkey': + prefix = storeOptions.client?.keyPrefix ?? ''; + break; + default: + prefix = ''; + } return `${prefix}${pluginId}`; } @@ -317,7 +368,9 @@ export class CacheManager { private createValkeyStoreFactory(): StoreFactory { const KeyvValkey = require('@keyv/valkey').default; - const { createCluster } = require('@keyv/valkey'); + // `@keyv/valkey` doesn't export a `createCluster` function, but is compatible with the one from `@keyv/redis` + // See https://keyv.org/docs/storage-adapters/valkey + const { createCluster } = require('@keyv/redis'); const stores: Record = {}; return (pluginId, defaultTtl) => { @@ -327,9 +380,7 @@ export class CacheManager { ); } if (!stores[pluginId]) { - const valkeyOptions = this.storeOptions?.client || { - keyPrefixSeparator: ':', - }; + const valkeyOptions = this.storeOptions?.client; if (this.storeOptions?.cluster) { // Create a Valkey cluster (Redis cluster under the hood) const cluster = createCluster(this.storeOptions?.cluster); diff --git a/packages/backend-defaults/src/entrypoints/cache/types.ts b/packages/backend-defaults/src/entrypoints/cache/types.ts index 522ab2ac3e..60e32e8618 100644 --- a/packages/backend-defaults/src/entrypoints/cache/types.ts +++ b/packages/backend-defaults/src/entrypoints/cache/types.ts @@ -17,6 +17,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { HumanDuration, durationToMilliseconds } from '@backstage/types'; import { RedisClusterOptions, KeyvRedisOptions } from '@keyv/redis'; +import { KeyvValkeyOptions } from '@keyv/valkey'; /** * Options for Redis cache store. @@ -24,11 +25,22 @@ import { RedisClusterOptions, KeyvRedisOptions } from '@keyv/redis'; * @public */ export type RedisCacheStoreOptions = { - type: 'redis' | 'valkey'; + type: 'redis'; client?: KeyvRedisOptions; cluster?: RedisClusterOptions; }; +/** + * Options for Valkey cache store. + * + * @public + */ +export type ValkeyCacheStoreOptions = { + type: 'valkey'; + client?: KeyvValkeyOptions; + cluster?: RedisClusterOptions; +}; + /** * Union type of all cache store options. * @@ -36,6 +48,7 @@ export type RedisCacheStoreOptions = { */ export type CacheStoreOptions = | RedisCacheStoreOptions + | ValkeyCacheStoreOptions | InfinispanCacheStoreOptions; /** diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts index 5a4631283e..18ed72a085 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -23,6 +23,7 @@ import { import { readHttpServerOptions } from '../rootHttpRouter/http/config'; import { SrvResolvers } from './SrvResolvers'; import { trimEnd } from 'lodash'; +import { getEndpoints } from './parsing'; type Resolver = (pluginId: string) => Promise; @@ -238,25 +239,7 @@ export class HostDiscovery implements DiscoveryService { const endpoints = defaultEndpoints?.slice() ?? []; // Allow config to override the default endpoints - const endpointConfigs = config.getOptionalConfigArray( - 'discovery.endpoints', - ); - for (const endpointConfig of endpointConfigs ?? []) { - if (typeof endpointConfig.get('target') === 'string') { - endpoints.push({ - target: endpointConfig.getString('target'), - plugins: endpointConfig.getStringArray('plugins'), - }); - } else { - endpoints.push({ - target: { - internal: endpointConfig.getOptionalString('target.internal'), - external: endpointConfig.getOptionalString('target.external'), - }, - plugins: endpointConfig.getStringArray('plugins'), - }); - } - } + endpoints.push(...getEndpoints(config)); // Build up a new set of resolvers const internalResolvers: Map = new Map(); diff --git a/packages/backend-defaults/src/entrypoints/discovery/parsing.ts b/packages/backend-defaults/src/entrypoints/discovery/parsing.ts new file mode 100644 index 0000000000..3a02fc4f51 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/discovery/parsing.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 type { HostDiscoveryEndpoint } from './HostDiscovery'; + +export function getEndpoints(config: Config): HostDiscoveryEndpoint[] { + const endpoints: HostDiscoveryEndpoint[] = []; + // Allow config to override the default endpoints + const endpointConfigs = config.getOptionalConfigArray('discovery.endpoints'); + for (const endpointConfig of endpointConfigs ?? []) { + if (typeof endpointConfig.get('target') === 'string') { + endpoints.push({ + target: endpointConfig.getString('target'), + plugins: endpointConfig.getStringArray('plugins'), + }); + } else { + endpoints.push({ + target: { + internal: endpointConfig.getOptionalString('target.internal'), + external: endpointConfig.getOptionalString('target.external'), + }, + plugins: endpointConfig.getStringArray('plugins'), + }); + } + } + return endpoints; +} diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts index 578c76378a..083207d6aa 100644 --- a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts @@ -49,9 +49,10 @@ describe('permissionsRegistryServiceFactory', () => { }), ], }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'"`, - ); + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Backend startup failed due to the following errors: + Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'" + `); await expect( startTestBackend({ @@ -75,8 +76,9 @@ describe('permissionsRegistryServiceFactory', () => { }), ], }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'"`, - ); + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Backend startup failed due to the following errors: + Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'" + `); }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts index ca9085273c..4a5db18f6f 100644 --- a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts @@ -69,7 +69,7 @@ describe('DefaultRootHealthService', () => { await expect(service.getReadiness()).resolves.toEqual({ status: 503, payload: { - message: 'Backend is shuttting down', + message: 'Backend is shutting down', status: 'error', }, }); diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts index e7255610bc..52812fc6b1 100644 --- a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts @@ -50,7 +50,7 @@ export class DefaultRootHealthService implements RootHealthService { if (this.#state === 'down') { return { status: 503, - payload: { message: 'Backend is shuttting down', status: 'error' }, + payload: { message: 'Backend is shutting down', status: 'error' }, }; } diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts index 97daef359a..38cdf75396 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts @@ -39,6 +39,9 @@ describe('readHelmetOptions', () => { crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false, + referrerPolicy: { + policy: ['no-referrer'], + }, }); }); @@ -50,6 +53,9 @@ describe('readHelmetOptions', () => { scriptSrcAttr: ['custom'], 'object-src': ['asd'], }, + referrer: { + policy: ['foo', 'bar'], + }, }); expect(readHelmetOptions(config)).toEqual({ contentSecurityPolicy: { @@ -71,6 +77,9 @@ describe('readHelmetOptions', () => { crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false, + referrerPolicy: { + policy: ['foo', 'bar'], + }, }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts index fc1fc59bcb..040da63e5f 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts @@ -46,6 +46,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions { crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false, + referrerPolicy: readReferrerPolicy(config), }; } @@ -113,3 +114,30 @@ export function applyCspDirectives( return result; } + +type ReferrerPolicy = Record | undefined; + +/** + * Attempts to read the ReferrerPolicy from the backend configuration object. + * + * @example + * ```yaml + * backend: + * referrer: + * policy: ["strict-origin-when-cross-origin"] + * ``` + */ +function readReferrerPolicy(config?: Config): ReferrerPolicy { + const cc = config?.getOptionalConfig('referrer'); + const result: Record = {}; + + if (!cc) { + result.policy = ['no-referrer']; + } else { + for (const key of cc.keys()) { + result[key] = cc.getStringArray(key); + } + } + + return result; +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts index 8f8d1b7656..e9982bc589 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts @@ -285,7 +285,7 @@ describe('rootHttpRouterServiceFactory', () => { // Immediately start failing the readiness health check await request(app).get('/.backstage/health/v1/readiness').expect(503, { - message: 'Backend is shuttting down', + message: 'Backend is shutting down', status: 'error', }); @@ -301,7 +301,7 @@ describe('rootHttpRouterServiceFactory', () => { .expect(200, { status: 'ok' }); await request(app).get('/.backstage/health/v1/readiness').expect(503, { - message: 'Backend is shuttting down', + message: 'Backend is shutting down', status: 'error', }); @@ -312,7 +312,7 @@ describe('rootHttpRouterServiceFactory', () => { .expect(200, { status: 'ok' }); await request(app).get('/.backstage/health/v1/readiness').expect(503, { - message: 'Backend is shuttting down', + message: 'Backend is shutting down', status: 'error', }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts index c48cef2f1f..0455912ca9 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -102,7 +102,9 @@ export class WinstonLogger implements RootLoggerService { return obj; } - obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***'); + if (typeof obj[MESSAGE] === 'string') { + obj[MESSAGE] = obj[MESSAGE].replace(redactionPattern, '***'); + } return obj; })(), @@ -157,8 +159,11 @@ export class WinstonLogger implements RootLoggerService { format.printf((info: TransformableInfo) => { const { timestamp, level, message, plugin, service, ...fields } = info; const prefix = plugin || service; - const timestampColor = colorizer.colorize('timestamp', timestamp); - const prefixColor = colorizer.colorize('prefix', prefix); + const timestampColor = colorizer.colorize( + 'timestamp', + String(timestamp), + ); + const prefixColor = colorizer.colorize('prefix', String(prefix)); const extraFields = Object.entries(fields) .map(([key, value]) => { diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts index de1b6e4caf..5bd3fb38cb 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -46,7 +46,7 @@ describe('TaskWorker', () => { const settings: TaskSettingsV2 = { version: 2, cadence: '*/2 * * * * *', - initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO()!, + initialDelayDuration: Duration.fromObject({ seconds: 3 }).toISO()!, // over 1 second, to cover for 1-second granularity in some database timestamps timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO()!, }; @@ -65,7 +65,7 @@ describe('TaskWorker', () => { expect(JSON.parse(row.settings_json)).toEqual({ version: 2, cadence: '*/2 * * * * *', - initialDelayDuration: 'PT1S', + initialDelayDuration: 'PT3S', timeoutAfterDuration: 'PT1M', }); await expect(TaskWorker.taskStates(knex)).resolves.toEqual( @@ -80,6 +80,10 @@ describe('TaskWorker', () => { ]), ); + // Note that this is timing sensitive - if things take too long between + // persistTask and findReadyTask, this test will fail. We set some margin + // of initialDelayDuration to cover for this, but if this turns out to be + // flaky, we may have to revisit how this test is written. await expect(worker.findReadyTask()).resolves.toEqual({ result: 'not-ready-yet', }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts index eb040ef415..e47967de90 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -280,8 +280,10 @@ export class TaskWorker { .sendAt() .minus({ seconds: 1 }) // immediately, if "* * * * * *" .toUTC(); + // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same + const timeConverted = DateTime.fromJSDate(time.toJSDate()); - nextStartAt = this.nextRunAtRaw(time); + nextStartAt = this.nextRunAtRaw(timeConverted); startAt ||= nextStartAt; } else if (isManual) { nextStartAt = this.knex.raw('null'); @@ -418,8 +420,10 @@ export class TaskWorker { if (isCron) { const time = new CronTime(settings.cadence).sendAt().toUTC(); this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); + // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same + const timeConverted = DateTime.fromJSDate(time.toJSDate()); - nextRun = this.nextRunAtRaw(time); + nextRun = this.nextRunAtRaw(timeConverted); } else if (isManual) { nextRun = this.knex.raw('null'); } else { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts index 3a228f1e9c..73302d37e5 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts @@ -68,11 +68,19 @@ export class BitbucketCloudUrlReader implements UrlReaderService { ) { this.integration = integration; this.deps = deps; - const { host, username, appPassword } = integration.config; + const { host, username, appPassword, token, clientId, clientSecret } = + integration.config; - if (username && !appPassword) { + // Validate: OAuth requires both clientId and clientSecret + if ((clientId && !clientSecret) || (clientSecret && !clientId)) { throw new Error( - `Bitbucket Cloud integration for '${host}' has configured a username but is missing a required appPassword.`, + `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`, + ); + } + + if (username && !token && !appPassword) { + throw new Error( + `Bitbucket Cloud integration for '${host}' has configured a username but is missing a required token or appPassword.`, ); } } @@ -91,7 +99,7 @@ export class BitbucketCloudUrlReader implements UrlReaderService { url, this.integration.config, ); - const requestOptions = getBitbucketCloudRequestOptions( + const requestOptions = await getBitbucketCloudRequestOptions( this.integration.config, ); @@ -149,7 +157,7 @@ export class BitbucketCloudUrlReader implements UrlReaderService { ); const archiveResponse = await fetch( downloadUrl, - getBitbucketCloudRequestOptions(this.integration.config), + await getBitbucketCloudRequestOptions(this.integration.config), ); if (!archiveResponse.ok) { const message = `Failed to read tree from ${url}, ${archiveResponse.status} ${archiveResponse.statusText}`; @@ -228,8 +236,10 @@ export class BitbucketCloudUrlReader implements UrlReaderService { } toString() { - const { host, username, appPassword } = this.integration.config; - const authed = Boolean(username && appPassword); + // TODO: appPassword can be removed once fully + // deprecated by BitBucket on 9th June 2026. + const { host, username, appPassword, token } = this.integration.config; + const authed = Boolean(username && (token ?? appPassword)); return `bitbucketCloud{host=${host},authed=${authed}}`; } @@ -248,7 +258,7 @@ export class BitbucketCloudUrlReader implements UrlReaderService { const commitsResponse = await fetch( commitsApiUrl, - getBitbucketCloudRequestOptions(this.integration.config), + await getBitbucketCloudRequestOptions(this.integration.config), ); if (!commitsResponse.ok) { const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts index b1d7018308..ece9e5e7d5 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts @@ -25,8 +25,6 @@ import { setupServer } from 'msw/node'; import { FetchUrlReader } from './FetchUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; -const fetchUrlReader = new FetchUrlReader(); - describe('FetchUrlReader', () => { const worker = setupServer(); @@ -188,6 +186,16 @@ describe('FetchUrlReader', () => { describe('read', () => { it('should return etag from the response', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + const { buffer } = await fetchUrlReader.readUrl( 'https://backstage.io/some-resource', ); @@ -196,12 +204,32 @@ describe('FetchUrlReader', () => { }); it('should throw NotFound if server responds with 404', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/not-exists'), ).rejects.toThrow(NotFoundError); }); it('should throw Error if server responds with 500', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/error'), ).rejects.toThrow(Error); @@ -210,6 +238,16 @@ describe('FetchUrlReader', () => { describe('readUrl', () => { it('should throw NotModified if server responds with 304 from etag', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/some-resource', { etag: 'foo', @@ -218,6 +256,16 @@ describe('FetchUrlReader', () => { }); it('should throw NotModified if server responds with 304 from lastModifiedAfter', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/some-resource', { lastModifiedAfter: new Date('2020-01-01T00:00:00Z'), @@ -238,6 +286,16 @@ describe('FetchUrlReader', () => { ), ); + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await fetchUrlReader.readUrl( 'https://backstage.io/requires-authentication', { @@ -247,6 +305,16 @@ describe('FetchUrlReader', () => { }); it('should return etag from the response', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + const response = await fetchUrlReader.readUrl( 'https://backstage.io/some-resource', ); @@ -255,20 +323,300 @@ describe('FetchUrlReader', () => { }); it('should throw NotFound if server responds with 404', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/not-exists'), ).rejects.toThrow(NotFoundError); }); it('should throw Error if server responds with 500', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.readUrl('https://backstage.io/error'), ).rejects.toThrow(Error); }); + + it('should block redirects to disallowed hosts to prevent SSRF', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/redirect', (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('location', 'https://evil.com/steal-data'), + ); + }), + ); + + await expect( + reader.readUrl('https://backstage.io/redirect'), + ).rejects.toThrow(/not allowed/); + }); + + it('should allow redirects to allowed hosts', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/redirect', (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('location', 'https://backstage.io/some-resource'), + ); + }), + ); + + const response = await reader.readUrl('https://backstage.io/redirect'); + expect((await response.buffer()).toString()).toBe('content foo'); + }); + + it('should block initial requests to disallowed hosts', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + await expect( + reader.readUrl('https://evil.com/steal-data'), + ).rejects.toThrow(/not allowed/); + }); + + it('should handle relative redirect locations', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/old-path', (_req, res, ctx) => { + return res(ctx.status(301), ctx.set('location', '/some-resource')); + }), + ); + + const response = await reader.readUrl('https://backstage.io/old-path'); + expect((await response.buffer()).toString()).toBe('content foo'); + }); + + it('should handle relative redirect locations with ../', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/deep/nested/path', (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('location', '../../some-resource'), + ); + }), + ); + + const response = await reader.readUrl( + 'https://backstage.io/deep/nested/path', + ); + expect((await response.buffer()).toString()).toBe('content foo'); + }); + + it('should follow multiple redirect hops', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/hop1', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/hop2')); + }), + rest.get('https://backstage.io/hop2', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/hop3')); + }), + rest.get('https://backstage.io/hop3', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/some-resource')); + }), + ); + + const response = await reader.readUrl('https://backstage.io/hop1'); + expect((await response.buffer()).toString()).toBe('content foo'); + }); + + it('should reject when max redirects exceeded', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + // Create a chain of 6 redirects (exceeds MAX_REDIRECTS of 5) + worker.use( + rest.get('https://backstage.io/loop0', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop1')); + }), + rest.get('https://backstage.io/loop1', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop2')); + }), + rest.get('https://backstage.io/loop2', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop3')); + }), + rest.get('https://backstage.io/loop3', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop4')); + }), + rest.get('https://backstage.io/loop4', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop5')); + }), + rest.get('https://backstage.io/loop5', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/loop6')); + }), + ); + + await expect( + reader.readUrl('https://backstage.io/loop0'), + ).rejects.toThrow(/Too many redirects/); + }); + + it('should block redirect to disallowed host mid-chain', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/hop1', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/hop2')); + }), + rest.get('https://backstage.io/hop2', (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('location', 'https://evil.com/steal'), + ); + }), + ); + + await expect(reader.readUrl('https://backstage.io/hop1')).rejects.toThrow( + /not allowed/, + ); + }); + + it('should handle 307 and 308 redirects', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/temp-redirect', (_req, res, ctx) => { + return res(ctx.status(307), ctx.set('location', '/some-resource')); + }), + ); + + const response = await reader.readUrl( + 'https://backstage.io/temp-redirect', + ); + expect((await response.buffer()).toString()).toBe('content foo'); + }); + + it('should validate paths in redirect targets', async () => { + const reader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io', paths: ['/allowed/'] }], + }, + }, + }), + ); + + worker.use( + rest.get('https://backstage.io/allowed/start', (_req, res, ctx) => { + return res(ctx.status(302), ctx.set('location', '/forbidden/path')); + }), + ); + + await expect( + reader.readUrl('https://backstage.io/allowed/start'), + ).rejects.toThrow(/not allowed/); + }); }); describe('search', () => { it('should return a file', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + const data = await fetchUrlReader.search( `https://backstage.io/some-resource`, { etag: 'etag' }, @@ -280,6 +628,16 @@ describe('FetchUrlReader', () => { }); it('should return an empty list of file if not found', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + const data = await fetchUrlReader.search( `https://backstage.io/not-exists`, { etag: 'etag' }, @@ -289,6 +647,16 @@ describe('FetchUrlReader', () => { }); it('throws if given URL with wildcard', async () => { + const fetchUrlReader = FetchUrlReader.fromConfig( + new ConfigReader({ + backend: { + reading: { + allow: [{ host: 'backstage.io' }], + }, + }, + }), + ); + await expect( fetchUrlReader.search(`https://backstage.io/some-resource*`, { etag: 'etag', diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts index a1a61e4cdc..11ace8127c 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts @@ -30,6 +30,10 @@ import { import { ReaderFactory } from './types'; import path from 'path'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; +import { Config } from '@backstage/config'; + +const REDIRECT_STATUS_CODES = [301, 302, 307, 308]; +const MAX_REDIRECTS = 5; const isInRange = (num: number, [start, end]: [number, number]) => { return num >= start && num <= end; @@ -66,6 +70,37 @@ const parsePortPredicate = (port: string | undefined) => { return (url: URL) => !url.port; }; +function predicateFromConfig(config: Config): (url: URL) => boolean { + const allow = config + .getOptionalConfigArray('backend.reading.allow') + ?.map(allowConfig => { + const paths = allowConfig.getOptionalStringArray('paths'); + const checkPath = paths + ? (url: URL) => { + const targetPath = path.posix.normalize(url.pathname); + return paths.some(allowedPath => + targetPath.startsWith(allowedPath), + ); + } + : (_url: URL) => true; + const host = allowConfig.getString('host'); + const [hostname, port] = host.split(':'); + + const checkPort = parsePortPredicate(port); + + if (hostname.startsWith('*.')) { + const suffix = hostname.slice(1); + return (url: URL) => + url.hostname.endsWith(suffix) && checkPath(url) && checkPort(url); + } + + return (url: URL) => + url.hostname === hostname && checkPath(url) && checkPort(url); + }); + + return allow?.length ? url => allow.some(p => p(url)) : () => false; +} + /** * A {@link @backstage/backend-plugin-api#UrlReaderService} that does a plain fetch of the URL. * @@ -85,38 +120,21 @@ export class FetchUrlReader implements UrlReaderService { * An optional list of paths which are allowed. If the list is omitted all paths are allowed. */ static factory: ReaderFactory = ({ config }) => { - const predicates = - config - .getOptionalConfigArray('backend.reading.allow') - ?.map(allowConfig => { - const paths = allowConfig.getOptionalStringArray('paths'); - const checkPath = paths - ? (url: URL) => { - const targetPath = path.posix.normalize(url.pathname); - return paths.some(allowedPath => - targetPath.startsWith(allowedPath), - ); - } - : (_url: URL) => true; - const host = allowConfig.getString('host'); - const [hostname, port] = host.split(':'); - - const checkPort = parsePortPredicate(port); - - if (hostname.startsWith('*.')) { - const suffix = hostname.slice(1); - return (url: URL) => - url.hostname.endsWith(suffix) && checkPath(url) && checkPort(url); - } - return (url: URL) => - url.hostname === hostname && checkPath(url) && checkPort(url); - }) ?? []; - - const reader = new FetchUrlReader(); - const predicate = (url: URL) => predicates.some(p => p(url)); + const predicate = predicateFromConfig(config); + const reader = new FetchUrlReader({ predicate }); return [{ reader, predicate }]; }; + static fromConfig(config: Config): FetchUrlReader { + return new FetchUrlReader({ predicate: predicateFromConfig(config) }); + } + + readonly #predicate: (url: URL) => boolean; + + private constructor(options: { predicate: (url: URL) => boolean }) { + this.#predicate = options.predicate; + } + async read(url: string): Promise { const response = await this.readUrl(url); return response.buffer(); @@ -126,41 +144,69 @@ export class FetchUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadUrlOptions, ): Promise { - let response: Response; - try { - response = await fetch(url, { - headers: { - ...(options?.etag && { 'If-None-Match': options.etag }), - ...(options?.lastModifiedAfter && { - 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), - }), - ...(options?.token && { Authorization: `Bearer ${options.token}` }), - }, - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can - // be removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - signal: options?.signal as any, - }); - } catch (e) { - throw new Error(`Unable to read ${url}, ${e}`); + let currentUrl = url; + + for ( + let redirectCount = 0; + redirectCount < MAX_REDIRECTS; + redirectCount += 1 + ) { + // Validate URL against predicate if configured + const parsedUrl = new URL(currentUrl); + if (!this.#predicate(parsedUrl)) { + throw new Error( + `URL not allowed by backend.reading.allow configuration: ${currentUrl}`, + ); + } + + let response: Response; + try { + response = await fetch(currentUrl, { + headers: { + ...(options?.etag && { 'If-None-Match': options.etag }), + ...(options?.lastModifiedAfter && { + 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), + }), + ...(options?.token && { Authorization: `Bearer ${options.token}` }), + }, + // Handle redirects manually to validate targets against the allowlist + redirect: 'manual', + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can + // be removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + signal: options?.signal as any, + }); + } catch (e) { + throw new Error(`Unable to read ${currentUrl}, ${e}`); + } + + if (response.ok) { + return ReadUrlResponseFactory.fromResponse(response); + } + + if (response.status === 304) { + throw new NotModifiedError(); + } + + const location = response.headers.get('location'); + if (!REDIRECT_STATUS_CODES.includes(response.status) || !location) { + const message = `could not read ${currentUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + // Follow the redirect + currentUrl = new URL(location, currentUrl).toString(); } - if (response.status === 304) { - throw new NotModifiedError(); - } - - if (response.ok) { - return ReadUrlResponseFactory.fromResponse(response); - } - - const message = `could not read ${url}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); + throw new Error( + `Too many redirects (max ${MAX_REDIRECTS}) when reading ${url}`, + ); } async readTree(): Promise { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index 65cd275574..c5ce323c54 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -670,6 +670,17 @@ describe('GitlabUrlReader', () => { ); }); + it('works when there are multiple globs', async () => { + const result = await gitlabProcessor.search( + 'https://gitlab.com/backstage/mock/tree/main/**/docs/**/index.*', + ); + expect(result.etag).toBe('sha123abc'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://gitlab.com/backstage/mock/tree/main/docs/index.md', + ); + }); + it('works for the naive case', async () => { const result = await gitlabProcessor.search( 'https://gitlab.com/backstage/mock/tree/main/**/index.*', diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index 19f1846dda..ee290e31e6 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -319,14 +319,10 @@ export class GitlabUrlReader implements UrlReaderService { */ private getStaticPart(globPattern: string) { const segments = globPattern.split('/'); - let i = segments.length; - while ( - i > 0 && - new Minimatch(segments.slice(0, i).join('/')).match(globPattern) - ) { - i--; - } - return segments.slice(0, i).join('/'); + const globIndex = segments.findIndex(segment => segment.match(/[*?]/)); + return globIndex === -1 + ? globPattern + : segments.slice(0, globIndex).join('/'); } toString() { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.test.ts index 7429bae94f..52d9660632 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.test.ts @@ -23,12 +23,18 @@ import { UrlReaderPredicateTuple } from './types'; import packageinfo from '../../../../package.json'; import { mockServices } from '@backstage/backend-test-utils'; import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api'; +import { Readable } from 'stream'; const bucketGetFilesMock = jest.fn(); class Bucket { getFiles(query: any) { return bucketGetFilesMock(query); } + file(_name: string) { + return { + createReadStream: () => Readable.from(Buffer.from('mock content')), + }; + } } class Storage { bucket() { @@ -186,4 +192,56 @@ describe('GcsUrlReader', () => { expect((await data.files[0].content()).toString()).toEqual('content'); }); }); + + describe('readTree', () => { + const { reader } = createReader({ integrations: { googleGcs: {} } })[0]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns files with relative paths', async () => { + const mockFile1 = { + name: 'prefix/file1.yaml', + metadata: { updated: '2024-01-01T00:00:00Z' }, + createReadStream: () => Readable.from(Buffer.from('content1')), + }; + const mockFile2 = { + name: 'prefix/subdir/file2.yaml', + metadata: { updated: '2024-01-02T00:00:00Z' }, + createReadStream: () => Readable.from(Buffer.from('content2')), + }; + bucketGetFilesMock.mockResolvedValue([[mockFile1, mockFile2]]); + + const result = await reader.readTree( + 'https://storage.cloud.google.com/bucket/prefix/', + ); + const files = await result.files(); + + expect(files).toHaveLength(2); + expect(files[0].path).toBe('file1.yaml'); + expect(files[1].path).toBe('subdir/file2.yaml'); + }); + + it('calls getFiles with correct prefix', async () => { + bucketGetFilesMock.mockResolvedValue([[]]); + + await reader.readTree( + 'https://storage.cloud.google.com/bucket/some/prefix/', + ); + + expect(bucketGetFilesMock).toHaveBeenCalledWith({ + autoPaginate: true, + prefix: 'some/prefix/', + }); + }); + + it('throws if readTree url contains glob pattern', async () => { + await expect( + reader.readTree('https://storage.cloud.google.com/bucket/path/*'), + ).rejects.toThrow( + 'GcsUrlReader readTree does not support glob patterns, use search instead', + ); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts index 0bc5add43f..a29b98eb51 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts @@ -17,22 +17,25 @@ import * as GoogleCloud from '@google-cloud/storage'; import { UrlReaderService, + UrlReaderServiceReadTreeOptions, UrlReaderServiceReadTreeResponse, UrlReaderServiceReadUrlOptions, UrlReaderServiceReadUrlResponse, UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; -import { ReaderFactory } from './types'; +import { ReaderFactory, ReadTreeResponseFactory } from './types'; import getRawBody from 'raw-body'; import { GoogleGcsIntegrationConfig, readGoogleGcsIntegrationConfig, } from '@backstage/integration'; + import { Readable } from 'stream'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import packageinfo from '../../../../package.json'; import { assertError } from '@backstage/errors'; +import { relative } from 'path/posix'; const GOOGLE_GCS_HOST = 'storage.cloud.google.com'; @@ -59,7 +62,7 @@ const parseURL = ( * @public */ export class GoogleGcsUrlReader implements UrlReaderService { - static factory: ReaderFactory = ({ config, logger }) => { + static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => { if (!config.has('integrations.googleGcs')) { return []; } @@ -83,20 +86,29 @@ export class GoogleGcsUrlReader implements UrlReaderService { userAgent: `backstage/backend-defaults.GoogleGcsUrlReader/${packageinfo.version}`, }); } - const reader = new GoogleGcsUrlReader(gcsConfig, storage); + const reader = new GoogleGcsUrlReader(gcsConfig, storage, { + treeResponseFactory, + }); const predicate = (url: URL) => url.host === GOOGLE_GCS_HOST; return [{ reader, predicate }]; }; private readonly integration: GoogleGcsIntegrationConfig; private readonly storage: GoogleCloud.Storage; + private readonly deps: { + treeResponseFactory: ReadTreeResponseFactory; + }; constructor( integration: GoogleGcsIntegrationConfig, storage: GoogleCloud.Storage, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, ) { this.integration = integration; this.storage = storage; + this.deps = deps; } private readStreamFromUrl(url: string): Readable { @@ -121,8 +133,32 @@ export class GoogleGcsUrlReader implements UrlReaderService { return ReadUrlResponseFactory.fromReadable(stream); } - async readTree(): Promise { - throw new Error('GcsUrlReader does not implement readTree'); + async readTree( + url: string, + _options?: UrlReaderServiceReadTreeOptions, + ): Promise { + const { bucket, key } = parseURL(url); + + if (key.match(/[*?]/)) { + throw new Error( + 'GcsUrlReader readTree does not support glob patterns, use search instead', + ); + } + + const [files] = await this.storage.bucket(bucket).getFiles({ + autoPaginate: true, + prefix: key, + }); + + const responses = files.map(file => ({ + data: file.createReadStream(), + path: relative(key, file.name), + lastModifiedAt: file.metadata.updated + ? new Date(file.metadata.updated as string) + : undefined, + })); + + return this.deps.treeResponseFactory.fromReadableArray(responses); } async search( diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.test.ts index 7420f14d70..8f0d260f6d 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.test.ts @@ -108,4 +108,16 @@ describe('ReadableArrayResponse', () => { }, }); }); + + it('should validate relative paths', async () => { + const arr: FromReadableArrayOptions = [ + { data: createReadStream(path1), path: '../other/file.yaml' }, + ]; + + const res = new ReadableArrayResponse(arr, targetDir.path, 'etag'); + + await expect(res.dir()).rejects.toThrow( + 'Relative path is not allowed to refer to a directory outside its parent', + ); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.ts index 095b58edb1..6740771ad7 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ReadableArrayResponse.ts @@ -15,6 +15,7 @@ */ import { + resolveSafeChildPath, UrlReaderServiceReadTreeResponse, UrlReaderServiceReadTreeResponseDirOptions, UrlReaderServiceReadTreeResponseFile, @@ -98,7 +99,7 @@ export class ReadableArrayResponse implements UrlReaderServiceReadTreeResponse { for (let i = 0; i < this.stream.length; i++) { if (!this.stream[i].path.endsWith('/')) { - const filePath = platformPath.join(dir, this.stream[i].path); + const filePath = resolveSafeChildPath(dir, this.stream[i].path); await fs.mkdir(dirname(filePath), { recursive: true }); await pipeline(this.stream[i].data, fs.createWriteStream(filePath)); } diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.test.ts index f69fb2e0e1..36b3d129e9 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.test.ts @@ -15,9 +15,10 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import { TarArchiveResponse } from './TarArchiveResponse'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import tar from 'tar'; const archiveData = fs.readFileSync( resolvePath(__filename, '../../__fixtures__/mock-main.tar.gz'), @@ -223,4 +224,84 @@ describe('TarArchiveResponse', () => { await expect(res.dir({ targetDir: sub })).rejects.toThrow('NOPE'); await expect(fs.pathExists(sub)).resolves.toBe(true); }); + + describe('symlink handling', () => { + const tempDir = createMockDirectory(); + + it('should not extract symlinks with absolute targets', async () => { + const archiveSourceDir = tempDir.resolve('source'); + await fs.ensureDir(joinPath(archiveSourceDir, 'repo')); + await fs.writeFile( + joinPath(archiveSourceDir, 'repo', 'file.txt'), + 'file content', + ); + + // Create a symlink with an absolute path target + const absoluteTarget = tempDir.resolve('other.txt'); + await fs.writeFile(absoluteTarget, 'other content'); + await fs.symlink( + absoluteTarget, + joinPath(archiveSourceDir, 'repo', 'abs-link'), + ); + + const tarballPath = tempDir.resolve('archive.tar.gz'); + await tar.create( + { gzip: true, file: tarballPath, cwd: archiveSourceDir }, + ['repo'], + ); + + const stream = fs.createReadStream(tarballPath); + const res = new TarArchiveResponse(stream, '', targetDir.path, 'etag'); + + targetDir.addContent({ out: {} }); + const dir = await res.dir({ targetDir: targetDir.resolve('out') }); + + // Regular file should be extracted + await expect( + fs.readFile(joinPath(dir, 'file.txt'), 'utf8'), + ).resolves.toBe('file content'); + + // Symlink with absolute target should not be extracted + await expect(fs.lstat(joinPath(dir, 'abs-link'))).rejects.toThrow( + 'ENOENT', + ); + }); + + it('should extract symlinks with relative targets within archive', async () => { + const archiveSourceDir = tempDir.resolve('source2'); + await fs.ensureDir(joinPath(archiveSourceDir, 'repo', 'subdir')); + await fs.writeFile( + joinPath(archiveSourceDir, 'repo', 'target.txt'), + 'target content', + ); + + // Create a relative symlink pointing to a file within the archive + await fs.symlink( + '../target.txt', + joinPath(archiveSourceDir, 'repo', 'subdir', 'rel-link'), + ); + + const tarballPath = tempDir.resolve('archive2.tar.gz'); + await tar.create( + { gzip: true, file: tarballPath, cwd: archiveSourceDir }, + ['repo'], + ); + + const stream = fs.createReadStream(tarballPath); + const res = new TarArchiveResponse(stream, '', targetDir.path, 'etag'); + + targetDir.addContent({ out2: {} }); + const dir = await res.dir({ targetDir: targetDir.resolve('out2') }); + + // The symlink should be extracted and point to the correct file + const linkPath = joinPath(dir, 'subdir', 'rel-link'); + const stats = await fs.lstat(linkPath); + expect(stats.isSymbolicLink()).toBe(true); + + // Following the symlink should give us the target content + await expect(fs.readFile(linkPath, 'utf8')).resolves.toBe( + 'target content', + ); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts index 3102137f64..ab3a593c18 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/TarArchiveResponse.ts @@ -15,6 +15,7 @@ */ import { + isChildPath, UrlReaderServiceReadTreeResponse, UrlReaderServiceReadTreeResponseDirOptions, UrlReaderServiceReadTreeResponseFile, @@ -23,7 +24,7 @@ import concatStream from 'concat-stream'; import fs from 'fs-extra'; import platformPath from 'path'; import { pipeline as pipelineCb, Readable } from 'stream'; -import tar, { Parse, ParseStream, ReadEntry } from 'tar'; +import tar, { FileStat, Parse, ParseStream, ReadEntry } from 'tar'; import { promisify } from 'util'; import { stripFirstDirectoryFromPath } from './util'; @@ -182,6 +183,22 @@ export class TarArchiveResponse implements UrlReaderServiceReadTreeResponse { return false; } + // Block symlinks/hardlinks that escape the extraction directory + const entry = stat as FileStat & { type?: string; linkpath?: string }; + if ( + (entry.type === 'SymbolicLink' || entry.type === 'Link') && + entry.linkpath + ) { + const strippedPath = path.split('/').slice(strip).join('/'); + const linkDir = platformPath.dirname( + platformPath.join(dir, strippedPath), + ); + const targetPath = platformPath.resolve(linkDir, entry.linkpath); + if (!isChildPath(dir, targetPath)) { + return false; + } + } + // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. const relativePath = this.stripFirstDirectory diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ZipArchiveResponse.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ZipArchiveResponse.test.ts index 444cbf60b1..dcd9bcbd7e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/tree/ZipArchiveResponse.test.ts @@ -271,4 +271,47 @@ describe('ZipArchiveResponse', () => { 'invalid relative path: ../side.txt', ); }); + + describe('symlink handling', () => { + it('should extract symlink entries as regular files', async () => { + const externalPath = targetDir.resolve('external.txt'); + await fs.writeFile(externalPath, 'external content'); + + const zipPath = targetDir.resolve('test.zip'); + await new Promise((resolve, reject) => { + const output = fs.createWriteStream(zipPath); + const archive = createArchive('zip'); + + output.on('close', () => resolve()); + archive.on('error', reject); + archive.pipe(output); + + archive.append('file content', { name: 'file.txt' }); + archive.symlink('link', externalPath); + + archive.finalize(); + }); + + const stream = createReadStream(zipPath); + const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag'); + + targetDir.addContent({ out: {} }); + const outDir = targetDir.resolve('out'); + const dir = await res.dir({ targetDir: outDir }); + + await expect( + fs.readFile(resolvePath(dir, 'file.txt'), 'utf8'), + ).resolves.toBe('file content'); + + const entryPath = resolvePath(dir, 'link'); + const stats = await fs.lstat(entryPath); + + // yauzl extracts symlink entries as regular files + expect(stats.isSymbolicLink()).toBe(false); + expect(stats.isFile()).toBe(true); + + const content = await fs.readFile(entryPath, 'utf8'); + expect(content).toBe(externalPath); + }); + }); }); diff --git a/packages/backend-dev-utils/CHANGELOG.md b/packages/backend-dev-utils/CHANGELOG.md index 81686fb493..d5884588fb 100644 --- a/packages/backend-dev-utils/CHANGELOG.md +++ b/packages/backend-dev-utils/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/backend-dev-utils +## 0.1.6 + +### Patch Changes + +- 2bae83a: Internal update for Node.js v24 support. + +## 0.1.6-next.0 + +### Patch Changes + +- 2bae83a: Internal update for Node.js v24 support. + ## 0.1.5 ### Patch Changes diff --git a/packages/backend-dev-utils/package.json b/packages/backend-dev-utils/package.json index 4d22c5010f..ba46f4b36e 100644 --- a/packages/backend-dev-utils/package.json +++ b/packages/backend-dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dev-utils", - "version": "0.1.5", + "version": "0.1.6", "backstage": { "role": "node-library" }, diff --git a/packages/backend-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts index 6e1d3d181e..814767347e 100644 --- a/packages/backend-dev-utils/src/ipcClient.ts +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -126,7 +126,7 @@ export class BackstageIpcClient { timeout.unref(); this.#handlers.set(id, responseHandler); - this.#sendMessage(request, (e: Error) => { + this.#sendMessage(request, undefined, undefined, (e: Error | null) => { if (e) { reject(e); } diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 30bce3a09d..86d46b773d 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,181 @@ # @backstage/backend-dynamic-feature-service +## 0.7.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/cli-common@0.1.17 + - @backstage/plugin-app-node@0.1.41 + - @backstage/plugin-scaffolder-node@0.12.3 + - @backstage/backend-openapi-utils@0.6.5 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + - @backstage/plugin-catalog-backend@3.3.1 + - @backstage/plugin-events-backend@0.5.10 + +## 0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-search-backend-node@1.4.0 + +## 0.7.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-scaffolder-node@0.12.3-next.0 + +## 0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.40 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-scaffolder-node@0.12.2 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-search-common@1.2.21 + +## 0.7.7 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-app-node@0.1.40 + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-events-backend@0.5.9 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + - @backstage/plugin-scaffolder-node@0.12.2 + +## 0.7.7-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-app-node@0.1.40-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-events-backend@0.5.9-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-node@0.12.2-next.1 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + - @backstage/plugin-search-common@1.2.21 + +## 0.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-backend@0.5.9-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-app-node@0.1.40-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/plugin-scaffolder-node@0.12.2-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-common@1.2.21 + +## 0.7.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/plugin-events-backend@0.5.8 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/plugin-scaffolder-node@0.12.1 + - @backstage/backend-openapi-utils@0.6.3 + - @backstage/plugin-app-node@0.1.39 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-search-common@1.2.21 + +## 0.7.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-app-node@0.1.39-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-scaffolder-node@0.12.1-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + ## 0.7.6-next.0 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index d203e6d72a..9a2e55fcd1 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.6-next.0", + "version": "0.7.8", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" @@ -70,7 +70,7 @@ "@manypkg/get-packages": "^1.1.3", "@module-federation/sdk": "^0.9.0", "chokidar": "^3.5.3", - "express": "^4.17.1", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "fs-extra": "^11.2.0", "lodash": "^4.17.21", diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index f1917123a4..2bb41c3594 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/backend-openapi-utils +## 0.6.5 + +### Patch Changes + +- 6678b78: Internal update to use native feature from our request validation library for handling base path determination. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## 0.6.5-next.0 + +### Patch Changes + +- 6678b78: Internal update to use native feature from our request validation library for handling base path determination. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.4 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## 0.6.4-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## 0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 0.6.3-next.0 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index a70f729714..0401a9b6fb 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-openapi-utils", - "version": "0.6.3-next.0", + "version": "0.6.5", "description": "OpenAPI typescript support.", "backstage": { "role": "node-library" @@ -52,7 +52,7 @@ "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "ajv": "^8.16.0", - "express": "^4.17.1", + "express": "^4.22.0", "express-openapi-validator": "^5.5.8", "express-promise-router": "^4.1.0", "get-port": "^5.1.1", diff --git a/packages/backend-openapi-utils/src/stub.ts b/packages/backend-openapi-utils/src/stub.ts index 8ab00c4df4..717e2ae19b 100644 --- a/packages/backend-openapi-utils/src/stub.ts +++ b/packages/backend-openapi-utils/src/stub.ts @@ -31,13 +31,6 @@ import { middleware as OpenApiValidator } from 'express-openapi-validator'; import { OPENAPI_SPEC_ROUTE } from './constants'; import { isErrorResult, merge } from 'openapi-merge'; -type PropertyOverrideRequest = Request & { - [key: symbol]: string; -}; - -const baseUrlSymbol = Symbol(); -const originalUrlSymbol = Symbol(); - function validatorErrorTransformer(): ErrorRequestHandler { return (error: Error, _: Request, _2: Response, next: NextFunction) => { next(new InputError(error.message)); @@ -75,30 +68,6 @@ function createRouterWithValidation( const router = PromiseRouter(); router.use(options?.middleware || getDefaultRouterMiddleware()); - /** - * Middleware to setup the routing for OpenApiValidator. OpenApiValidator expects `req.originalUrl` - * and `req.baseUrl` to be the full path. We adjust them here to basically be nothing and then - * revive the old values in the last function in this method. We could instead update `req.path` - * but that might affect the routing and I'd rather not. - * - * TODO: I opened https://github.com/cdimascio/express-openapi-validator/issues/843 - * to track this on the middleware side, but there was a similar ticket, https://github.com/cdimascio/express-openapi-validator/issues/113 - * that has had minimal activity. If that changes, update this to use a new option on their side. - */ - router.use((req: Request, _, next) => { - /** - * Express typings are weird. They don't recognize PropertyOverrideRequest as a valid - * Request child and try to overload as PathParams. Just cast it here, since we know - * what we're doing. - */ - const customRequest = req as PropertyOverrideRequest; - customRequest[baseUrlSymbol] = customRequest.baseUrl; - customRequest.baseUrl = ''; - customRequest[originalUrlSymbol] = customRequest.originalUrl; - customRequest.originalUrl = customRequest.url; - next(); - }); - // TODO: Handle errors by converting from OpenApiValidator errors to known @backstage/errors errors. router.use( OpenApiValidator({ @@ -106,6 +75,7 @@ function createRouterWithValidation( coerceTypes: false, allowUnknownQueryParameters: false, }, + useRequestUrl: true, ignoreUndocumented: true, validateResponses: false, ...options?.validatorOptions, @@ -113,19 +83,6 @@ function createRouterWithValidation( }), ); - /** - * Revert `req.baseUrl` and `req.originalUrl` changes. This ensures that any further usage - * of these variables will be unchanged. - */ - router.use((req: Request, _, next) => { - const customRequest = req as PropertyOverrideRequest; - customRequest.baseUrl = customRequest[baseUrlSymbol]; - customRequest.originalUrl = customRequest[originalUrlSymbol]; - delete customRequest[baseUrlSymbol]; - delete customRequest[originalUrlSymbol]; - next(); - }); - // Any errors from the middleware get through here. router.use(validatorErrorTransformer()); diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index fedc20cd49..3ecec4e9b4 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,95 @@ # @backstage/backend-plugin-api +## 1.6.1 + +### Patch Changes + +- ae4dd5d: Move some of the symlink resolution to `isChildPath` +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + +## 1.6.0 + +### Minor Changes + +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/cli-common@0.1.16 + +## 1.6.0-next.1 + +### Minor Changes + +- 2a0c4b0: Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## 1.5.1-next.0 + +### Patch Changes + +- d9759a1: **BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## 1.5.0 + +### Minor Changes + +- a17d9df: Promote `instanceMetadata` service to main entrypoint. +- 62fc2de: Explicitly mark `coreServices.rootInstanceMetadata` as a root service. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-node@0.10.6 + +## 1.5.0-next.2 + +### Minor Changes + +- 62fc2de: Explicitly mark `coreServices.rootInstanceMetadata` as a root service. + +## 1.5.0-next.1 + +### Minor Changes + +- a17d9df: Promote `instanceMetadata` service to main entrypoint. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + ## 1.4.5-next.0 ### Patch Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 78ff5d7131..581445e803 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "1.4.5-next.0", + "version": "1.6.1", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 9be7a29ed5..e06e0c45d2 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -103,28 +103,24 @@ export const actionsServiceRef: ServiceRef< 'singleton' >; -// @alpha (undocumented) -export type BackendFeatureMeta = - | { - type: 'plugin'; - pluginId: string; - } - | { - type: 'module'; - pluginId: string; - moduleId: string; - }; - -// @alpha (undocumented) -export interface InstanceMetadataService { +// @public (undocumented) +export interface RootSystemMetadataService { // (undocumented) - getInstalledFeatures: () => BackendFeatureMeta[]; + getInstalledPlugins: () => Promise< + ReadonlyArray + >; } -// @alpha (undocumented) -export const instanceMetadataServiceRef: ServiceRef< - InstanceMetadataService, - 'plugin', +// @public (undocumented) +export interface RootSystemMetadataServicePluginInfo { + // (undocumented) + readonly pluginId: string; +} + +// @alpha +export const rootSystemMetadataServiceRef: ServiceRef< + RootSystemMetadataService, + 'root', 'singleton' >; diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 52843dec96..c46ed254c8 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -232,6 +232,11 @@ export namespace coreServices { const rootLogger: ServiceRef; const scheduler: ServiceRef; const urlReader: ServiceRef; + const rootInstanceMetadata: ServiceRef< + RootInstanceMetadataService, + 'root', + 'singleton' + >; } // @public @@ -578,6 +583,24 @@ export interface RootHttpRouterService { use(path: string, handler: Handler): void; } +// @public (undocumented) +export interface RootInstanceMetadataService { + // (undocumented) + getInstalledPlugins: () => Promise< + ReadonlyArray + >; +} + +// @public (undocumented) +export interface RootInstanceMetadataServicePluginInfo { + // (undocumented) + readonly modules: ReadonlyArray<{ + moduleId: string; + }>; + // (undocumented) + readonly pluginId: string; +} + // @public export interface RootLifecycleService extends LifecycleService { // (undocumented) diff --git a/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts b/packages/backend-plugin-api/src/alpha/RootSystemMetadataService.ts similarity index 67% rename from packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts rename to packages/backend-plugin-api/src/alpha/RootSystemMetadataService.ts index 869d361343..67cc43f8af 100644 --- a/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/alpha/RootSystemMetadataService.ts @@ -14,19 +14,14 @@ * limitations under the License. */ -/** @alpha */ -export type BackendFeatureMeta = - | { - type: 'plugin'; - pluginId: string; - } - | { - type: 'module'; - pluginId: string; - moduleId: string; - }; - -/** @alpha */ -export interface InstanceMetadataService { - getInstalledFeatures: () => BackendFeatureMeta[]; +/** @public */ +export interface RootSystemMetadataServicePluginInfo { + readonly pluginId: string; +} + +/** @public */ +export interface RootSystemMetadataService { + getInstalledPlugins: () => Promise< + ReadonlyArray + >; } diff --git a/packages/backend-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts index 5bb69eb4e2..56f02f5275 100644 --- a/packages/backend-plugin-api/src/alpha/index.ts +++ b/packages/backend-plugin-api/src/alpha/index.ts @@ -15,9 +15,9 @@ */ export type { - BackendFeatureMeta, - InstanceMetadataService, -} from './InstanceMetadataService'; + RootSystemMetadataServicePluginInfo, + RootSystemMetadataService, +} from './RootSystemMetadataService'; export type { ActionsRegistryService, @@ -30,5 +30,5 @@ export type { ActionsService, ActionsServiceAction } from './ActionsService'; export { actionsRegistryServiceRef, actionsServiceRef, - instanceMetadataServiceRef, + rootSystemMetadataServiceRef, } from './refs'; diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts index 81996679f7..a890271364 100644 --- a/packages/backend-plugin-api/src/alpha/refs.ts +++ b/packages/backend-plugin-api/src/alpha/refs.ts @@ -16,15 +16,6 @@ import { createServiceRef } from '@backstage/backend-plugin-api'; -/** - * @alpha - */ -export const instanceMetadataServiceRef = createServiceRef< - import('./InstanceMetadataService').InstanceMetadataService ->({ - id: 'core.instanceMetadata', -}); - /** * Service for calling distributed actions * @@ -54,3 +45,14 @@ export const actionsRegistryServiceRef = createServiceRef< >({ id: 'alpha.core.actionsRegistry', }); + +/** + * Read information about your current Backstage deployment. + * @alpha + */ +export const rootSystemMetadataServiceRef = createServiceRef< + import('./RootSystemMetadataService').RootSystemMetadataService +>({ + id: 'alpha.core.rootSystemMetadata', + scope: 'root', +}); diff --git a/packages/backend-plugin-api/src/paths.test.ts b/packages/backend-plugin-api/src/paths.test.ts index 6073d43a24..946250372c 100644 --- a/packages/backend-plugin-api/src/paths.test.ts +++ b/packages/backend-plugin-api/src/paths.test.ts @@ -65,5 +65,85 @@ describe('paths', () => { `${workspacePath}/template`, ); }); + + it('should throw an error if the path is a symlink pointing to a non-existent target outside base', () => { + // This tests the case where realpathSync would fail with ENOENT + // but the symlink itself exists and points outside the base directory + const nonExistentTarget = `${secondDirectory.path}/does-not-exist.txt`; + mockDir.addContent({ + [`${workspacePath}/dangling-link`]: ({ symlink }) => + symlink(nonExistentTarget), + }); + + expect(() => + resolveSafeChildPath(workspacePath, './dangling-link'), + ).toThrow( + 'Relative path is not allowed to refer to a directory outside its parent', + ); + }); + + it('should allow symlinks pointing to non-existent targets within base directory', () => { + mockDir.addContent({ + [`${workspacePath}/internal-link`]: ({ symlink }) => + symlink('./future-file.txt'), + }); + + expect(resolveSafeChildPath(workspacePath, './internal-link')).toEqual( + `${workspacePath}/internal-link`, + ); + }); + + it('should throw an error when writing through a symlink to a non-existent file outside base', () => { + // Symlink in workspace points outside, target directory exists but file doesn't + // e.g., /workspace/evil -> /etc, then write to evil/newfile.conf + // The check should catch that evil/newfile.conf resolves to /etc/newfile.conf + mockDir.addContent({ + [`${workspacePath}/escape-link`]: ({ symlink }) => + symlink(secondDirectory.path), + }); + + // This should throw because escape-link/new-file.txt would write to secondDirectory/new-file.txt + expect(() => + resolveSafeChildPath(workspacePath, './escape-link/new-file.txt'), + ).toThrow( + 'Relative path is not allowed to refer to a directory outside its parent', + ); + }); + + it('should throw an error for symlink chains pointing outside base', () => { + // link1 -> link2 -> outside + // Even if final target doesn't exist, should detect the escape + const nonExistentOutside = `${secondDirectory.path}/does-not-exist`; + mockDir.addContent({ + [`${workspacePath}/link2`]: ({ symlink }) => + symlink(nonExistentOutside), + [`${workspacePath}/link1`]: ({ symlink }) => symlink('./link2'), + }); + + expect(() => resolveSafeChildPath(workspacePath, './link1')).toThrow( + 'Relative path is not allowed to refer to a directory outside its parent', + ); + }); + + it('should throw when deeply nested non-existent path has symlink ancestor pointing outside', () => { + // Tests the recursive parent-walking behavior in resolveRealPath. + // When given a/b/c/d/file.txt where none of b/c/d exist, the code walks up + // the tree until finding 'a' (a symlink), resolves it, then rebuilds the path. + mockDir.addContent({ + [`${workspacePath}/escape`]: ({ symlink }) => + symlink(secondDirectory.path), + }); + + // escape/deeply/nested/path/file.txt requires walking up 4 levels + // before finding the symlink at 'escape' + expect(() => + resolveSafeChildPath( + workspacePath, + './escape/deeply/nested/path/file.txt', + ), + ).toThrow( + 'Relative path is not allowed to refer to a directory outside its parent', + ); + }); }); }); diff --git a/packages/backend-plugin-api/src/paths.ts b/packages/backend-plugin-api/src/paths.ts index c77f232633..7ec661cf75 100644 --- a/packages/backend-plugin-api/src/paths.ts +++ b/packages/backend-plugin-api/src/paths.ts @@ -17,7 +17,6 @@ import { isChildPath } from '@backstage/cli-common'; import { NotAllowedError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; -import { realpathSync as realPath } from 'fs'; /** @internal */ export const packagePathMocks = new Map< @@ -63,10 +62,9 @@ export function resolvePackagePath(name: string, ...paths: string[]) { * @returns A path that is guaranteed to point to or within the base path. */ export function resolveSafeChildPath(base: string, path: string): string { - const resolvedBasePath = resolveRealPath(base); - const targetPath = resolvePath(resolvedBasePath, path); + const targetPath = resolvePath(base, path); - if (!isChildPath(resolvedBasePath, resolveRealPath(targetPath))) { + if (!isChildPath(base, targetPath)) { throw new NotAllowedError( 'Relative path is not allowed to refer to a directory outside its parent', ); @@ -76,16 +74,5 @@ export function resolveSafeChildPath(base: string, path: string): string { return resolvePath(base, path); } -function resolveRealPath(path: string): string { - try { - return realPath(path); - } catch (ex) { - if (ex.code !== 'ENOENT') { - throw ex; - } - } - - return path; -} // Re-export isChildPath so that backend packages don't need to depend on cli-common export { isChildPath }; diff --git a/packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts new file mode 100644 index 0000000000..c324fed331 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export interface RootInstanceMetadataServicePluginInfo { + readonly pluginId: string; + readonly modules: ReadonlyArray<{ + moduleId: string; + }>; +} + +/** @public */ +export interface RootInstanceMetadataService { + getInstalledPlugins: () => Promise< + ReadonlyArray + >; +} diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index 8c8c6c0f82..887b05ee4e 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -277,4 +277,16 @@ export namespace coreServices { export const urlReader = createServiceRef< import('./UrlReaderService').UrlReaderService >({ id: 'core.urlReader' }); + + /** + * Information about the current Backstage instance. + * + * @public + */ + export const rootInstanceMetadata = createServiceRef< + import('./RootInstanceMetadataService').RootInstanceMetadataService + >({ + id: 'core.rootInstanceMetadata', + scope: 'root', + }); } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index c811a513a8..6bcee4b043 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -85,4 +85,8 @@ export type { UrlReaderServiceSearchResponseFile, } from './UrlReaderService'; export type { BackstageUserInfo, UserInfoService } from './UserInfoService'; +export type { + RootInstanceMetadataService, + RootInstanceMetadataServicePluginInfo, +} from './RootInstanceMetadataService'; export { coreServices } from './coreServices'; diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 7625dc43c4..943d1757de 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,129 @@ # @backstage/backend-test-utils +## 1.10.3 + +### Patch Changes + +- 872eb91: Upgrade `zod-to-json-schema` to latest version +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-app-api@1.4.1 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + +## 1.10.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/backend-app-api@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + +## 1.10.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-events-node@0.4.18 + - @backstage/plugin-permission-common@0.9.3 + +## 1.10.2 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 8be23a4: Switched `textextensions` dependency for `text-extensions`. +- 5a737e1: Fix PostgreSQL 18 `TestDatabases` by pinning the data directory +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-app-api@1.4.0 + - @backstage/backend-plugin-api@1.6.0 + +## 1.10.2-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 8be23a4: Switched `textextensions` dependency for `text-extensions`. +- 5a737e1: Fix PostgreSQL 18 `TestDatabases` by pinning the data directory +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/backend-app-api@1.4.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## 1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-app-api@1.4.0-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## 1.10.0 + +### Minor Changes + +- d57b13b: Added support for Postgres 18 to the available `TestDatabases`. + + Note that the set of _default_ databases to test against for users of the `TestDatabases` class was also updated to include Postgres 14 and 18, instead of 13 and 17. If you need to override this, you can pass in an explicit `ids` argument, for example `ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3']`. + +### Patch Changes + +- f3001fd: Tweak some of the mock services to have more precise types +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/backend-app-api@1.3.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-events-node@0.4.17 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/config@1.3.6 + +## 1.10.0-next.2 + +### Patch Changes + +- f3001fd: Tweak some of the mock services to have more precise types +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + +## 1.10.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-app-api@1.3.0-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 1.10.0-next.0 ### Minor Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 7ce66eaa85..27c00a28dd 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "1.10.0-next.0", + "version": "1.10.3", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" @@ -67,7 +67,7 @@ "@types/qs": "^6.9.6", "better-sqlite3": "^12.0.0", "cookie": "^0.7.0", - "express": "^4.17.1", + "express": "^4.22.0", "fs-extra": "^11.0.0", "keyv": "^5.2.1", "knex": "^3.0.0", @@ -75,12 +75,12 @@ "mysql2": "^3.0.0", "pg": "^8.11.3", "pg-connection-string": "^2.3.0", - "testcontainers": "^10.0.0", - "textextensions": "^5.16.0", + "testcontainers": "^11.9.0", + "text-extensions": "^2.4.0", "uuid": "^11.0.0", "yn": "^4.0.0", "zod": "^3.22.4", - "zod-to-json-schema": "^3.20.4" + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index e7763329a8..da5002275b 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -35,6 +35,7 @@ import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { RootInstanceMetadataService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -229,11 +230,7 @@ export namespace mockServices { export function events(): EventsService; // (undocumented) export namespace events { - const factory: () => ServiceFactory< - EventsService, - 'plugin', - 'singleton' | 'multiton' - >; + const factory: () => ServiceFactory; const mock: ( partialImpl?: Partial | undefined, ) => ServiceMock; @@ -319,7 +316,7 @@ export namespace mockServices { const // (undocumented) factory: ( options?: Options | undefined, - ) => ServiceFactory; + ) => ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -344,6 +341,21 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export function rootInstanceMetadata(): RootInstanceMetadataService; + // (undocumented) + export namespace rootInstanceMetadata { + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + const // (undocumented) + factory: () => ServiceFactory< + RootInstanceMetadataService, + 'root', + 'singleton' + >; + } + // (undocumented) export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; @@ -353,7 +365,7 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) - export function rootLogger(options?: rootLogger.Options): LoggerService; + export function rootLogger(options?: rootLogger.Options): RootLoggerService; // (undocumented) export namespace rootLogger { // (undocumented) @@ -363,7 +375,7 @@ export namespace mockServices { const // (undocumented) factory: ( options?: Options | undefined, - ) => ServiceFactory; + ) => ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/database/postgres.ts b/packages/backend-test-utils/src/database/postgres.ts index e6122b9ab7..d7238cb36a 100644 --- a/packages/backend-test-utils/src/database/postgres.ts +++ b/packages/backend-test-utils/src/database/postgres.ts @@ -77,7 +77,11 @@ export async function startPostgresContainer(image: string): Promise<{ const container = await new GenericContainer(image) .withExposedPorts(5432) - .withEnvironment({ POSTGRES_PASSWORD: password }) + .withEnvironment({ + // Since postgres 18, the default directory changed - so we pin it here + PGDATA: '/var/lib/postgresql/data', + POSTGRES_PASSWORD: password, + }) .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) .start(); diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts index fd22f09432..82d46d361b 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts @@ -271,14 +271,16 @@ describe('createMockDirectory', () => { }); it('should reject non-child paths', () => { - const path = mockDir.resolve('/root/a.txt'); - expect(() => mockDir.setContent({ '/root/a.txt': 'a' })).toThrow( + const pathOutside = createMockDirectory().path; + + const path = mockDir.resolve(pathOutside); + expect(() => mockDir.setContent({ [pathOutside]: 'a' })).toThrow( `Provided path must resolve to a child path of the mock directory, got '${path}'`, ); - expect(() => mockDir.addContent({ '/root/a.txt': 'a' })).toThrow( + expect(() => mockDir.addContent({ [pathOutside]: 'a' })).toThrow( `Provided path must resolve to a child path of the mock directory, got '${path}'`, ); - expect(() => mockDir.content({ path: '/root/a.txt' })).toThrow( + expect(() => mockDir.content({ path: pathOutside })).toThrow( `Provided path must resolve to a child path of the mock directory, got '${path}'`, ); }); diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index a1d616d8e0..f940a4cd7b 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -17,7 +17,7 @@ import os from 'os'; import { isChildPath } from '@backstage/backend-plugin-api'; import fs from 'fs-extra'; -import textextensions from 'textextensions'; +import textextensions from 'text-extensions'; import { dirname, extname, diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index f7d57d3fb1..9e941db3bb 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -34,7 +34,7 @@ import { DatabaseService, DiscoveryService, HttpAuthService, - LoggerService, + RootInstanceMetadataService, PermissionsService, RootConfigService, SchedulerService, @@ -43,6 +43,7 @@ import { UserInfoService, coreServices, createServiceFactory, + RootLoggerService, } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; import { EventsService, eventsServiceRef } from '@backstage/plugin-events-node'; @@ -76,14 +77,15 @@ function createLoggerMock() { function simpleFactoryWithOptions< TService, TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', TOptions extends [options?: object] = [], >( - ref: ServiceRef, + ref: ServiceRef, factory: (...options: TOptions) => TService, -): (...options: TOptions) => ServiceFactory { +): (...options: TOptions) => ServiceFactory { const factoryWithOptions = (...options: TOptions) => createServiceFactory({ - service: ref as ServiceRef, + service: ref as ServiceRef, deps: {}, async factory() { return factory(...options); @@ -92,8 +94,8 @@ function simpleFactoryWithOptions< return Object.assign( factoryWithOptions, factoryWithOptions(...([undefined] as unknown as TOptions)), - ) as ServiceFactory & - ((...options: TOptions) => ServiceFactory); + ) as ServiceFactory & + ((...options: TOptions) => ServiceFactory); } /** @@ -177,7 +179,7 @@ export namespace mockServices { })); } - export function rootLogger(options?: rootLogger.Options): LoggerService { + export function rootLogger(options?: rootLogger.Options): RootLoggerService { return MockRootLoggerService.create(options); } export namespace rootLogger { @@ -556,4 +558,19 @@ export namespace mockServices { subscribe: jest.fn(), })); } + + export function rootInstanceMetadata(): RootInstanceMetadataService { + return { + getInstalledPlugins: () => Promise.resolve([]), + }; + } + export namespace rootInstanceMetadata { + export const mock = simpleMock(coreServices.rootInstanceMetadata, () => ({ + getInstalledPlugins: jest.fn(), + })); + export const factory = simpleFactoryWithOptions( + coreServices.rootInstanceMetadata, + rootInstanceMetadata, + ); + } } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 12e2ac4f1d..9b61cfc397 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,337 @@ # example-backend +## 0.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + - @backstage/plugin-app-backend@0.5.10 + - @backstage/plugin-techdocs-backend@2.1.4 + - @backstage/plugin-scaffolder-backend@3.1.1 + - @backstage/plugin-mcp-actions-backend@0.1.7 + - @backstage/plugin-auth-backend@0.26.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4 + - @backstage/plugin-devtools-backend@0.5.13 + - @backstage/plugin-auth-node@0.6.11 + - @backstage/plugin-permission-common@0.9.4 + - @backstage/plugin-permission-node@0.10.8 + - @backstage/plugin-catalog-backend@3.3.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10 + - @backstage/plugin-catalog-backend-module-openapi@0.2.18 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16 + - @backstage/plugin-events-backend@0.5.10 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.18 + - @backstage/plugin-search-backend@2.0.10 + - @backstage/plugin-search-backend-module-techdocs@0.4.10 + +## 0.0.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + - @backstage/plugin-mcp-actions-backend@0.1.7-next.1 + - @backstage/plugin-auth-backend@0.26.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.1 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-devtools-backend@0.5.13-next.1 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-scaffolder-backend@3.1.1-next.2 + - @backstage/plugin-search-backend@2.0.10-next.1 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.4-next.2 + +## 0.0.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.4-next.1 + - @backstage/plugin-catalog-backend@3.3.1-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.18-next.0 + - @backstage/plugin-scaffolder-backend@3.1.1-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.4-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.18-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.10-next.0 + +## 0.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/plugin-catalog-backend@3.3.1-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.10-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7 + - @backstage/plugin-devtools-backend@0.5.13-next.0 + - @backstage/plugin-events-backend@0.5.10-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-mcp-actions-backend@0.1.7-next.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-scaffolder-backend@3.1.1-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17 + - @backstage/plugin-search-backend@2.0.10-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.11 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9 + - @backstage/plugin-search-backend-module-explore@0.3.10 + - @backstage/plugin-search-backend-module-techdocs@0.4.9 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.4-next.0 + +## 0.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mcp-actions-backend@0.1.6 + - @backstage/plugin-scaffolder-backend@3.1.0 + - @backstage/plugin-search-backend-node@1.4.0 + - @backstage/plugin-auth-node@0.6.10 + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-app-backend@0.5.9 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3 + - @backstage/plugin-auth-backend@0.25.7 + - @backstage/plugin-catalog-backend@3.3.0 + - @backstage/plugin-devtools-backend@0.5.12 + - @backstage/plugin-events-backend@0.5.9 + - @backstage/plugin-kubernetes-backend@0.21.0 + - @backstage/plugin-notifications-backend@0.6.1 + - @backstage/plugin-permission-backend@0.7.7 + - @backstage/plugin-permission-node@0.10.7 + - @backstage/plugin-proxy-backend@0.6.9 + - @backstage/plugin-search-backend@2.0.9 + - @backstage/plugin-signals-backend@0.3.11 + - @backstage/plugin-techdocs-backend@2.1.3 + - @backstage/plugin-auth-backend-module-github-provider@0.4.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3 + - @backstage/plugin-search-backend-module-explore@0.3.10 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17 + - @backstage/plugin-search-backend-module-catalog@0.3.11 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9 + - @backstage/plugin-search-backend-module-techdocs@0.4.9 + +## 0.0.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mcp-actions-backend@0.1.6-next.1 + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-app-backend@0.5.9-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.1 + - @backstage/plugin-auth-backend@0.25.7-next.1 + - @backstage/plugin-auth-node@0.6.10-next.1 + - @backstage/plugin-catalog-backend@3.2.1-next.1 + - @backstage/plugin-devtools-backend@0.5.12-next.1 + - @backstage/plugin-events-backend@0.5.9-next.1 + - @backstage/plugin-kubernetes-backend@0.21.0-next.2 + - @backstage/plugin-notifications-backend@0.6.1-next.1 + - @backstage/plugin-permission-backend@0.7.7-next.1 + - @backstage/plugin-permission-node@0.10.7-next.1 + - @backstage/plugin-proxy-backend@0.6.9-next.1 + - @backstage/plugin-scaffolder-backend@3.1.0-next.1 + - @backstage/plugin-search-backend@2.0.9-next.1 + - @backstage/plugin-signals-backend@0.3.11-next.1 + - @backstage/plugin-techdocs-backend@2.1.3-next.2 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.10-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.2 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.1 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.11-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.9-next.1 + - @backstage/plugin-search-backend-node@1.4.0-next.1 + +## 0.0.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.4.0-next.0 + - @backstage/plugin-auth-node@0.6.10-next.0 + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-scaffolder-backend@3.1.0-next.0 + - @backstage/plugin-kubernetes-backend@0.20.5-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.6-next.0 + - @backstage/plugin-search-backend@2.0.9-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.11-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.9-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.10-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.9-next.0 + - @backstage/plugin-app-backend@0.5.9-next.0 + - @backstage/plugin-auth-backend@0.25.7-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.3.10-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.15-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.3-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.7-next.0 + - @backstage/plugin-permission-backend@0.7.7-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.15-next.0 + - @backstage/plugin-permission-node@0.10.7-next.0 + - @backstage/plugin-events-backend@0.5.9-next.0 + - @backstage/plugin-proxy-backend@0.6.9-next.0 + - @backstage/plugin-catalog-backend@3.2.1-next.0 + - @backstage/plugin-devtools-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.7-next.0 + - @backstage/plugin-notifications-backend@0.6.1-next.0 + - @backstage/plugin-signals-backend@0.3.11-next.0 + - @backstage/plugin-techdocs-backend@2.1.3-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.9-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.17-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.15-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.3-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.17-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-permission-common@0.9.3 + +## 0.0.44 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-backend@3.2.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.8 + - @backstage/plugin-kubernetes-backend@0.20.4 + - @backstage/plugin-techdocs-backend@2.1.2 + - @backstage/plugin-signals-backend@0.3.10 + - @backstage/plugin-notifications-backend@0.6.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.16 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-permission-backend@0.7.6 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/plugin-search-backend@2.0.8 + - @backstage/plugin-auth-backend@0.25.6 + - @backstage/plugin-scaffolder-backend@3.0.1 + - @backstage/plugin-search-backend-node@1.3.17 + - @backstage/plugin-events-backend@0.5.8 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8 + - @backstage/plugin-devtools-backend@0.5.11 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.6 + - @backstage/plugin-auth-node@0.6.9 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-mcp-actions-backend@0.1.5 + - @backstage/plugin-app-backend@0.5.8 + - @backstage/plugin-auth-backend-module-github-provider@0.3.9 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.14 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.6 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14 + - @backstage/plugin-permission-node@0.10.6 + - @backstage/plugin-proxy-backend@0.6.8 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16 + - @backstage/plugin-search-backend-module-catalog@0.3.10 + - @backstage/plugin-search-backend-module-explore@0.3.9 + - @backstage/plugin-search-backend-module-techdocs@0.4.8 + +## 0.0.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-backend@0.6.0-next.2 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-kubernetes-backend@0.20.4-next.2 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.2 + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-scaffolder-backend@3.0.1-next.1 + - @backstage/plugin-search-backend@2.0.8-next.1 + +## 0.0.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.2.0-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.7.8-next.1 + - @backstage/plugin-kubernetes-backend@0.20.4-next.1 + - @backstage/plugin-techdocs-backend@2.1.2-next.1 + - @backstage/plugin-signals-backend@0.3.10-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/plugin-notifications-backend@0.5.12-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.16-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-permission-backend@0.7.6-next.1 + - @backstage/plugin-permission-common@0.9.3-next.1 + - @backstage/plugin-search-backend@2.0.8-next.1 + - @backstage/plugin-auth-backend@0.25.6-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.8-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.14-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.6-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.5-next.1 + - @backstage/plugin-scaffolder-backend@3.0.1-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.8-next.1 + - @backstage/plugin-app-backend@0.5.8-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.3.9-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.14-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.2-next.1 + - @backstage/plugin-auth-node@0.6.9-next.1 + - @backstage/plugin-devtools-backend@0.5.11-next.1 + - @backstage/plugin-events-backend@0.5.8-next.1 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.6-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.14-next.1 + - @backstage/plugin-permission-node@0.10.6-next.1 + - @backstage/plugin-proxy-backend@0.6.8-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.16-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.9-next.1 + - @backstage/plugin-search-backend-node@1.3.17-next.1 + ## 0.0.44-next.0 ### Patch Changes diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index e07c266057..91d0f02dbc 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -9,7 +9,7 @@ # # Once the commands have been run, you can build the image using `yarn build-image` -FROM node:20-bookworm-slim +FROM node:24-trixie-slim # Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, # in which case you should also move better-sqlite3 to "devDependencies" in package.json. diff --git a/packages/backend/package.json b/packages/backend/package.json index d6efe662c7..52c58b0983 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.44-next.0", + "version": "0.0.46", "backstage": { "role": "backend" }, @@ -66,9 +66,9 @@ "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-signals-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", - "@opentelemetry/auto-instrumentations-node": "^0.61.0", - "@opentelemetry/exporter-prometheus": "^0.54.0", - "@opentelemetry/sdk-node": "^0.54.0", + "@opentelemetry/auto-instrumentations-node": "^0.67.0", + "@opentelemetry/exporter-prometheus": "^0.208.0", + "@opentelemetry/sdk-node": "^0.208.0", "example-app": "link:../app" }, "devDependencies": { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 541c3bd791..14eb36a3a6 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,6 +15,7 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { rootSystemMetadataServiceFactory } from '@backstage/backend-defaults/alpha'; import { coreServices, createBackendFeatureLoader, @@ -69,7 +70,7 @@ backend.add(searchLoader); backend.add(import('@backstage/plugin-techdocs-backend')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); -backend.add(import('./instanceMetadata')); +backend.add(rootSystemMetadataServiceFactory); backend.add(import('@backstage/plugin-events-backend-module-google-pubsub')); backend.add(import('@backstage/plugin-mcp-actions-backend')); diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts deleted file mode 100644 index 5f835f3117..0000000000 --- a/packages/backend/src/instanceMetadata.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; - -// Example usage of the instance metadata service to log the installed features. -export default createBackendPlugin({ - pluginId: 'instance-metadata-logging', - register(env) { - env.registerInit({ - deps: { - instanceMetadata: instanceMetadataServiceRef, - logger: coreServices.logger, - }, - async init({ instanceMetadata, logger }) { - logger.info( - `Installed features on this instance: ${instanceMetadata - .getInstalledFeatures() - .join(', ')}`, - ); - }, - }); - }, -}); diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 5eb97934f9..9ac3e62d06 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/catalog-client +## 1.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + ## 1.12.1-next.0 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index e5c63b1e0d..fefca00189 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.12.1-next.0", + "version": "1.12.1", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 9d42e9d675..2fb24c4943 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-model +## 1.7.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + ## 1.7.6-next.0 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 93a13b7edf..85c299e941 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "1.7.6-next.0", + "version": "1.7.6", "description": "Types and validators that help describe the model of a Backstage Catalog", "backstage": { "role": "common-library" diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index 4a7880688d..6f7ffadceb 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/cli-common +## 0.1.17 + +### Patch Changes + +- ae4dd5d: Move some of the symlink resolution to `isChildPath` + +## 0.1.16 + +### Patch Changes + +- 5cfb2a4: Added new `run`, `runOutput`, and `runCheck` utilities to help run child processes in a safe and portable way. +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- 2bae83a: Bumped dev dependencies `@types/node` + +## 0.1.16-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/errors@1.2.7 + +## 0.1.16-next.1 + +### Patch Changes + +- 5cfb2a4: Added new `run`, `runOutput`, and `runCheck` utilities to help run child processes in a safe and portable way. + +## 0.1.16-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks + ## 0.1.15 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index d8242fd325..eb41beffd6 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-common", - "version": "0.1.15", + "version": "0.1.17", "description": "Common functionality used by cli, backend, and create-app", "backstage": { "role": "node-library" @@ -34,8 +34,15 @@ "start": "backstage-cli package start", "test": "backstage-cli package test" }, + "dependencies": { + "@backstage/errors": "workspace:^", + "cross-spawn": "^7.0.3", + "global-agent": "^3.0.0", + "undici": "^7.2.3" + }, "devDependencies": { "@backstage/cli": "workspace:^", - "@types/node": "^20.16.0" + "@types/cross-spawn": "^6.0.2", + "@types/node": "^22.13.14" } } diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index 34f56c3f19..175ec3cb2b 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -3,9 +3,23 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ChildProcess } from 'child_process'; +import { CustomErrorBase } from '@backstage/errors'; +import { SpawnOptions } from 'child_process'; + // @public export const BACKSTAGE_JSON = 'backstage.json'; +// @public +export function bootstrapEnvProxyAgents(): void; + +// @public +export class ExitCodeError extends CustomErrorBase { + constructor(code: number, command?: string); + // (undocumented) + readonly code: number; +} + // @public export function findPaths(searchDir: string): Paths; @@ -26,4 +40,32 @@ export type Paths = { // @public export type ResolveFunc = (...paths: string[]) => string; + +// @public +export function run(args: string[], options?: RunOptions): RunChildProcess; + +// @public +export function runCheck(args: string[]): Promise; + +// @public +export interface RunChildProcess extends ChildProcess { + waitForExit(): Promise; +} + +// @public +export type RunOnOutput = (data: Buffer) => void; + +// @public +export type RunOptions = Omit & { + env?: Partial; + onStdout?: RunOnOutput; + onStderr?: RunOnOutput; + stdio?: SpawnOptions['stdio']; +}; + +// @public +export function runOutput( + args: string[], + options?: RunOptions, +): Promise; ``` diff --git a/packages/cli-common/src/errors.ts b/packages/cli-common/src/errors.ts new file mode 100644 index 0000000000..07664d2ffb --- /dev/null +++ b/packages/cli-common/src/errors.ts @@ -0,0 +1,34 @@ +/* + * 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 { CustomErrorBase } from '@backstage/errors'; + +/** + * Error thrown when a child process exits with a non-zero code. + * @public + */ +export class ExitCodeError extends CustomErrorBase { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index 8809764b43..d11601b044 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -23,3 +23,13 @@ export { findPaths, BACKSTAGE_JSON } from './paths'; export { isChildPath } from './isChildPath'; export type { Paths, ResolveFunc } from './paths'; +export { bootstrapEnvProxyAgents } from './proxyBootstrap'; +export { + run, + runOutput, + runCheck, + type RunChildProcess, + type RunOptions, + type RunOnOutput, +} from './run'; +export { ExitCodeError } from './errors'; diff --git a/packages/cli-common/src/isChildPath.ts b/packages/cli-common/src/isChildPath.ts index f66a0cfa94..ba680a6243 100644 --- a/packages/cli-common/src/isChildPath.ts +++ b/packages/cli-common/src/isChildPath.ts @@ -14,7 +14,49 @@ * limitations under the License. */ -import { relative, isAbsolute } from 'path'; +import { + relative, + isAbsolute, + resolve as resolvePath, + dirname, + basename, +} from 'path'; +import { realpathSync, lstatSync, readlinkSync } from 'fs'; + +// Resolves a path to its real location, following symlinks. +// Handles cases where the final target doesn't exist by recursively +// resolving parent directories. +function resolveRealPath(path: string): string { + try { + return realpathSync(path); + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + } + + // Check if path itself is a dangling symlink - recursively resolve the target + // to handle symlink chains (e.g., link1 -> link2 -> /outside) + try { + if (lstatSync(path).isSymbolicLink()) { + const target = resolvePath(dirname(path), readlinkSync(path)); + return resolveRealPath(target); + } + } catch (ex) { + if (ex.code !== 'ENOENT') { + throw ex; + } + } + + // Path doesn't exist - walk up the tree until we find an existing path, + // resolve it, then rebuild the non-existent portion on top + const parent = dirname(path); + if (parent === path) { + return path; // Hit filesystem root + } + + return resolvePath(resolveRealPath(parent), basename(path)); +} /** * Checks if path is the same as or a child path of base. @@ -22,7 +64,10 @@ import { relative, isAbsolute } from 'path'; * @public */ export function isChildPath(base: string, path: string): boolean { - const relativePath = relative(base, path); + const resolvedBase = resolveRealPath(base); + const resolvedPath = resolveRealPath(path); + + const relativePath = relative(resolvedBase, resolvedPath); if (relativePath === '') { // The same directory return true; diff --git a/packages/cli-common/src/proxyBootstrap.test.ts b/packages/cli-common/src/proxyBootstrap.test.ts new file mode 100644 index 0000000000..cefa1bd36e --- /dev/null +++ b/packages/cli-common/src/proxyBootstrap.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { bootstrapEnvProxyAgents } from './proxyBootstrap'; + +// Avoid mutating the global agents used in other tests +jest.mock('global-agent', () => ({ + bootstrap: jest.fn(), +})); +jest.mock('undici', () => ({ + setGlobalDispatcher: jest.fn(), + EnvHttpProxyAgent: jest.fn(), +})); + +describe('bootstrapEnvProxyAgents', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + jest.clearAllMocks(); + }); + + it('should bootstrap global-agent if GLOBAL_AGENT_HTTP_PROXY is set', () => { + process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + bootstrapEnvProxyAgents(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); + + it('should bootstrap global-agent if GLOBAL_AGENT_HTTPS_PROXY is set', () => { + process.env.GLOBAL_AGENT_HTTPS_PROXY = 'https://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + bootstrapEnvProxyAgents(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); + + it('should use undici EnvHttpProxyAgent if HTTP_PROXY is set', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com'; + + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + bootstrapEnvProxyAgents(); + + expect(EnvHttpProxyAgent).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledWith( + expect.any(EnvHttpProxyAgent), + ); + }); + + it('should use undici EnvHttpProxyAgent if HTTPS_PROXY is set', () => { + process.env.HTTPS_PROXY = 'https://proxy.example.com'; + + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + bootstrapEnvProxyAgents(); + + expect(EnvHttpProxyAgent).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledWith( + expect.any(EnvHttpProxyAgent), + ); + }); + + it('should not bootstrap global-agent or set undici dispatcher if no proxy is set', () => { + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + const { setGlobalDispatcher } = + require('undici') as typeof import('undici'); + + bootstrapEnvProxyAgents(); + + expect(bootstrap).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + }); + + it('should respect GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE', () => { + process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = 'CUSTOM_AGENT_'; + process.env.CUSTOM_AGENT_HTTP_PROXY = 'http://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + bootstrapEnvProxyAgents(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli-common/src/proxyBootstrap.ts b/packages/cli-common/src/proxyBootstrap.ts new file mode 100644 index 0000000000..b7375b6e55 --- /dev/null +++ b/packages/cli-common/src/proxyBootstrap.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 function can be called to setup undici and node-fetch Proxy agents. + * + * You can set GLOBAL_AGENT_HTTP(S)_PROXY to configure a proxy to be used in the + * CLIs. + * + * You can also configure a custom namespace by setting + * GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE which will replace the default + * "GLOBAL_AGENT_" env-var prefix. + * + * Make sure to call this function before any other imports. + * + * @public + */ +export function bootstrapEnvProxyAgents() { + // see https://www.npmjs.com/package/global-agent + const globalAgentNamespace = + process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_'; + if ( + process.env[`${globalAgentNamespace}HTTP_PROXY`] || + process.env[`${globalAgentNamespace}HTTPS_PROXY`] + ) { + const globalAgent = + require('global-agent') as typeof import('global-agent'); + globalAgent.bootstrap(); + } + + if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) { + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + setGlobalDispatcher(new EnvHttpProxyAgent()); + } +} diff --git a/packages/cli-common/src/run.test.ts b/packages/cli-common/src/run.test.ts new file mode 100644 index 0000000000..01c6c915d6 --- /dev/null +++ b/packages/cli-common/src/run.test.ts @@ -0,0 +1,342 @@ +/* + * 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 { run, runOutput, runCheck } from './run'; +import { ExitCodeError } from './errors'; + +describe('run', () => { + const activeChildren: Array<{ kill: () => void }> = []; + + afterEach(() => { + // Clean up any active child processes + activeChildren.forEach(child => { + try { + child.kill(); + } catch { + // Ignore errors during cleanup + } + }); + activeChildren.length = 0; + jest.restoreAllMocks(); + }); + + describe('run', () => { + it('should throw error for empty args', () => { + expect(() => run([])).toThrow('run requires at least one argument'); + }); + + it('should run a successful command', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + await expect(child.waitForExit()).resolves.not.toThrow(); + }); + + it('should throw ExitCodeError for non-zero exit code', async () => { + const child = run(['node', '--eval', 'process.exit(1)']); + activeChildren.push(child); + await expect(child.waitForExit()).rejects.toThrow(ExitCodeError); + await expect(child.waitForExit()).rejects.toThrow( + /Command 'node --eval process\.exit\(1\)' exited with code 1/, + ); + }); + + it('should call onStdout callback', async () => { + const stdoutChunks: Buffer[] = []; + const child = run(['node', '--version'], { + onStdout: data => stdoutChunks.push(data), + }); + activeChildren.push(child); + await child.waitForExit(); + expect(stdoutChunks.length).toBeGreaterThan(0); + const output = Buffer.concat(stdoutChunks).toString(); + expect(output).toMatch(/v\d+\.\d+\.\d+/); + }); + + it('should call onStderr callback', async () => { + const stderrChunks: Buffer[] = []; + const child = run(['node', '--eval', 'console.error("test error")'], { + onStderr: data => stderrChunks.push(data), + }); + activeChildren.push(child); + await child.waitForExit(); + expect(stderrChunks.length).toBeGreaterThan(0); + const output = Buffer.concat(stderrChunks).toString(); + expect(output).toContain('test error'); + }); + + it('should use custom stdio', async () => { + const child = run(['node', '--version'], { + stdio: 'pipe', + }); + activeChildren.push(child); + expect(child.stdout).toBeTruthy(); + expect(child.stderr).toBeTruthy(); + await child.waitForExit(); + }); + + it('should use custom env', async () => { + const customEnv = { CUSTOM_VAR: 'test-value' }; + const child = run( + ['node', '--eval', 'console.log(process.env.CUSTOM_VAR)'], + { + env: customEnv, + onStdout: data => { + const output = data.toString(); + expect(output).toContain('test-value'); + }, + }, + ); + activeChildren.push(child); + await child.waitForExit(); + }); + + it('should set FORCE_COLOR in env', async () => { + const child = run( + ['node', '--eval', 'console.log(process.env.FORCE_COLOR)'], + { + onStdout: data => { + const output = data.toString(); + expect(output.trim()).toBe('true'); + }, + }, + ); + activeChildren.push(child); + await child.waitForExit(); + }); + + it('should handle process already exited', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + // Wait for it to complete + await child.waitForExit(); + // Call waitForExit again - should return immediately + await expect(child.waitForExit()).resolves.not.toThrow(); + }); + + it('should handle multiple simultaneous calls to waitForExit', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + // Call waitForExit multiple times simultaneously + const [result1, result2, result3] = await Promise.all([ + child.waitForExit(), + child.waitForExit(), + child.waitForExit(), + ]); + // All should resolve successfully + expect(result1).toBeUndefined(); + expect(result2).toBeUndefined(); + expect(result3).toBeUndefined(); + }); + + it('should handle multiple simultaneous calls to waitForExit with error', async () => { + const child = run(['node', '--eval', 'process.exit(1)']); + activeChildren.push(child); + // Call waitForExit multiple times simultaneously + const promises = [ + child.waitForExit(), + child.waitForExit(), + child.waitForExit(), + ]; + // All should reject with the same error + for (const promise of promises) { + await expect(promise).rejects.toThrow(ExitCodeError); + await expect(promise).rejects.toThrow( + /Command 'node --eval process\.exit\(1\)' exited with code 1/, + ); + } + }); + + it('should handle signal handlers cleanup', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + const originalListeners = process.listenerCount('SIGINT'); + await child.waitForExit(); + // Signal handlers should be cleaned up + expect(process.listenerCount('SIGINT')).toBe(originalListeners); + }); + + it('should kill child process on SIGINT', async () => { + const child = run(['node', '--eval', 'setTimeout(() => {}, 10000)']); + activeChildren.push(child); + const killSpy = jest.spyOn(child, 'kill'); + // Start waiting (this registers signal handlers) + const waitPromise = child.waitForExit(); + // Simulate SIGINT + process.emit('SIGINT' as any, 'SIGINT'); + // Give it a moment + await new Promise(resolve => setTimeout(resolve, 100)); + expect(killSpy).toHaveBeenCalled(); + killSpy.mockRestore(); + // Clean up + child.kill(); + // Wait for cleanup + try { + await Promise.race([ + waitPromise, + new Promise(resolve => setTimeout(resolve, 100)), + ]); + } catch { + // Expected to fail + } + }); + + it('should kill child process on SIGTERM', async () => { + const child = run(['node', '--eval', 'setTimeout(() => {}, 10000)']); + activeChildren.push(child); + const killSpy = jest.spyOn(child, 'kill'); + // Start waiting (this registers signal handlers) + const waitPromise = child.waitForExit(); + // Simulate SIGTERM + process.emit('SIGTERM' as any, 'SIGTERM'); + // Give it a moment + await new Promise(resolve => setTimeout(resolve, 100)); + expect(killSpy).toHaveBeenCalled(); + killSpy.mockRestore(); + // Clean up + child.kill(); + // Wait for cleanup + try { + await Promise.race([ + waitPromise, + new Promise(resolve => setTimeout(resolve, 100)), + ]); + } catch { + // Expected to fail + } + }); + + it('should not kill already killed process on signal', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + await child.waitForExit(); + const killSpy = jest.spyOn(child, 'kill'); + // Simulate SIGINT after process has exited + process.emit('SIGINT' as any, 'SIGINT'); + await new Promise(resolve => setTimeout(resolve, 100)); + // Should not be called since process already exited + expect(killSpy).not.toHaveBeenCalled(); + killSpy.mockRestore(); + }); + + it('should handle process error', async () => { + const child = run(['nonexistent-command-12345']); + activeChildren.push(child); + await expect(child.waitForExit()).rejects.toThrow(); + }); + }); + + describe('runOutput', () => { + it('should throw error for empty args', async () => { + await expect(runOutput([])).rejects.toThrow( + 'runOutput requires at least one argument', + ); + }); + + it('should return stdout', async () => { + const output = await runOutput([ + 'node', + '--eval', + 'console.log("test output")', + ]); + expect(output).toBe('test output'); + }); + + it('should trim output', async () => { + const output = await runOutput([ + 'node', + '--eval', + 'console.log(" test output ")', + ]); + expect(output).toBe('test output'); + }); + + it('should attach stdout to error on failure', async () => { + let error: Error | undefined; + try { + await runOutput([ + 'node', + '--eval', + 'console.log("stdout before error"); process.exit(1)', + ]); + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(ExitCodeError); + expect((error as Error & { stdout?: string }).stdout).toContain( + 'stdout before error', + ); + }); + + it('should attach stderr to error on failure', async () => { + let error: Error | undefined; + try { + await runOutput([ + 'node', + '--eval', + 'console.error("stderr error"); process.exit(1)', + ]); + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(ExitCodeError); + expect((error as Error & { stderr?: string }).stderr).toContain( + 'stderr error', + ); + }); + + it('should call custom onStdout callback', async () => { + const customChunks: Buffer[] = []; + const output = await runOutput( + ['node', '--eval', 'console.log("test")'], + { + onStdout: data => customChunks.push(data), + }, + ); + expect(output).toBe('test'); + expect(customChunks.length).toBeGreaterThan(0); + }); + + it('should call custom onStderr callback', async () => { + const customChunks: Buffer[] = []; + await runOutput( + ['node', '--eval', 'console.error("error"); console.log("ok")'], + { + onStderr: data => customChunks.push(data), + }, + ); + expect(customChunks.length).toBeGreaterThan(0); + const errorOutput = Buffer.concat(customChunks).toString(); + expect(errorOutput).toContain('error'); + }); + }); + + describe('runCheck', () => { + it('should return true for successful command', async () => { + const result = await runCheck(['node', '--version']); + expect(result).toBe(true); + }); + + it('should return false for failed command', async () => { + const result = await runCheck(['node', '--eval', 'process.exit(1)']); + expect(result).toBe(false); + }); + + it('should return false for nonexistent command', async () => { + const result = await runCheck(['nonexistent-command-12345']); + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/cli-common/src/run.ts b/packages/cli-common/src/run.ts new file mode 100644 index 0000000000..9bdfbcd1f6 --- /dev/null +++ b/packages/cli-common/src/run.ts @@ -0,0 +1,219 @@ +/* + * 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 { ChildProcess, SpawnOptions } from 'child_process'; +import spawn from 'cross-spawn'; +import { ExitCodeError } from './errors'; +import { assertError } from '@backstage/errors'; + +/** + * Callback function that can be used to receive stdout or stderr data from a child process. + * + * @public + */ +export type RunOnOutput = (data: Buffer) => void; + +/** + * Options for running a child process with {@link run} or related functions. + * + * @public + */ +export type RunOptions = Omit & { + env?: Partial; + onStdout?: RunOnOutput; + onStderr?: RunOnOutput; + stdio?: SpawnOptions['stdio']; +}; + +/** + * Child process handle returned by {@link run}. + * + * @public + */ +export interface RunChildProcess extends ChildProcess { + /** + * Waits for the child process to exit. + * + * @remarks + * + * Resolves when the process exits successfully (exit code 0) or is terminated by a signal. + * If the process exits with a non-zero exit code, the promise is rejected with an {@link ExitCodeError}. + * + * @returns A promise that resolves when the process exits successfully or is terminated by a signal, or rejects on error. + */ + waitForExit(): Promise; +} + +/** + * Runs a command and returns a child process handle. + * + * @public + */ +export function run(args: string[], options: RunOptions = {}): RunChildProcess { + if (args.length === 0) { + throw new Error('run requires at least one argument'); + } + + const [name, ...cmdArgs] = args; + + const { onStdout, onStderr, stdio: customStdio, ...spawnOptions } = options; + const env: NodeJS.ProcessEnv = { + ...process.env, + FORCE_COLOR: 'true', + ...(options.env ?? {}), + }; + + const stdio = + customStdio ?? + ([ + 'inherit', + onStdout ? 'pipe' : 'inherit', + onStderr ? 'pipe' : 'inherit', + ] as ('inherit' | 'pipe')[]); + + const child = spawn(name, cmdArgs, { + ...spawnOptions, + stdio, + env, + }) as RunChildProcess; + + if (onStdout && child.stdout) { + child.stdout.on('data', onStdout); + } + if (onStderr && child.stderr) { + child.stderr.on('data', onStderr); + } + + const commandName = args.join(' '); + + let waitPromise: Promise | undefined; + + child.waitForExit = async (): Promise => { + if (waitPromise) { + return waitPromise; + } + + waitPromise = new Promise((resolve, reject) => { + if (typeof child.exitCode === 'number') { + if (child.exitCode) { + reject(new ExitCodeError(child.exitCode, commandName)); + } else { + resolve(); + } + return; + } + + function onError(error: Error) { + cleanup(); + reject(error); + } + + function onExit(code: number | null) { + cleanup(); + if (code) { + reject(new ExitCodeError(code, commandName)); + } else { + resolve(); + } + } + + function onSignal() { + if (!child.killed && child.exitCode === null) { + child.kill(); + } + } + + function cleanup() { + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.removeListener(signal, onSignal); + } + child.removeListener('error', onError); + child.removeListener('exit', onExit); + } + + child.once('error', onError); + child.once('exit', onExit); + + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.addListener(signal, onSignal); + } + }); + + return waitPromise; + }; + + return child; +} + +/** + * Runs a command and returns the stdout. + * + * @remarks + * + * On error, both stdout and stderr are attached to the error object as properties. + * + * @public + */ +export async function runOutput( + args: string[], + options?: RunOptions, +): Promise { + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + + if (args.length === 0) { + throw new Error('runOutput requires at least one argument'); + } + + try { + await run(args, { + ...options, + onStdout: data => { + stdoutChunks.push(data); + options?.onStdout?.(data); + }, + onStderr: data => { + stderrChunks.push(data); + options?.onStderr?.(data); + }, + }).waitForExit(); + + return Buffer.concat(stdoutChunks).toString().trim(); + } catch (error) { + assertError(error); + + (error as Error & { stdout?: string }).stdout = + Buffer.concat(stdoutChunks).toString(); + (error as Error & { stderr?: string }).stderr = + Buffer.concat(stderrChunks).toString(); + + throw error; + } +} + +/** + * Runs a command and returns true if it exits with code 0, false otherwise. + * + * @public + */ +export async function runCheck(args: string[]): Promise { + try { + await run(args).waitForExit(); + return true; + } catch { + return false; + } +} diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index 7921933482..bdacd69297 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/cli-node +## 0.2.16 + +### Patch Changes + +- 4e8c726: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## 0.2.16-next.1 + +### Patch Changes + +- 4e8c726: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.2.15 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + ## 0.2.15-next.0 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index abd7c959a7..52242bdd48 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.15-next.0", + "version": "0.2.16", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" diff --git a/packages/cli-node/src/git/GitUtils.ts b/packages/cli-node/src/git/GitUtils.ts index 7b87487c56..7edb3581d6 100644 --- a/packages/cli-node/src/git/GitUtils.ts +++ b/packages/cli-node/src/git/GitUtils.ts @@ -15,23 +15,27 @@ */ import { assertError, ForwardedError } from '@backstage/errors'; -import { execFile, paths } from '../util'; +import { paths } from '../paths'; +import { runOutput } from '@backstage/cli-common'; /** * Run a git command, trimming the output splitting it into lines. */ export async function runGit(...args: string[]) { try { - const { stdout } = await execFile('git', args, { - shell: true, + const stdout = await runOutput(['git', ...args], { cwd: paths.targetRoot, }); return stdout.trim().split(/\r\n|\r|\n/); } catch (error) { assertError(error); - if (error.stderr || typeof error.code === 'number') { - const stderr = (error.stderr as undefined | Buffer)?.toString('utf8'); - const msg = stderr?.trim() ?? `with exit code ${error.code}`; + if ( + 'code' in error && + typeof (error as { code?: number }).code === 'number' + ) { + const code = (error as { code?: number }).code; + const stderr = (error as { stderr?: string }).stderr; + const msg = stderr?.trim() ?? `with exit code ${code}`; throw new Error(`git ${args[0]} failed, ${msg}`); } throw new ForwardedError('Unknown execution error', error); @@ -83,10 +87,8 @@ export class GitUtils { // silently fall back to using the ref directly if merge base is not available } - const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], { - shell: true, + const stdout = await runOutput(['git', 'show', `${showRef}:${path}`], { cwd: paths.targetRoot, - maxBuffer: 1024 * 1024 * 50, }); return stdout; } diff --git a/packages/cli-node/src/monorepo/PackageGraph.test.ts b/packages/cli-node/src/monorepo/PackageGraph.test.ts index e2378b91c0..9bc9cbf72b 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.test.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.test.ts @@ -23,7 +23,7 @@ import { GitUtils } from '../git'; const mockListChangedFiles = jest.spyOn(GitUtils, 'listChangedFiles'); const mockReadFileAtRef = jest.spyOn(GitUtils, 'readFileAtRef'); -jest.mock('../util', () => ({ +jest.mock('../paths', () => ({ paths: { targetRoot: '/', resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths), diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 2f82af0808..76fbe9ba4f 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -16,7 +16,7 @@ import path from 'path'; import { getPackages, Package } from '@manypkg/get-packages'; -import { paths } from '../util'; +import { paths } from '../paths'; import { PackageRole } from '../roles'; import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index f47ac4b9ad..da78c8dd53 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { paths } from '../util'; +import { paths } from '../paths'; import fs from 'fs-extra'; /** diff --git a/packages/cli-node/src/monorepo/isMonorepo.test.ts b/packages/cli-node/src/monorepo/isMonorepo.test.ts index 30c2d3ccbc..de0dae0893 100644 --- a/packages/cli-node/src/monorepo/isMonorepo.test.ts +++ b/packages/cli-node/src/monorepo/isMonorepo.test.ts @@ -19,7 +19,7 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); -jest.mock('../util', () => ({ +jest.mock('../paths', () => ({ paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, })); diff --git a/packages/cli-node/src/pacman/PackageManager.test.ts b/packages/cli-node/src/pacman/PackageManager.test.ts index 3997da554c..dd35e74902 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -21,8 +21,8 @@ import { withLogCollector } from '@backstage/test-utils'; const mockDir = createMockDirectory(); -jest.mock('../util', () => ({ - ...jest.requireActual('../util'), +jest.mock('../paths', () => ({ + ...jest.requireActual('../paths'), paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, })); diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index fc0aaa2c97..44c0849705 100644 --- a/packages/cli-node/src/pacman/PackageManager.ts +++ b/packages/cli-node/src/pacman/PackageManager.ts @@ -16,7 +16,8 @@ import { Yarn } from './yarn'; import { Lockfile } from './Lockfile'; -import { SpawnOptionsPartialEnv, paths } from '../util'; +import { paths } from '../paths'; +import { RunOptions } from '@backstage/cli-common'; import fs from 'fs-extra'; /** @@ -55,7 +56,7 @@ export interface PackageManager { getMonorepoPackages(): Promise; /** Uses the package manager to run a command in the repo. */ - run(args: string[], options?: SpawnOptionsPartialEnv): Promise; + run(args: string[], options?: RunOptions): Promise; /** * Executes the package manager's pack command to bundle the repo into an diff --git a/packages/cli-node/src/pacman/yarn/Yarn.test.ts b/packages/cli-node/src/pacman/yarn/Yarn.test.ts index 9584ef54b2..9bf866b821 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.test.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.test.ts @@ -19,8 +19,8 @@ import { Yarn } from './Yarn'; const mockDir = createMockDirectory(); -jest.mock('../../util', () => ({ - ...jest.requireActual('../../util'), +jest.mock('../../paths', () => ({ + ...jest.requireActual('../../paths'), paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, })); diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index 0a2b8d55ea..a6544344ca 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -23,7 +23,8 @@ import { PackageInfo, PackageManager } from '../PackageManager'; import { Lockfile } from '../Lockfile'; import { YarnVersion } from './types'; import fs from 'fs-extra'; -import { paths, run, execFile, SpawnOptionsPartialEnv } from '../../util'; +import { paths } from '../../paths'; +import { run, runOutput, RunOptions } from '@backstage/cli-common'; export class Yarn implements PackageManager { constructor(private readonly yarnVersion: YarnVersion) {} @@ -63,8 +64,8 @@ export class Yarn implements PackageManager { }); } - async run(args: string[], options?: SpawnOptionsPartialEnv) { - await run('yarn', args, options); + async run(args: string[], options?: RunOptions) { + await run(['yarn', ...args], options).waitForExit(); } async fetchPackageInfo(): Promise { @@ -98,8 +99,7 @@ function detectYarnVersion(dir?: string): Promise { const promise = Promise.resolve().then(async () => { try { - const { stdout } = await execFile('yarn', ['--version'], { - shell: true, + const stdout = await runOutput(['yarn', '--version'], { cwd, }); const versionString = stdout.trim(); @@ -109,9 +109,6 @@ function detectYarnVersion(dir?: string): Promise { return { version: versionString, codename }; } catch (error) { assertError(error); - if ('stderr' in error) { - process.stderr.write(error.stderr as Buffer); - } throw new ForwardedError('Failed to determine yarn version', error); } }); diff --git a/packages/cli-node/src/paths.ts b/packages/cli-node/src/paths.ts new file mode 100644 index 0000000000..2c658c27b3 --- /dev/null +++ b/packages/cli-node/src/paths.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. + */ + +import { findPaths } from '@backstage/cli-common'; + +/* eslint-disable-next-line no-restricted-syntax */ +export const paths = findPaths(__dirname); diff --git a/packages/cli-node/src/util.ts b/packages/cli-node/src/util.ts deleted file mode 100644 index eaf05855b1..0000000000 --- a/packages/cli-node/src/util.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ChildProcess, - execFile as execFileCb, - spawn, - SpawnOptions, -} from 'child_process'; -import { promisify } from 'util'; -import { findPaths } from '@backstage/cli-common'; -import { ExitCodeError } from './errors'; - -export const execFile = promisify(execFileCb); - -/* eslint-disable-next-line no-restricted-syntax */ -export const paths = findPaths(__dirname); - -/** - * A function that can be used to log data from a child process - * - * @public - */ -export type LogFunc = (data: Buffer) => void; - -/** - * Options for running a child process - * - * @public - */ -export type SpawnOptionsPartialEnv = Omit & { - env?: Partial; - // Pipe stdout to this log function - stdoutLogFunc?: LogFunc; - // Pipe stderr to this log function - stderrLogFunc?: LogFunc; -}; - -// Runs a child command, returning a promise that is only resolved if the child exits with code 0. -export async function run( - name: string, - args: string[] = [], - options: SpawnOptionsPartialEnv = {}, -) { - const { stdoutLogFunc, stderrLogFunc } = options; - const env: NodeJS.ProcessEnv = { - ...process.env, - FORCE_COLOR: 'true', - ...(options.env ?? {}), - }; - - const stdio = [ - 'inherit', - stdoutLogFunc ? 'pipe' : 'inherit', - stderrLogFunc ? 'pipe' : 'inherit', - ] as ('inherit' | 'pipe')[]; - - const child = spawn(name, args, { - stdio, - shell: true, - ...options, - env, - }); - - if (stdoutLogFunc && child.stdout) { - child.stdout.on('data', stdoutLogFunc); - } - if (stderrLogFunc && child.stderr) { - child.stderr.on('data', stderrLogFunc); - } - - await waitForExit(child, name); -} - -async function waitForExit( - child: ChildProcess & { exitCode: number | null }, - name?: string, -): Promise { - if (typeof child.exitCode === 'number') { - if (child.exitCode) { - throw new ExitCodeError(child.exitCode, name); - } - return; - } - - await new Promise((resolve, reject) => { - child.once('error', error => reject(error)); - child.once('exit', code => { - if (code) { - reject(new ExitCodeError(code, name)); - } else { - resolve(); - } - }); - }); -} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 5c18ed6c8a..7ee344b24a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,183 @@ # @backstage/cli +## 0.35.2 + +### Patch Changes + +- 320c6a9: Bump `@swc/core` to support `ES2023` and `ES2024` +- c0d7bf6: Added `--include` and `--format` options to `backstage-cli info` command for including additional packages via glob patterns and outputting as JSON or Text. +- f6a5d2f: Fixed CSS module class name collisions when running multiple versions of packages simultaneously by using content-based hashing for class name generation. +- 140cbc2: Added `@backstage/backend-test-utils` to backend package templates. +- 4eeba9e: Upgrade `zod-validation-error` to version 4 +- 9ee5996: Bump minimum required `@swc/core` to avoid transpilation bug +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/integration@1.19.2 + +## 0.35.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## 0.35.2-next.0 + +### Patch Changes + +- 320c6a9: Bump `@swc/core` to support `ES2023` and `ES2024` +- 9ee5996: Bump minimum required `@swc/core` to avoid transpilation bug +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/cli-common@0.1.16 + - @backstage/cli-node@0.2.16 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.7 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/integration@1.19.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## 0.35.0 + +### Minor Changes + +- f6f22a9: Provide `--no-node-snapshot` by default when running the `package start` or `package test`. You can disable this behavior by providing `NODE_OPTIONS='--node-snapshot'`. +- f8dff94: Switched the default module resolution to `bundler` and the `module` setting to `ES2020`. + + You may need to bump some dependencies as part of this change and fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages. + + You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript. + + A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field. + +- cd0b8a1: **BREAKING**: `jest` is now a peer dependency. If you run tests using Backstage CLI, you must add Jest and its environment dependencies as `devDependencies` in your project. + + You can choose to install either Jest 29 or Jest 30. The built-in Jest version before this change was Jest 29, however, we recommend that you switch to Jest 30. Upgrading will solve the `Could not parse CSS stylesheet` errors, allow you to use MSW v2 in web packages, and ensure that you remain compatible with future versions of the Backstage CLI. Support for Jest 29 is temporary, with the purpose of allowing you to upgrade at your own pace, but it will eventually be removed. + + - **Jest 29**: Install `jest@^29` and `jest-environment-jsdom@^29`. No migration needed, but you may see `Could not parse CSS stylesheet` warnings/errors when testing components from `@backstage/ui` or other packages using CSS `@layer` declarations. + - **Jest 30**: Install `jest@^30`, `@jest/environment-jsdom-abstract@^30`, and `jsdom@^27`. Fixes the stylesheet parsing warnings/errors, but requires migration steps. + + See the [Jest 30 migration guide](https://backstage.io/docs/tutorials/jest30-migration) for detailed migration instructions. + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- e7db290: Add missing peer/dev dependencies to the frontend plugin template. + + `react-dom` was not declared as a peer dependency, causing module resolution + errors when generating plugins outside a Backstage monorepo. This adds + `react-dom` to `peerDependencies` (for consuming apps) and `devDependencies` + (for local development). `react-router-dom` is also added to `peerDependencies` (for consuming apps) and `devDependencies` + to support routing during plugin development. + + Fixes: + + - Module not found: Can't resolve 'react-dom' + - Module not found: Can't resolve 'react-router-dom' + +- 1226647: Updated dependency `esbuild` to `^0.27.0`. +- f89a074: Updated dependency `@pmmmwh/react-refresh-webpack-plugin` to `^0.6.0`. +- 2b81751: Updated dependency `webpack` to `~5.103.0`. +- fafd9e1: Fixed internal usage of `yargs`. +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- 2bae83a: Switched compilation target to ES2022 in order to match the new set of supported Node.js versions, which are 22 and 24. + + The TypeScript compilation target has been set to ES2022, because setting it to a higher target will break projects on older TypeScript versions. If you use a newer TypeScript version in your own project, you can bump `compilerOptions.target` to ES2023 or ES2024 in your own `tsconfig.json` file. + +- 7fbac5c: Updated to use new utilities from `@backstage/cli-common`. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/integration@1.19.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + +## 0.35.0-next.2 + +### Minor Changes + +- f8dff94: Switched the default module resolution to `bundler` and the `module` setting to `ES2020`. + + You may need to bump some dependencies as part of this change and fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages. + + You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript. + + A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field. + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 1226647: Updated dependency `esbuild` to `^0.27.0`. +- f89a074: Updated dependency `@pmmmwh/react-refresh-webpack-plugin` to `^0.6.0`. +- 2b81751: Updated dependency `webpack` to `~5.103.0`. +- fafd9e1: Fixed internal usage of `yargs`. +- 2bae83a: Switched ECMAScript version to ES2023. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/integration@1.18.3-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## 0.34.6-next.1 + +### Patch Changes + +- 7fbac5c: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-node@0.2.16-next.1 + - @backstage/cli-common@0.1.16-next.1 + +## 0.34.6-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config@1.3.6 + - @backstage/release-manifests@0.0.13 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.0 + - @backstage/types@1.2.2 + +## 0.34.5 + +### Patch Changes + +- fc7cbfc: The templates executed with the `yarn new` command now supports templating filenames. +- da19cb5: Fix inconsistent behavior in the `new` command for the `@internal` scope: it now consistently defaults to the `backstage-plugin-` infix whether the `--scope` option is not set or it's set to `internal`. +- fc7cbfc: Added a template for the `yarn new` command to create an catalog entity provider. To add this template to an explicit list in the root `package.json`, use `@backstage/cli/templates/catalog-provider-module`. +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 279279c: Fixes an issue where using the `backstage-cli new --scope` command with a scope that already includes the `@` symbol (e.g., `@backstage-community`) would result in a double `@@` prefix in the generated package name, causing invalid `package.json` files. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/eslint-plugin@0.2.0 + - @backstage/config-loader@1.10.6 + - @backstage/config@1.3.6 + - @backstage/cli-node@0.2.15 + - @backstage/catalog-model@1.7.6 + +## 0.34.5-next.1 + +### Patch Changes + +- da19cb5: Fix inconsistent behavior in the `new` command for the `@internal` scope: it now consistently defaults to the `backstage-plugin-` infix whether the `--scope` option is not set or it's set to `internal`. +- b2bef92: Convert all enums to erasable-syntax compliant patterns + ## 0.34.5-next.0 ### Patch Changes diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index a8df5242ad..8979e91a82 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -148,7 +148,9 @@ Options: Usage: Options: + --format --help + --include --version ``` @@ -324,7 +326,7 @@ Options: ### `backstage-cli package test` ``` -Usage: backstage-cli [--config=] [TestPathPattern] +Usage: backstage-cli [--config=] [TestPathPatterns] Options: --all @@ -349,7 +351,6 @@ Options: --debug --detectLeaks --detectOpenHandles - --env --errorOnDeprecated --filter --findRelatedTests @@ -359,7 +360,6 @@ Options: --globals --haste --ignoreProjects - --init --injectGlobals --json --lastCommit @@ -400,13 +400,13 @@ Options: --silent --skipFilter --snapshotSerializers - --testEnvironment + --testEnvironment, --env --testEnvironmentOptions --testFailureExitCode --testLocationInResults --testMatch --testPathIgnorePatterns - --testPathPattern + --testPathPatterns --testRegex --testResultsProcessor --testRunner @@ -418,6 +418,7 @@ Options: --useStderr --verbose --version + --waitForUnhandledRejections --watch --watchAll --watchPathIgnorePatterns diff --git a/packages/cli/config/getJestEnvironment.js b/packages/cli/config/getJestEnvironment.js new file mode 100644 index 0000000000..aac2f79021 --- /dev/null +++ b/packages/cli/config/getJestEnvironment.js @@ -0,0 +1,49 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function getJestMajorVersion() { + const jestVersion = require('jest/package.json').version; + const majorVersion = parseInt(jestVersion.split('.')[0], 10); + return majorVersion; +} + +function getJestEnvironment() { + const majorVersion = getJestMajorVersion(); + + if (majorVersion >= 30) { + try { + require.resolve('@jest/environment-jsdom-abstract'); + require.resolve('jsdom'); + } catch { + throw new Error( + 'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' + + 'Please install them as dev dependencies.', + ); + } + return require.resolve('./jest-environment-jsdom'); + } + try { + require.resolve('jest-environment-jsdom'); + } catch { + throw new Error( + 'Jest 29 requires jest-environment-jsdom. ' + + 'Please install it as a dev dependency.', + ); + } + return require.resolve('jest-environment-jsdom'); +} + +module.exports = { getJestMajorVersion, getJestEnvironment }; diff --git a/packages/cli/config/jest-environment-jsdom/index.js b/packages/cli/config/jest-environment-jsdom/index.js new file mode 100644 index 0000000000..9d8b76eaf5 --- /dev/null +++ b/packages/cli/config/jest-environment-jsdom/index.js @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default; +const jsdom = require('jsdom'); + +/** + * A custom JSDOM environment that extends the abstract base and applies + * fixes for Web API globals that are missing or incorrectly implemented + * in JSDOM. + * + * Based on https://github.com/mswjs/jest-fixed-jsdom + */ +class FixedJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + + // Fix Web API globals that JSDOM doesn't properly expose + this.global.TextDecoder = TextDecoder; + this.global.TextEncoder = TextEncoder; + this.global.TextDecoderStream = TextDecoderStream; + this.global.TextEncoderStream = TextEncoderStream; + this.global.ReadableStream = ReadableStream; + + this.global.Blob = Blob; + this.global.Headers = Headers; + this.global.FormData = FormData; + this.global.Request = Request; + this.global.Response = Response; + this.global.fetch = fetch; + this.global.AbortController = AbortController; + this.global.AbortSignal = AbortSignal; + this.global.structuredClone = structuredClone; + this.global.URL = URL; + this.global.URLSearchParams = URLSearchParams; + + this.global.BroadcastChannel = BroadcastChannel; + this.global.TransformStream = TransformStream; + this.global.WritableStream = WritableStream; + + // Needed to ensure `e instanceof Error` works as expected with errors thrown from + // any of the native APIs above. Without this, the JSDOM `Error` is what the test + // code will use for comparison with `e`, which fails the instanceof check. + this.global.Error = Error; + } +} + +module.exports = FixedJSDOMEnvironment; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index f011d14b19..94e5b9c4a8 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -20,6 +20,10 @@ const crypto = require('crypto'); const glob = require('util').promisify(require('glob')); const { version } = require('../package.json'); const paths = require('@backstage/cli-common').findPaths(process.cwd()); +const { + getJestEnvironment, + getJestMajorVersion, +} = require('./getJestEnvironment'); const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs']; @@ -211,7 +215,7 @@ function getRoleConfig(role, pkgJson) { }; if (FRONTEND_ROLES.includes(role)) { return { - testEnvironment: require.resolve('jest-environment-jsdom'), + testEnvironment: getJestEnvironment(), // The caching module loader is only used to speed up frontend tests, // as it breaks real dynamic imports of ESM modules. runtime: envOptions.oldTests @@ -279,7 +283,10 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) { ); } - if (options.testEnvironment === require.resolve('jest-environment-jsdom')) { + if ( + options.testEnvironment === getJestEnvironment() && + getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+ + ) { // FIXME https://github.com/jsdom/jsdom/issues/1724 options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill')); } diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js index a3fdf106e9..5652ff67e7 100644 --- a/packages/cli/config/jestCachingModuleLoader.js +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -14,6 +14,8 @@ * limitations under the License. */ +// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version +// eslint-disable-next-line @backstage/no-undeclared-imports const { default: JestRuntime } = require('jest-runtime'); module.exports = class CachingJestRuntime extends JestRuntime { diff --git a/packages/cli/config/jestRejectNetworkRequests.js b/packages/cli/config/jestRejectNetworkRequests.js index de6b486809..ef577b3a22 100644 --- a/packages/cli/config/jestRejectNetworkRequests.js +++ b/packages/cli/config/jestRejectNetworkRequests.js @@ -22,7 +22,7 @@ const errorMessage = 'Network requests are not allowed in tests'; const origHttpAgent = http.globalAgent; const origHttpsAgent = https.globalAgent; const origFetch = global.fetch; -const origXMLHttpRequest = global.fetch; +const origXMLHttpRequest = global.XMLHttpRequest; http.globalAgent = new http.Agent({ lookup() { @@ -36,10 +36,21 @@ https.globalAgent = new https.Agent({ }, }); +const BLOCKING_FETCH_SYMBOL = Symbol.for( + 'backstage.jestRejectNetworkRequests.blockingFetch', +); + if (global.fetch) { - global.fetch = async () => { - throw new Error(errorMessage); + const blockingFetch = async (input, init) => { + // If global.fetch still has our marker, block the request + if (global.fetch[BLOCKING_FETCH_SYMBOL]) { + throw new Error(errorMessage); + } + // MSW (or something else) wrapped us - pass through + return origFetch(input, init); }; + blockingFetch[BLOCKING_FETCH_SYMBOL] = true; + global.fetch = blockingFetch; } if (global.XMLHttpRequest) { diff --git a/packages/cli/config/nodeTransform.cjs b/packages/cli/config/nodeTransform.cjs index f54527b4a7..54984b74a4 100644 --- a/packages/cli/config/nodeTransform.cjs +++ b/packages/cli/config/nodeTransform.cjs @@ -61,7 +61,7 @@ addHook( ignoreDynamic: true, }, jsc: { - target: 'es2022', + target: 'es2023', parser: { syntax: 'typescript', }, diff --git a/packages/cli/config/nodeTransformHooks.mjs b/packages/cli/config/nodeTransformHooks.mjs index 7bf0473bb8..7e00eff9f6 100644 --- a/packages/cli/config/nodeTransformHooks.mjs +++ b/packages/cli/config/nodeTransformHooks.mjs @@ -277,7 +277,7 @@ export async function load(url, context, nextLoad) { exportInteropAnnotation: true, }, jsc: { - target: 'es2022', + target: 'es2023', parser: { syntax: 'typescript', }, diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json index ac5e62b52e..40a572da4d 100644 --- a/packages/cli/config/tsconfig.json +++ b/packages/cli/config/tsconfig.json @@ -12,9 +12,9 @@ "incremental": true, "isolatedModules": true, "jsx": "react", - "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2022"], - "module": "ESNext", - "moduleResolution": "node", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2023"], + "module": "ES2020", + "moduleResolution": "bundler", "noEmit": false, "noFallthroughCasesInSwitch": true, "noImplicitAny": true, diff --git a/packages/cli/package.json b/packages/cli/package.json index a3ed006a3d..0669dadc5e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.5-next.0", + "version": "0.35.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -70,10 +70,9 @@ "@spotify/eslint-config-base": "^15.0.0", "@spotify/eslint-config-react": "^15.0.0", "@spotify/eslint-config-typescript": "^15.0.0", - "@swc/core": "^1.3.46", - "@swc/helpers": "^0.5.0", - "@swc/jest": "^0.2.22", - "@types/jest": "^29.5.11", + "@swc/core": "^1.15.6", + "@swc/helpers": "^0.5.17", + "@swc/jest": "^0.2.39", "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", @@ -88,7 +87,7 @@ "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", "eslint-formatter-friendly": "^7.0.0", @@ -100,7 +99,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unused-imports": "^4.1.4", "eslint-rspack-plugin": "^4.2.1", - "express": "^4.17.1", + "express": "^4.22.0", "fs-extra": "^11.2.0", "git-url-parse": "^15.0.0", "glob": "^7.1.7", @@ -109,11 +108,7 @@ "handlebars": "^4.7.3", "html-webpack-plugin": "^5.6.3", "inquirer": "^8.2.0", - "jest": "^29.7.0", - "jest-cli": "^29.7.0", "jest-css-modules": "^2.1.0", - "jest-environment-jsdom": "^29.0.2", - "jest-runtime": "^29.0.2", "json-schema": "^0.4.0", "lodash": "^4.17.21", "minimatch": "^9.0.0", @@ -148,7 +143,7 @@ "yml-loader": "^2.1.0", "yn": "^4.0.0", "zod": "^3.22.4", - "zod-validation-error": "^3.4.0" + "zod-validation-error": "^4.0.2" }, "devDependencies": { "@backstage/backend-plugin-api": "workspace:^", @@ -167,14 +162,16 @@ "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", + "@jest/environment-jsdom-abstract": "^30.0.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@types/cross-spawn": "^6.0.2", "@types/ejs": "^3.1.3", "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0", "@types/http-proxy": "^1.17.4", "@types/inquirer": "^8.1.3", - "@types/node": "^20.16.0", + "@types/jest": "^30.0.0", + "@types/node": "^22.13.14", "@types/npm-packlist": "^3.0.0", "@types/recursive-readdir": "^2.2.0", "@types/rollup-plugin-peer-deps-external": "^2.2.0", @@ -188,25 +185,34 @@ "esbuild-loader": "^4.0.0", "eslint-webpack-plugin": "^4.2.0", "fork-ts-checker-webpack-plugin": "^9.0.0", + "jest": "^30.2.0", + "jsdom": "^27.1.0", "mini-css-extract-plugin": "^2.4.2", "msw": "^1.0.0", "nodemon": "^3.0.1", "terser-webpack-plugin": "^5.1.3", - "webpack": "~5.96.0", + "webpack": "~5.103.0", "webpack-dev-server": "^5.0.0" }, "peerDependencies": { + "@jest/environment-jsdom-abstract": "^30.0.0", "@module-federation/enhanced": "^0.9.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "esbuild-loader": "^4.0.0", "eslint-webpack-plugin": "^4.2.0", "fork-ts-checker-webpack-plugin": "^9.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-environment-jsdom": "*", + "jsdom": "^27.1.0", "mini-css-extract-plugin": "^2.4.2", "terser-webpack-plugin": "^5.1.3", - "webpack": "~5.96.0", + "webpack": "~5.103.0", "webpack-dev-server": "^5.0.0" }, "peerDependenciesMeta": { + "@jest/environment-jsdom-abstract": { + "optional": true + }, "@module-federation/enhanced": { "optional": true }, @@ -222,6 +228,12 @@ "fork-ts-checker-webpack-plugin": { "optional": true }, + "jest-environment-jsdom": { + "optional": true + }, + "jsdom": { + "optional": true + }, "mini-css-extract-plugin": { "optional": true }, diff --git a/packages/cli/src/lib/run.ts b/packages/cli/src/lib/run.ts deleted file mode 100644 index e8602fcb18..0000000000 --- a/packages/cli/src/lib/run.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - SpawnOptions, - spawn, - ChildProcess, - execFile as execFileCb, -} from 'child_process'; -import { ExitCodeError } from './errors'; -import { promisify } from 'util'; -import { assertError, ForwardedError } from '@backstage/errors'; - -export const execFile = promisify(execFileCb); - -type LogFunc = (data: Buffer) => void; - -type SpawnOptionsPartialEnv = Omit & { - env?: Partial; - // Pipe stdout to this log function - stdoutLogFunc?: LogFunc; - // Pipe stderr to this log function - stderrLogFunc?: LogFunc; -}; - -// Runs a child command, returning a promise that is only resolved if the child exits with code 0. -export async function run( - name: string, - args: string[] = [], - options: SpawnOptionsPartialEnv = {}, -) { - const { stdoutLogFunc, stderrLogFunc } = options; - const env: NodeJS.ProcessEnv = { - ...process.env, - FORCE_COLOR: 'true', - ...(options.env ?? {}), - }; - - const stdio = [ - 'inherit', - stdoutLogFunc ? 'pipe' : 'inherit', - stderrLogFunc ? 'pipe' : 'inherit', - ] as ('inherit' | 'pipe')[]; - - const child = spawn(name, args, { - stdio, - shell: true, - ...options, - env, - }); - - if (stdoutLogFunc && child.stdout) { - child.stdout.on('data', stdoutLogFunc); - } - if (stderrLogFunc && child.stderr) { - child.stderr.on('data', stderrLogFunc); - } - - await waitForExit(child, name); -} - -export async function runPlain(cmd: string, ...args: string[]) { - try { - const { stdout } = await execFile(cmd, args, { shell: true }); - return stdout.trim(); - } catch (error) { - assertError(error); - if ('stderr' in error) { - process.stderr.write(error.stderr as Buffer); - } - if (typeof error.code === 'number') { - throw new ExitCodeError(error.code, [cmd, ...args].join(' ')); - } - throw new ForwardedError('Unknown execution error', error); - } -} - -export async function runCheck(cmd: string, ...args: string[]) { - try { - await execFile(cmd, args, { shell: true }); - return true; - } catch (error) { - return false; - } -} - -export async function waitForExit( - child: ChildProcess & { exitCode: number | null }, - name?: string, -): Promise { - if (typeof child.exitCode === 'number') { - if (child.exitCode) { - throw new ExitCodeError(child.exitCode, name); - } - return; - } - - await new Promise((resolve, reject) => { - child.once('error', error => reject(error)); - child.once('exit', code => { - if (code) { - reject(new ExitCodeError(code, name)); - } else { - resolve(); - } - }); - }); -} diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index de6e7e1ed2..e823ce8769 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -14,16 +14,17 @@ * limitations under the License. */ -import * as runObj from '../run'; +import * as runObj from '@backstage/cli-common'; import * as yarn from './yarn'; import { fetchPackageInfo, mapDependencies } from './packages'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; -jest.mock('../run', () => { +jest.mock('@backstage/cli-common', () => { + const actual = jest.requireActual('@backstage/cli-common'); return { - run: jest.fn(), - execFile: jest.fn(), + ...actual, + runOutput: jest.fn(), }; }); @@ -39,42 +40,40 @@ describe('fetchPackageInfo', () => { }); it('should forward info for yarn classic', async () => { - jest.spyOn(runObj, 'execFile').mockResolvedValue({ - stdout: `{"type":"inspect","data":{"the":"data"}}`, - stderr: '', - }); + jest + .spyOn(runObj, 'runOutput') + .mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`); jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic'); await expect(fetchPackageInfo('my-package')).resolves.toEqual({ the: 'data', }); - expect(runObj.execFile).toHaveBeenCalledWith( + expect(runObj.runOutput).toHaveBeenCalledWith([ 'yarn', - ['info', '--json', 'my-package'], - { shell: true }, - ); + 'info', + '--json', + 'my-package', + ]); }); it('should forward info for yarn berry', async () => { - jest - .spyOn(runObj, 'execFile') - .mockResolvedValue({ stdout: `{"the":"data"}`, stderr: '' }); + jest.spyOn(runObj, 'runOutput').mockResolvedValue(`{"the":"data"}`); jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry'); await expect(fetchPackageInfo('my-package')).resolves.toEqual({ the: 'data', }); - expect(runObj.execFile).toHaveBeenCalledWith( + expect(runObj.runOutput).toHaveBeenCalledWith([ 'yarn', - ['npm', 'info', '--json', 'my-package'], - { shell: true }, - ); + 'npm', + 'info', + '--json', + 'my-package', + ]); }); it('should throw if no info with yarn classic', async () => { - jest - .spyOn(runObj, 'execFile') - .mockResolvedValue({ stdout: '', stderr: '' }); + jest.spyOn(runObj, 'runOutput').mockResolvedValue(''); jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic'); await expect(fetchPackageInfo('my-package')).rejects.toThrow( @@ -83,9 +82,10 @@ describe('fetchPackageInfo', () => { }); it('should throw if no info with yarn berry', async () => { - jest - .spyOn(runObj, 'execFile') - .mockRejectedValue({ stdout: 'bla bla bla Response Code: 404 bla bla' }); + const error = new Error('Command failed'); + (error as Error & { stdout?: string }).stdout = + 'bla bla bla Response Code: 404 bla bla'; + jest.spyOn(runObj, 'runOutput').mockRejectedValue(error); jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry'); await expect(fetchPackageInfo('my-package')).rejects.toThrow( diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 223f6b8aab..11f1ba3041 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -17,7 +17,7 @@ import { minimatch } from 'minimatch'; import { getPackages } from '@manypkg/get-packages'; import { detectYarnVersion } from './yarn'; -import { execFile } from '../run'; +import { runOutput } from '@backstage/cli-common'; import { NotFoundError } from '@backstage/errors'; const DEP_TYPES = [ @@ -54,11 +54,7 @@ export async function fetchPackageInfo( const cmd = yarnVersion === 'classic' ? ['info'] : ['npm', 'info']; try { - const { stdout: output } = await execFile( - 'yarn', - [...cmd, '--json', name], - { shell: true }, - ); + const output = await runOutput(['yarn', ...cmd, '--json', name]); if (!output) { throw new NotFoundError( @@ -81,7 +77,12 @@ export async function fetchPackageInfo( throw error; } - if (error?.stdout.includes('Response Code: 404')) { + if ( + error instanceof Error && + 'stdout' in error && + typeof error.stdout === 'string' && + error.stdout.includes('Response Code: 404') + ) { throw new NotFoundError( `No package information found for package ${name}`, ); diff --git a/packages/cli/src/lib/versioning/yarn.ts b/packages/cli/src/lib/versioning/yarn.ts index b6c0383bc2..908ddca949 100644 --- a/packages/cli/src/lib/versioning/yarn.ts +++ b/packages/cli/src/lib/versioning/yarn.ts @@ -15,10 +15,7 @@ */ import { assertError, ForwardedError } from '@backstage/errors'; -import { execFile as execFileCb } from 'child_process'; -import { promisify } from 'util'; - -const execFile = promisify(execFileCb); +import { runOutput } from '@backstage/cli-common'; const versions = new Map>(); @@ -30,16 +27,12 @@ export function detectYarnVersion(dir?: string): Promise<'classic' | 'berry'> { const promise = Promise.resolve().then(async () => { try { - const { stdout } = await execFile('yarn', ['--version'], { - shell: true, + const stdout = await runOutput(['yarn', '--version'], { cwd, }); return stdout.trim().startsWith('1.') ? 'classic' : 'berry'; } catch (error) { assertError(error); - if ('stderr' in error) { - process.stderr.write(error.stderr as Buffer); - } throw new ForwardedError('Failed to determine yarn version', error); } }); diff --git a/packages/cli/src/modules/build/lib/builder/config.ts b/packages/cli/src/modules/build/lib/builder/config.ts index 8add3559f4..7776571e43 100644 --- a/packages/cli/src/modules/build/lib/builder/config.ts +++ b/packages/cli/src/modules/build/lib/builder/config.ts @@ -16,7 +16,9 @@ import chalk from 'chalk'; import fs from 'fs-extra'; +import { createHash } from 'crypto'; import { + basename, extname, relative as relativePath, resolve as resolvePath, @@ -239,7 +241,18 @@ export async function makeRollupConfigs( include: /node_modules/, exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/], }), - postcss(), + postcss({ + modules: { + generateScopedName(name: string, filename: string, css: string) { + const hash = createHash('md5') + .update(css) + .digest('hex') + .slice(0, 10); + const file = basename(filename, '.module.css'); + return `${file}_${name}__${hash}`; + }, + }, + }), forwardFileImports({ exclude: /\.icon\.svg$/, include: [ @@ -259,7 +272,7 @@ export async function makeRollupConfigs( json(), yaml(), esbuild({ - target: 'ES2022', + target: 'ES2023', minify: options.minify, }), ], diff --git a/packages/cli/src/modules/build/lib/builder/types.ts b/packages/cli/src/modules/build/lib/builder/types.ts index 937bd157c9..f80d0d1306 100644 --- a/packages/cli/src/modules/build/lib/builder/types.ts +++ b/packages/cli/src/modules/build/lib/builder/types.ts @@ -13,13 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* We want to maintain the same information as an enum, so we disable the redeclaration warning */ +/* eslint-disable @typescript-eslint/no-redeclare */ import { BackstagePackage, BackstagePackageJson } from '@backstage/cli-node'; -export enum Output { - esm, - cjs, - types, +export const Output = { + esm: 0, + cjs: 1, + types: 2, +} as const; + +/** + * @public + */ +export type Output = (typeof Output)[keyof typeof Output]; + +/** + * @public + */ +export namespace Output { + export type esm = typeof Output.esm; + export type cjs = typeof Output.cjs; + export type types = typeof Output.types; } export type BuildOptions = { diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index da444dff15..284d49d10a 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -29,7 +29,7 @@ import { paths as cliPaths } from '../../../../lib/paths'; import fs from 'fs-extra'; import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; -import { runPlain } from '../../../../lib/run'; +import { runOutput } from '@backstage/cli-common'; import { transforms } from './transforms'; import { version } from '../../../../lib/version'; import yn from 'yn'; @@ -78,14 +78,14 @@ async function readBuildInfo() { let commit: string | undefined; try { - commit = await runPlain('git', 'rev-parse', 'HEAD'); + commit = await runOutput(['git', 'rev-parse', 'HEAD']); } catch (error) { // ignore, see below } let gitVersion: string | undefined; try { - gitVersion = await runPlain('git', 'describe', '--always'); + gitVersion = await runOutput(['git', 'describe', '--always']); } catch (error) { // ignore, see below } diff --git a/packages/cli/src/modules/build/lib/bundler/optimization.ts b/packages/cli/src/modules/build/lib/bundler/optimization.ts index 63319ebec6..8d0eea5b88 100644 --- a/packages/cli/src/modules/build/lib/bundler/optimization.ts +++ b/packages/cli/src/modules/build/lib/bundler/optimization.ts @@ -34,13 +34,13 @@ export const optimization = ( minimize: !isDev, minimizer: [ new MinifyPlugin({ - target: 'ES2022', + target: 'ES2023', format: 'iife', exclude: 'remoteEntry.js', }), // Avoid iife wrapping of module federation remote entry as it breaks the variable assignment new MinifyPlugin({ - target: 'ES2022', + target: 'ES2023', format: undefined, include: 'remoteEntry.js', }), diff --git a/packages/cli/src/modules/build/lib/bundler/transforms.ts b/packages/cli/src/modules/build/lib/bundler/transforms.ts index 64e33b220e..ecf6e20751 100644 --- a/packages/cli/src/modules/build/lib/bundler/transforms.ts +++ b/packages/cli/src/modules/build/lib/bundler/transforms.ts @@ -67,7 +67,7 @@ export const transforms = (options: TransformOptions): Transforms => { : 'builtin:swc-loader', options: { jsc: { - target: 'es2022', + target: 'es2023', externalHelpers: !isBackend, parser: { syntax: 'typescript', @@ -97,7 +97,7 @@ export const transforms = (options: TransformOptions): Transforms => { : 'builtin:swc-loader', options: { jsc: { - target: 'es2022', + target: 'es2023', externalHelpers: !isBackend, parser: { syntax: 'ecmascript', diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts index 971409cd93..90635f1a73 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts @@ -25,7 +25,7 @@ import { tmpdir } from 'os'; import tar, { CreateOptions, FileOptions } from 'tar'; import partition from 'lodash/partition'; import { paths } from '../../../../lib/paths'; -import { run } from '../../../../lib/run'; +import { run } from '@backstage/cli-common'; import { dependencies as cliDependencies, devDependencies as cliDevDependencies, @@ -228,11 +228,11 @@ export async function createDistWorkspace( await runParallelWorkers({ items: customBuild, worker: async ({ name, dir, args }) => { - await run('yarn', ['run', 'build', ...(args || [])], { + await run(['yarn', 'run', 'build', ...(args || [])], { cwd: dir, - stdoutLogFunc: prefixLogFunc(`${name}: `, 'stdout'), - stderrLogFunc: prefixLogFunc(`${name}: `, 'stderr'), - }); + onStdout: prefixLogFunc(`${name}: `, 'stdout'), + onStderr: prefixLogFunc(`${name}: `, 'stderr'), + }).waitForExit(); }, }); } @@ -321,9 +321,9 @@ async function moveToDistWorkspace( console.log(`Repacking ${target.name} into dist workspace`); const archivePath = resolvePath(workspaceDir, archive); - await run('yarn', ['pack', '--filename', archivePath], { + await run(['yarn', 'pack', '--filename', archivePath], { cwd: target.dir, - }); + }).waitForExit(); const outputDir = relativePath(paths.targetRoot, target.dir); const absoluteOutputPath = resolvePath(workspaceDir, outputDir); diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts new file mode 100644 index 0000000000..2b97ae352c --- /dev/null +++ b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { runBackend } from './runBackend'; +import spawn from 'cross-spawn'; + +// Mock external dependencies +jest.mock('chokidar', () => ({ + watch: jest.fn(() => ({ + on: jest.fn().mockReturnThis(), + add: jest.fn(), + })), +})); + +jest.mock('cross-spawn', () => + jest.fn(() => ({ + on: jest.fn().mockReturnThis(), + once: jest.fn().mockReturnThis(), + kill: jest.fn(), + killed: false, + exitCode: null, + pid: 12345, + })), +); + +jest.mock('../ipc', () => ({ + IpcServer: jest.fn().mockImplementation(() => ({ + addChild: jest.fn(), + })), + ServerDataStore: { + bind: jest.fn(), + }, +})); + +jest.mock('ctrlc-windows', () => ({ + ctrlc: jest.fn(), +})); + +describe('runBackend', () => { + let originalEnv: NodeJS.ProcessEnv; + let originalPlatform: string; + const mockSpawn = spawn as jest.MockedFunction; + + beforeEach(() => { + // Use fake timers to control debounce + jest.useFakeTimers(); + + // Save original environment + originalEnv = { ...process.env }; + process.env = { NODE_ENV: 'test' }; + originalPlatform = process.platform; + + // Mock process.stdin.on to prevent actual stdin reading + jest.spyOn(process.stdin, 'on').mockReturnValue(process.stdin); + + // Mock process.once to prevent actual signal handling + jest.spyOn(process, 'once').mockReturnValue(process); + }); + + afterEach(() => { + // Restore original environment + process.env = originalEnv; + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }); + + jest.clearAllMocks(); + jest.useRealTimers(); + }); + + describe('--no-node-snapshot argument handling', () => { + it('should pass --no-node-snapshot when NODE_OPTIONS is not set', () => { + delete process.env.NODE_OPTIONS; + + runBackend({ + entry: 'src/index', + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + }); + + it('should pass --no-node-snapshot when NODE_OPTIONS exists without --node-snapshot', () => { + process.env.NODE_OPTIONS = '--max-old-space-size=4096'; + + runBackend({ + entry: 'src/index', + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + }); + + it('should not pass --no-node-snapshot when --node-snapshot already exists in NODE_OPTIONS', () => { + process.env.NODE_OPTIONS = '--node-snapshot --max-old-space-size=4096'; + + runBackend({ + entry: 'src/index', + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).not.toContain('--no-node-snapshot'); + }); + + it('should not pass --no-node-snapshot when --node-snapshot exists in the middle of NODE_OPTIONS', () => { + process.env.NODE_OPTIONS = + '--max-old-space-size=4096 --node-snapshot --inspect'; + + runBackend({ + entry: 'src/index', + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).not.toContain('--no-node-snapshot'); + }); + + it('should pass --no-node-snapshot even with trailing spaces in NODE_OPTIONS', () => { + process.env.NODE_OPTIONS = '--max-old-space-size=4096 '; + + runBackend({ + entry: 'src/index', + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + }); + + it('should pass --no-node-snapshot alongside other option args like --inspect', () => { + delete process.env.NODE_OPTIONS; + + runBackend({ + entry: 'src/index', + inspectEnabled: true, + }); + + // Fast-forward past the debounce delay (100ms) + jest.advanceTimersByTime(100); + + expect(mockSpawn).toHaveBeenCalled(); + const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; + expect(spawnArgs).toContain('--no-node-snapshot'); + expect(spawnArgs).toContain('--inspect'); + }); + }); +}); diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli/src/modules/build/lib/runner/runBackend.ts index 49dccf804f..0550ff2563 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.ts @@ -47,7 +47,7 @@ export type RunBackendOptions = { }; export async function runBackend(options: RunBackendOptions) { - const envEnv = process.env as { NODE_ENV: string }; + const envEnv = process.env as { NODE_ENV: string; NODE_OPTIONS?: string }; if (!envEnv.NODE_ENV) { envEnv.NODE_ENV = 'development'; } @@ -115,6 +115,12 @@ export async function runBackend(options: RunBackendOptions) { } } + // Unless the user explicitly toggles node-snapshot, default to provide --no-node-snapshot to reduce number of steps to run scaffolder + // on Node LTS. + if (!envEnv.NODE_OPTIONS?.includes('--node-snapshot')) { + optionArgs.push('--no-node-snapshot'); + } + const userArgs = process.argv .slice(['node', 'backstage-cli', 'package', 'start'].length) .filter(arg => !optionArgs.includes(arg)); diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index d62dbd4d26..3eabf5e9fc 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -62,7 +62,7 @@ export default createCliPlugin({ path: ['config:print'], description: 'Print the app configuration for the current package', execute: async ({ args, info }) => { - const argv = await yargs + const argv = await yargs() .options({ package: { type: 'string' }, lax: { type: 'boolean' }, @@ -82,7 +82,7 @@ export default createCliPlugin({ description: 'Validate that the given configuration loads and matches schema', execute: async ({ args }) => { - const argv = await yargs + const argv = await yargs() .options({ package: { type: 'string' }, lax: { type: 'boolean' }, @@ -105,7 +105,7 @@ export default createCliPlugin({ path: ['config:schema'], description: 'Print the JSON schema for the given configuration', execute: async ({ args }) => { - const argv = await yargs + const argv = await yargs() .options({ package: { type: 'string' }, format: { type: 'string' }, @@ -122,7 +122,7 @@ export default createCliPlugin({ path: ['config', 'schema'], description: 'Print the JSON schema for the given configuration', execute: async ({ args }) => { - const argv = await yargs + const argv = await yargs() .options({ package: { type: 'string' }, format: { type: 'string' }, diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index 416c995541..6970699f13 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -16,14 +16,46 @@ import { version as cliVersion } from '../../../../package.json'; import os from 'os'; -import { runPlain } from '../../../lib/run'; +import { runOutput } from '@backstage/cli-common'; import { paths } from '../../../lib/paths'; import { Lockfile } from '../../../lib/versioning'; +import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -export default async () => { +interface InfoOptions { + include: string[]; + format: 'text' | 'json'; +} + +/** + * Attempts to read package.json from node_modules for a given package name. + * Returns undefined if the package.json cannot be read. + */ +function tryReadPackageJson( + packageName: string, + targetPath: string, +): BackstagePackageJson | undefined { + try { + return require(require.resolve(`${packageName}/package.json`, { + paths: [targetPath], + })); + } catch { + return undefined; + } +} + +/** + * Checks if a package has a backstage field in its package.json + */ +function hasBackstageField(packageName: string, targetPath: string): boolean { + const pkg = tryReadPackageJson(packageName, targetPath); + return pkg?.backstage !== undefined; +} + +export default async (options: InfoOptions) => { await new Promise(async () => { - const yarnVersion = await runPlain('yarn --version'); + const yarnVersion = await runOutput(['yarn', '--version']); const isLocal = fs.existsSync(paths.resolveOwn('./src')); const backstageFile = paths.resolveTargetRoot('backstage.json'); @@ -33,27 +65,151 @@ export default async () => { const backstageJson = await fs.readJSON(backstageFile); backstageVersion = backstageJson.version ?? 'N/A'; } catch (error) { - console.warn('The "backstage.json" file is not in the expected format'); - console.log(); + if (options.format !== 'json') { + console.warn( + 'The "backstage.json" file is not in the expected format', + ); + console.log(); + } } } - console.log(`OS: ${os.type} ${os.release} - ${os.platform}/${os.arch}`); - console.log(`node: ${process.version}`); - console.log(`yarn: ${yarnVersion}`); + // Build system info + const systemInfo = { + os: `${os.type} ${os.release} - ${os.platform}/${os.arch}`, + node: process.version, + yarn: yarnVersion, + cli: { version: cliVersion, local: isLocal }, + backstage: backstageVersion, + }; + + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const targetPath = paths.targetRoot; + + // Get workspace package names and their versions + const workspacePackages = new Map(); + try { + const packages = await PackageGraph.listTargetPackages(); + for (const pkg of packages) { + workspacePackages.set(pkg.packageJson.name, pkg.packageJson.version); + } + } catch { + // If we can't list workspace packages, continue without them + } + + // Collect all package names from lockfile + const allPackages = [...lockfile.keys()]; + const includePatterns = options.include || []; + + // Collect installed (non-local) packages + const installedDeps = new Set(); + // Collect local workspace packages + const localDeps = new Set(); + + // Process @backstage/* packages + for (const pkg of allPackages) { + if (pkg.startsWith('@backstage/')) { + if (workspacePackages.has(pkg)) { + localDeps.add(pkg); + } else { + installedDeps.add(pkg); + } + } + } + + // Process packages matching --include patterns + for (const pattern of includePatterns) { + for (const pkg of allPackages) { + if (minimatch(pkg, pattern)) { + if (workspacePackages.has(pkg)) { + localDeps.add(pkg); + } else { + installedDeps.add(pkg); + } + } + } + } + + // Process packages with backstage field in their package.json + for (const pkg of allPackages) { + // Skip @backstage/* packages (already processed above) + if (pkg.startsWith('@backstage/')) { + continue; + } + if (workspacePackages.has(pkg)) { + // Check if local package has backstage field + if (hasBackstageField(pkg, targetPath)) { + localDeps.add(pkg); + } + } else if (hasBackstageField(pkg, targetPath)) { + installedDeps.add(pkg); + } + } + + // Helper to get versions for a package as an array of objects + const getVersions = (dep: string): Array<{ version: string }> => { + const entries = lockfile.get(dep); + if (!entries) { + return [{ version: 'unknown' }]; + } + const versions = [...new Set(entries.map(i => i.version))]; + return versions.map(v => ({ version: v })); + }; + + const sortedInstalled = [...installedDeps].sort(); + const sortedLocal = [...localDeps].sort(); + + // If format is json, output JSON to stdout + if (options.format === 'json') { + const output = { + system: systemInfo, + dependencies: Object.fromEntries( + sortedInstalled.map(dep => [dep, getVersions(dep)]), + ), + local: Object.fromEntries( + sortedLocal.map(dep => [ + dep, + [{ version: workspacePackages.get(dep) ?? 'unknown' }], + ]), + ), + }; + + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); + return; + } + + // Print to console + console.log(`OS: ${systemInfo.os}`); + console.log(`node: ${systemInfo.node}`); + console.log(`yarn: ${systemInfo.yarn}`); console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); console.log(`backstage: ${backstageVersion}`); console.log(); + + // Print installed dependencies console.log('Dependencies:'); - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); + if (sortedInstalled.length > 0) { + const maxLength = Math.max(...sortedInstalled.map(d => d.length)); + for (const dep of sortedInstalled) { + const versions = getVersions(dep) + .map(v => v.version) + .join(', '); + console.log(` ${dep.padEnd(maxLength)} ${versions}`); + } + } else { + console.log(' (no installed Backstage packages found)'); + } - const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/')); - const maxLength = Math.max(...deps.map(d => d.length)); - - for (const dep of deps) { - const versions = new Set(lockfile.get(dep)!.map(i => i.version)); - console.log(` ${dep.padEnd(maxLength)} ${[...versions].join(', ')}`); + // Print local workspace packages + if (localDeps.size > 0) { + console.log(); + console.log('Local:'); + const maxLength = Math.max(...sortedLocal.map(d => d.length)); + for (const dep of sortedLocal) { + const version = workspacePackages.get(dep) ?? 'unknown'; + console.log(` ${dep.padEnd(maxLength)} ${version}`); + } } }); }; diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index e93f008905..c14926df85 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -24,8 +24,25 @@ export default createCliPlugin({ path: ['info'], description: 'Show helpful information for debugging and reporting bugs', execute: async ({ args }) => { - yargs().parse(args); - await lazy(() => import('./commands/info'), 'default')(args); + const argv = await yargs() + .options({ + include: { + type: 'string', + array: true, + default: [], + description: + 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', + }, + format: { + type: 'string', + choices: ['text', 'json'], + default: 'text', + description: 'Output format (text or json)', + }, + }) + .help() + .parse(args); + await lazy(() => import('./commands/info'), 'default')(argv); }, }); }, diff --git a/packages/cli/src/modules/maintenance/commands/repo/clean.ts b/packages/cli/src/modules/maintenance/commands/repo/clean.ts index 51c31129ea..c76076ad55 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/clean.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/clean.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import { execFile as execFileCb } from 'child_process'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { promisify } from 'util'; import { PackageGraph } from '@backstage/cli-node'; import { paths } from '../../../../lib/paths'; - -const execFile = promisify(execFileCb); +import { run } from '@backstage/cli-common'; export async function command(): Promise { const packages = await PackageGraph.listTargetPackages(); @@ -44,12 +41,9 @@ export async function command(): Promise { await fs.remove(resolvePath(pkg.dir, 'dist-types')); await fs.remove(resolvePath(pkg.dir, 'coverage')); } else if (cleanScript) { - const result = await execFile('yarn', ['run', 'clean'], { + await run(['yarn', 'run', 'clean'], { cwd: pkg.dir, - shell: true, - }); - process.stdout.write(result.stdout); - process.stderr.write(result.stderr); + }).waitForExit(); } } }), diff --git a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts b/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts index c8e8ef3a42..8f5e395b98 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { PackageGraph } from '@backstage/cli-node'; -import { runPlain } from '../../../lib/run'; +import { runOutput } from '@backstage/cli-common'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; @@ -84,6 +84,6 @@ export async function command() { } if (hasPrettier) { - await runPlain('prettier', '--write', ...configPaths); + await runOutput(['prettier', '--write', ...configPaths]); } } diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index 738610d09c..202704e030 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; import { Command } from 'commander'; -import * as runObj from '../../../../lib/run'; +import * as runObj from '@backstage/cli-common'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../../../lib/versioning/packages'; @@ -60,21 +60,22 @@ jest.mock('ora', () => ({ })); let mockDir: MockDirectory; -jest.mock('@backstage/cli-common', () => ({ - ...jest.requireActual('@backstage/cli-common'), - findPaths: () => ({ - resolveTargetRoot(filename: string) { - return mockDir.resolve(filename); - }, - get targetDir() { - return mockDir.path; - }, - }), -})); - -jest.mock('../../../../lib/run', () => { +jest.mock('@backstage/cli-common', () => { + const actual = jest.requireActual('@backstage/cli-common'); return { - run: jest.fn(), + ...actual, + findPaths: () => ({ + resolveTargetRoot(filename: string) { + return mockDir.resolve(filename); + }, + get targetDir() { + return mockDir.path; + }, + }), + run: jest.fn().mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + }), }; }); @@ -184,7 +185,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -222,8 +226,7 @@ describe('bump', () => { expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -277,7 +280,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -318,8 +324,7 @@ describe('bump', () => { expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); expect(runObj.run).not.toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -373,7 +378,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -421,8 +429,7 @@ describe('bump', () => { expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -477,7 +484,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -526,14 +536,14 @@ describe('bump', () => { expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); expect(runObj.run).toHaveBeenCalledTimes(2); - expect(runObj.run).toHaveBeenCalledWith('yarn', [ + expect(runObj.run).toHaveBeenCalledWith([ + 'yarn', 'plugin', 'import', 'https://versions.backstage.io/v1/releases/0.0.1/yarn-plugin', ]); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -587,7 +597,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/releases/999.0.1/manifest.json', @@ -656,7 +669,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -763,7 +779,10 @@ describe('bump', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -813,8 +832,7 @@ describe('bump', () => { expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -873,7 +891,10 @@ describe('bump', () => { }); mockFetchPackageInfo.mockRejectedValue(new NotFoundError('Nope')); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://versions.backstage.io/v1/tags/main/manifest.json', @@ -1094,7 +1115,10 @@ describe('environment variables', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://custom.example.com/v1/tags/main/manifest.json', @@ -1131,8 +1155,7 @@ describe('environment variables', () => { expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -1186,7 +1209,10 @@ describe('environment variables', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { await bump({ pattern: null, release: 'main' } as unknown as Command); @@ -1215,8 +1241,7 @@ describe('environment variables', () => { expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); @@ -1253,7 +1278,10 @@ describe('environment variables', () => { }, }); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + jest.spyOn(runObj, 'run').mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + } as any); worker.use( rest.get( 'https://custom.example.com/v1/tags/main/manifest.json', @@ -1291,14 +1319,14 @@ describe('environment variables', () => { ]); expect(runObj.run).toHaveBeenCalledTimes(2); - expect(runObj.run).toHaveBeenCalledWith('yarn', [ + expect(runObj.run).toHaveBeenCalledWith([ + 'yarn', 'plugin', 'import', 'https://custom.example.com/v1/releases/1.5.0/yarn-plugin', ]); expect(runObj.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + ['yarn', 'install'], expect.any(Object), ); }); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 0d5e2f2f35..cad477188b 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BACKSTAGE_JSON, bootstrapEnvProxyAgents } from '@backstage/cli-common'; -maybeBootstrapProxy(); +bootstrapEnvProxyAgents(); import { env } from 'process'; import fs from 'fs-extra'; @@ -32,7 +33,6 @@ import { mapDependencies, YarnInfoInspectData, } from '../../../../lib/versioning'; -import { BACKSTAGE_JSON } from '@backstage/cli-common'; import { runParallelWorkers } from '../../../../lib/parallel'; import { getManifestByReleaseLine, @@ -41,27 +41,7 @@ import { } from '@backstage/release-manifests'; import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; -import { run } from '../../../../lib/run'; - -function maybeBootstrapProxy() { - // see https://www.npmjs.com/package/global-agent - const globalAgentNamespace = - process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_'; - if ( - process.env[`${globalAgentNamespace}HTTP_PROXY`] || - process.env[`${globalAgentNamespace}HTTPS_PROXY`] - ) { - const globalAgent = - require('global-agent') as typeof import('global-agent'); - globalAgent.bootstrap(); - } - - if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) { - const { setGlobalDispatcher, EnvHttpProxyAgent } = - require('undici') as typeof import('undici'); - setGlobalDispatcher(new EnvHttpProxyAgent()); - } -} +import { run } from '@backstage/cli-common'; const DEP_TYPES = [ 'dependencies', @@ -155,7 +135,7 @@ export default async (opts: OptionValues) => { ? `${env.BACKSTAGE_VERSIONS_BASE_URL}/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin` : `https://versions.backstage.io/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`; - await run('yarn', ['plugin', 'import', yarnPluginUrl]); + await run(['yarn', 'plugin', 'import', yarnPluginUrl]).waitForExit(); console.log(); } diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts index 9ff00fd285..936d10e24c 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts @@ -17,7 +17,7 @@ import { MockDirectory, createMockDirectory, } from '@backstage/backend-test-utils'; -import * as run from '../../../../lib/run'; +import * as runObj from '@backstage/cli-common'; import migrate from './migrate'; import { withLogCollector } from '@backstage/test-utils'; import fs from 'fs-extra'; @@ -33,21 +33,22 @@ jest.mock('chalk', () => ({ })); let mockDir: MockDirectory; -jest.mock('@backstage/cli-common', () => ({ - ...jest.requireActual('@backstage/cli-common'), - findPaths: () => ({ - resolveTargetRoot(filename: string) { - return mockDir.resolve(filename); - }, - get targetDir() { - return mockDir.path; - }, - }), -})); - -jest.mock('../../../../lib/run', () => { +jest.mock('@backstage/cli-common', () => { + const actual = jest.requireActual('@backstage/cli-common'); return { - run: jest.fn(), + ...actual, + findPaths: () => ({ + resolveTargetRoot(filename: string) { + return mockDir.resolve(filename); + }, + get targetDir() { + return mockDir.path; + }, + }), + run: jest.fn().mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + }), }; }); @@ -58,8 +59,15 @@ function expectLogsToMatch(receivedLogs: String[], expected: String[]): void { describe('versions:migrate', () => { mockDir = createMockDirectory(); + beforeEach(() => { + (runObj.run as jest.Mock).mockReturnValue({ + exitCode: null, + waitForExit: jest.fn().mockResolvedValue(undefined), + }); + }); + afterEach(() => { - jest.resetAllMocks(); + (runObj.run as jest.Mock).mockClear(); }); it('should bump to the moved version when the package is moved', async () => { @@ -116,8 +124,6 @@ describe('versions:migrate', () => { }, }); - jest.spyOn(run, 'run').mockResolvedValue(undefined); - const { warn, log: logs } = await withLogCollector(async () => { await migrate({}); }); @@ -136,10 +142,9 @@ describe('versions:migrate', () => { 'Could not find package.json for @backstage/theme@^1.0.0 in b (dependencies)', ]); - expect(run.run).toHaveBeenCalledTimes(1); - expect(run.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + ['yarn', 'install'], expect.any(Object), ); @@ -227,16 +232,13 @@ describe('versions:migrate', () => { }, }); - jest.spyOn(run, 'run').mockResolvedValue(undefined); - await withLogCollector(async () => { await migrate({}); }); - expect(run.run).toHaveBeenCalledTimes(1); - expect(run.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + ['yarn', 'install'], expect.any(Object), ); @@ -259,7 +261,7 @@ describe('versions:migrate', () => { ); }); - it('should replaces the occurrences of changed packages, and is careful', async () => { + it('should replace occurrences of changed packages, and is careful', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ workspaces: { @@ -314,16 +316,13 @@ describe('versions:migrate', () => { }, }); - jest.spyOn(run, 'run').mockResolvedValue(undefined); - await withLogCollector(async () => { await migrate({}); }); - expect(run.run).toHaveBeenCalledTimes(1); - expect(run.run).toHaveBeenCalledWith( - 'yarn', - ['install'], + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + ['yarn', 'install'], expect.any(Object), ); diff --git a/packages/cli/src/modules/migrate/lib/utils.ts b/packages/cli/src/modules/migrate/lib/utils.ts index 5b7b544c87..c85f56dabb 100644 --- a/packages/cli/src/modules/migrate/lib/utils.ts +++ b/packages/cli/src/modules/migrate/lib/utils.ts @@ -16,7 +16,7 @@ import ora from 'ora'; import chalk from 'chalk'; -import { run } from '../../../lib/run'; +import { run } from '@backstage/cli-common'; export async function runYarnInstall() { const spinner = ora({ @@ -27,7 +27,7 @@ export async function runYarnInstall() { const installOutput = new Array(); try { - await run('yarn', ['install'], { + await run(['yarn', 'install'], { env: { FORCE_COLOR: 'true', // We filter out all of the npm_* environment variables that are added when @@ -39,9 +39,9 @@ export async function runYarnInstall() { ), ), }, - stdoutLogFunc: data => installOutput.push(data), - stderrLogFunc: data => installOutput.push(data), - }); + onStdout: data => installOutput.push(data), + onStderr: data => installOutput.push(data), + }).waitForExit(); spinner.succeed(); } catch (error) { spinner.fail(); diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli/src/modules/new/commands/new.test.ts index 77ee96cd09..2183d80062 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli/src/modules/new/commands/new.test.ts @@ -23,6 +23,8 @@ describe.each([ [undefined, undefined, undefined], ['internal', '@internal/', 'backstage-plugin-'], ['internal/', '@internal/', 'backstage-plugin-'], + ['@internal', '@internal/', 'backstage-plugin-'], + ['@internal/', '@internal/', 'backstage-plugin-'], ['acme-backstage', '@acme-backstage/', 'plugin-'], ['acme-backstage/', '@acme-backstage/', 'plugin-'], ['acme-backstage-plugins', '@acme-backstage-plugins/', 'plugin-'], diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index 9d09f42cf3..a38316dd62 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -42,7 +42,10 @@ export default async (opts: ArgOptions) => { let pluginInfix: string | undefined = undefined; let packagePrefix: string | undefined = undefined; if (scope) { - packagePrefix = scope.includes('/') ? `@${scope}` : `@${scope}/`; + const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`; + packagePrefix = normalizedScope.includes('/') + ? normalizedScope + : `${normalizedScope}/`; pluginInfix = scope.includes('backstage') ? 'plugin-' : 'backstage-plugin-'; } diff --git a/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts b/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts index 6d34e35e4c..8e31f740cd 100644 --- a/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts +++ b/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts @@ -24,6 +24,7 @@ import { } from '../types'; import { installNewPackage } from './installNewPackage'; import { writeTemplateContents } from './writeTemplateContents'; +import { run } from '@backstage/cli-common'; type ExecuteNewTemplateOptions = { config: PortableTemplateConfig; @@ -54,14 +55,25 @@ export async function executePortableTemplate( } if (!options.skipInstall) { - await Task.forCommand('yarn install', { - cwd: targetDir, - optional: true, - }); - await Task.forCommand('yarn lint --fix', { - cwd: targetDir, - optional: true, - }); + for (const command of [ + ['yarn', 'install'], + ['yarn', 'lint', '--fix'], + ]) { + const commandStr = command.join(' '); + try { + await Task.forItem('executing', commandStr, async () => { + await run(command, { + cwd: targetDir, + stdio: 'ignore', + }).waitForExit(); + }); + } catch (error) { + assertError(error); + Task.error( + `Warning: Failed to execute command '${commandStr}', ${error}`, + ); + } + } } Task.log(); diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts index f59546a308..c303bffbea 100644 --- a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts +++ b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts @@ -28,7 +28,7 @@ import { } from '../types'; import { PortableTemplate } from '../types'; import { ForwardedError } from '@backstage/errors'; -import { fromZodError } from 'zod-validation-error'; +import { fromZodError } from 'zod-validation-error/v3'; const templateDefinitionSchema = z .object({ diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts index b8a7940bcf..f8167ef339 100644 --- a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts +++ b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts @@ -196,7 +196,7 @@ describe('loadPortableTemplateConfig', () => { private: true, version: '0.1.0', packageNamePrefix: '@internal/', - packageNamePluginInfix: 'plugin-', + packageNamePluginInfix: 'backstage-plugin-', }); }); @@ -316,7 +316,7 @@ describe('loadPortableTemplateConfig', () => { version: '0.1.0', private: true, packageNamePrefix: '@internal/', - packageNamePluginInfix: 'plugin-', + packageNamePluginInfix: 'backstage-plugin-', }); await expect( @@ -333,7 +333,7 @@ describe('loadPortableTemplateConfig', () => { version: '0.1.0', private: true, packageNamePrefix: '@internal/', - packageNamePluginInfix: 'plugin-', + packageNamePluginInfix: 'backstage-plugin-', }); }); }); diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts index 5ef61bfb6b..dd937482a2 100644 --- a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts @@ -25,7 +25,7 @@ import { } from '../types'; import { parse as parseYaml } from 'yaml'; import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error'; +import { fromZodError } from 'zod-validation-error/v3'; import { ForwardedError } from '@backstage/errors'; const defaults = { @@ -70,6 +70,21 @@ type LoadConfigOptions = { overrides?: Partial; }; +function computePackageNamePluginInfix( + packageNamePrefix: string, + namePluginInfix?: string, +) { + const packageNamePluginInfix = + namePluginInfix ?? + (packageNamePrefix.includes('backstage') + ? defaults.packageNamePluginInfix + : 'backstage-plugin-'); + + return { + packageNamePluginInfix, + }; +} + export async function loadPortableTemplateConfig( options: LoadConfigOptions = {}, ): Promise { @@ -116,6 +131,16 @@ export async function loadPortableTemplateConfig( templateNameConflicts.set(pointer.name, rawPointer); } + const packageNamePrefix = + overrides.packageNamePrefix ?? + config?.globals?.namePrefix ?? + defaults.packageNamePrefix; + + const { packageNamePluginInfix } = computePackageNamePluginInfix( + packageNamePrefix, + overrides.packageNamePluginInfix ?? config?.globals?.namePluginInfix, + ); + return { isUsingDefaultTemplates: !config?.templates, templatePointers: templatePointerEntries.map(({ pointer }) => pointer), @@ -126,14 +151,8 @@ export async function loadPortableTemplateConfig( overrides.publishRegistry ?? config?.globals?.publishRegistry ?? defaults.publishRegistry, - packageNamePrefix: - overrides.packageNamePrefix ?? - config?.globals?.namePrefix ?? - defaults.packageNamePrefix, - packageNamePluginInfix: - overrides.packageNamePluginInfix ?? - config?.globals?.namePluginInfix ?? - defaults.packageNamePluginInfix, + packageNamePrefix, + packageNamePluginInfix, }; } diff --git a/packages/cli/src/modules/new/lib/tasks.ts b/packages/cli/src/modules/new/lib/tasks.ts index 2922f34f35..f3620d242f 100644 --- a/packages/cli/src/modules/new/lib/tasks.ts +++ b/packages/cli/src/modules/new/lib/tasks.ts @@ -16,11 +16,6 @@ import chalk from 'chalk'; import ora from 'ora'; -import { promisify } from 'util'; -import { exec as execCb } from 'child_process'; -import { assertError } from '@backstage/errors'; - -const exec = promisify(execCb); const TASK_NAME_MAX_LENGTH = 14; @@ -64,30 +59,4 @@ export class Task { throw error; } } - - static async forCommand( - command: string, - options?: { cwd?: string; optional?: boolean }, - ) { - try { - await Task.forItem('executing', command, async () => { - await exec(command, { cwd: options?.cwd }); - }); - } catch (error) { - assertError(error); - if (error.stderr) { - process.stderr.write(error.stderr as Buffer); - } - if (error.stdout) { - process.stdout.write(error.stdout as Buffer); - } - if (options?.optional) { - Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`); - } else { - throw new Error( - `Failed to execute command '${chalk.cyan(command)}', ${error}`, - ); - } - } - } } diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli/src/modules/test/commands/package/test.ts index e7ef0af302..992697d19c 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli/src/modules/test/commands/package/test.ts @@ -16,7 +16,7 @@ import { Command, OptionValues } from 'commander'; import { paths } from '../../../../lib/paths'; -import { runCheck } from '../../../../lib/run'; +import { runCheck } from '@backstage/cli-common'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -55,8 +55,8 @@ export default async (_opts: OptionValues, cmd: Command) => { !includesAnyOf(args, '--watch', '--watchAll') ) { const isGitRepo = () => - runCheck('git', 'rev-parse', '--is-inside-work-tree'); - const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root'); + runCheck(['git', 'rev-parse', '--is-inside-work-tree']); + const isMercurialRepo = () => runCheck(['hg', '--cwd', '.', 'root']); if ((await isGitRepo()) || (await isMercurialRepo())) { args.push('--watch'); @@ -78,10 +78,36 @@ export default async (_opts: OptionValues, cmd: Command) => { process.env.TZ = 'UTC'; } + // Unless the user explicitly toggles node-snapshot, default to provide --no-node-snapshot to reduce number of steps to run scaffolder + // on Node LTS. + if (!process.env.NODE_OPTIONS?.includes('--node-snapshot')) { + process.env.NODE_OPTIONS = `${ + process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ` : '' + }--no-node-snapshot`; + } + // This ensures that the process doesn't exit too early before stdout is flushed if (args.includes('--help')) { (process.stdout as any)._handle.setBlocking(true); } + // Because of the ongoing migration to v30 of jest, jest is no longer hard-depended to allow + // opt-in migration. Users instead need to add jest as a devDependency themselves and specify + // the version they want. This prints a helpful error message if jest is not found, i.e. they + // forgot/didn't know they had to add jest themselves. + try { + require.resolve('jest'); + } catch { + console.error( + [ + 'No Jest installation found in this project.', + '', + 'To support opt-in migration to Jest v30, the Backstage CLI now expects Jest to be installed as a devDependency.', + 'See the migration guide in the changelog for version options and their consequences.', + ].join('\n'), + ); + process.exit(1); + } + await require('jest').run(args); }; diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index f0520d29e2..2288d13476 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -17,12 +17,14 @@ import os from 'os'; import crypto from 'node:crypto'; import yargs from 'yargs'; +// 'jest-cli' is included with jest and should be kept in sync with the installed jest version +// eslint-disable-next-line @backstage/no-undeclared-imports import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli'; import { relative as relativePath } from 'path'; import { Command, OptionValues } from 'commander'; import { Lockfile, PackageGraph } from '@backstage/cli-node'; import { paths } from '../../../../lib/paths'; -import { runCheck, runPlain } from '../../../../lib/run'; +import { runCheck, runOutput } from '@backstage/cli-common'; import { isChildPath } from '@backstage/cli-common'; import { SuccessCache } from '../../../../lib/cache/SuccessCache'; @@ -63,14 +65,14 @@ async function readPackageTreeHashes(graph: PackageGraph) { ...pkg, path: relativePath(paths.targetRoot, pkg.dir), })); - const output = await runPlain( + const output = await runOutput([ 'git', 'ls-tree', - '--format="%(objectname)=%(path)"', + '--format=%(objectname)=%(path)', 'HEAD', '--', ...pkgs.map(pkg => pkg.path), - ); + ]); const map = new Map( output @@ -175,8 +177,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { !hasFlags('--coverage', '--watch', '--watchAll') ) { const isGitRepo = () => - runCheck('git', 'rev-parse', '--is-inside-work-tree'); - const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root'); + runCheck(['git', 'rev-parse', '--is-inside-work-tree']); + const isMercurialRepo = () => runCheck(['hg', '--cwd', '.', 'root']); if ((await isGitRepo()) || (await isMercurialRepo())) { isSingleWatchMode = true; @@ -289,6 +291,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise { process.env.TZ = 'UTC'; } + // Unless the user explicitly toggles node-snapshot, default to provide --no-node-snapshot to reduce number of steps to run scaffolder + // on Node LTS. + if (!process.env.NODE_OPTIONS?.includes('--node-snapshot')) { + process.env.NODE_OPTIONS = `${ + process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ` : '' + }--no-node-snapshot`; + } + // This ensures that the process doesn't exit too early before stdout is flushed if (args.includes('--jest-help')) { removeOptionArg(args, '--jest-help'); diff --git a/packages/cli/src/tests/transforms/transforms.test.ts b/packages/cli/src/tests/transforms/transforms.test.ts index 7fb76bd741..0f51242206 100644 --- a/packages/cli/src/tests/transforms/transforms.test.ts +++ b/packages/cli/src/tests/transforms/transforms.test.ts @@ -36,18 +36,18 @@ const exportValues = { }; const expectedExports = { - commonJs: { + commonJs: expect.objectContaining({ ...exportValues.commonJs, dyn: exportValues.all, default: { ...exportValues.commonJs, dyn: exportValues.all, }, - }, - module: { + }), + module: expect.objectContaining({ ...exportValues.all, dyn: exportValues.all, - }, + }), }; function loadFixture(fixture: string) { diff --git a/packages/cli/templates/frontend-plugin/package.json.hbs b/packages/cli/templates/frontend-plugin/package.json.hbs index 1ee8231228..9499d11b18 100644 --- a/packages/cli/templates/frontend-plugin/package.json.hbs +++ b/packages/cli/templates/frontend-plugin/package.json.hbs @@ -31,7 +31,9 @@ "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "peerDependencies": { - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", @@ -42,7 +44,9 @@ "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}", - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/node-library/package.json.hbs b/packages/cli/templates/node-library/package.json.hbs index 941bf309fd..5b292e3e23 100644 --- a/packages/cli/templates/node-library/package.json.hbs +++ b/packages/cli/templates/node-library/package.json.hbs @@ -20,6 +20,7 @@ "postpack": "backstage-cli package postpack" }, "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", "@backstage/cli": "{{versionQuery '@backstage/cli'}}" }, "files": [ diff --git a/packages/cli/templates/plugin-node-library/package.json.hbs b/packages/cli/templates/plugin-node-library/package.json.hbs index 876d6414ac..bcd064edcc 100644 --- a/packages/cli/templates/plugin-node-library/package.json.hbs +++ b/packages/cli/templates/plugin-node-library/package.json.hbs @@ -21,6 +21,7 @@ "postpack": "backstage-cli package postpack" }, "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", "@backstage/cli": "{{versionQuery '@backstage/cli'}}" }, "files": [ diff --git a/packages/cli/templates/scaffolder-backend-module/package.json.hbs b/packages/cli/templates/scaffolder-backend-module/package.json.hbs index 36c99c7687..72cb370443 100644 --- a/packages/cli/templates/scaffolder-backend-module/package.json.hbs +++ b/packages/cli/templates/scaffolder-backend-module/package.json.hbs @@ -26,6 +26,7 @@ "@backstage/plugin-scaffolder-node": "{{versionQuery '@backstage/plugin-scaffolder-node'}}" }, "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@backstage/plugin-scaffolder-node-test-utils": "{{versionQuery '@backstage/plugin-scaffolder-node-test-utils'}}" }, diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 89c2f0ecfa..22dd2a5df7 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/codemods +## 0.1.53 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## 0.1.53-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + +## 0.1.53-next.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## 0.1.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + ## 0.1.52 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 92b3dc7042..01f5ae9168 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/codemods", - "version": "0.1.52", + "version": "0.1.53", "description": "A collection of codemods for Backstage projects", "backstage": { "role": "cli" @@ -52,6 +52,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/jscodeshift": "^0.12.0", - "@types/node": "^20.16.0" + "@types/node": "^22.13.14" } } diff --git a/packages/codemods/src/action.ts b/packages/codemods/src/action.ts index b955ab17ea..467dd35f82 100644 --- a/packages/codemods/src/action.ts +++ b/packages/codemods/src/action.ts @@ -15,11 +15,9 @@ */ import { relative as relativePath } from 'path'; -import { spawn } from 'child_process'; import { OptionValues } from 'commander'; -import { findPaths } from '@backstage/cli-common'; +import { findPaths, run } from '@backstage/cli-common'; import { platform } from 'os'; -import { ExitCodeError } from './errors'; // eslint-disable-next-line no-restricted-syntax const paths = findPaths(__dirname); @@ -51,42 +49,25 @@ export function createCodemodAction(name: string) { console.log(`Running jscodeshift with these arguments: ${args.join(' ')}`); - let command; + let commandArgs: string[]; if (platform() === 'win32') { - command = 'jscodeshift'; + commandArgs = ['jscodeshift', ...args]; } else { // jscodeshift ships a slightly broken bin script with windows // line endings so we need to execute it using node rather than // letting the `#!/usr/bin/env node` take care of it - command = process.argv0; - args.unshift(require.resolve('.bin/jscodeshift')); + commandArgs = [ + process.argv0, + require.resolve('.bin/jscodeshift'), + ...args, + ]; } - const child = spawn(command, args, { - stdio: 'inherit', - shell: true, + await run(commandArgs, { env: { ...process.env, FORCE_COLOR: 'true', }, - }); - - if (typeof child.exitCode === 'number') { - if (child.exitCode) { - throw new ExitCodeError(child.exitCode, name); - } - return; - } - - await new Promise((resolve, reject) => { - child.once('error', error => reject(error)); - child.once('exit', code => { - if (code) { - reject(new ExitCodeError(code, name)); - } else { - resolve(); - } - }); - }); + }).waitForExit(); }; } diff --git a/packages/codemods/src/errors.ts b/packages/codemods/src/errors.ts index 2f67b94ae1..e27f66895c 100644 --- a/packages/codemods/src/errors.ts +++ b/packages/codemods/src/errors.ts @@ -15,6 +15,7 @@ */ import chalk from 'chalk'; +import { ExitCodeError } from '@backstage/cli-common'; export class CustomError extends Error { get name(): string { @@ -22,19 +23,6 @@ export class CustomError extends Error { } } -export class ExitCodeError extends CustomError { - readonly code: number; - - constructor(code: number, command?: string) { - if (command) { - super(`Command '${command}' exited with code ${code}`); - } else { - super(`Child exited with code ${code}`); - } - this.code = code; - } -} - export function exitWithError(error: Error): never { if (error instanceof ExitCodeError) { process.stderr.write(`\n${chalk.red(error.message)}\n\n`); diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index c2433f3c35..84ad0e04a1 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/config-loader +## 1.10.7 + +### Patch Changes + +- 741c47a: Updated dependency `typescript-json-schema` to `^0.67.0`. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## 1.10.7-next.1 + +### Patch Changes + +- 741c47a: Updated dependency `typescript-json-schema` to `^0.67.0`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 1.10.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 1.10.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + ## 1.10.6-next.0 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 10afca8ba4..e48723e5f7 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.6-next.0", + "version": "1.10.7", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" @@ -49,7 +49,7 @@ "json-schema-traverse": "^1.0.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "typescript-json-schema": "^0.65.0", + "typescript-json-schema": "^0.67.0", "yaml": "^2.0.0" }, "devDependencies": { @@ -57,7 +57,7 @@ "@backstage/cli": "workspace:^", "@types/json-schema-merge-allof": "^0.6.0", "@types/minimist": "^1.2.5", - "msw": "^1.0.0", + "msw": "^2.0.0", "zen-observable": "^0.10.0" } } diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 8908b4268b..ea226aa557 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -17,7 +17,7 @@ import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import fs from 'fs-extra'; -import { rest } from 'msw'; +import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createDeferred } from '@backstage/types'; @@ -74,32 +74,28 @@ describe('loadConfig', () => { }); const server = setupServer(); - const initialLoaderHandler = rest.get( + const initialLoaderHandler = http.get( `https://some.domain.io/app-config.yaml`, - (_req, res, ctx) => { - return res( - ctx.body( - `app: + () => { + return new HttpResponse( + `app: title: Remote Example App sessionKey: 'abc123' escaped: \$\${Escaped} `, - ), ); }, ); - const reloadHandler = rest.get( + const reloadHandler = http.get( `https://some.domain.io/app-config.yaml`, - (_req, res, ctx) => { - return res( - ctx.body( - `app: + () => { + return new HttpResponse( + `app: title: NEW ReMOTe ExaMPLe App sessionKey: 'abc123' escaped: \$\${Escaped} `, - ), ); }, ); diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index d48c98fbe4..a732d982d3 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -15,14 +15,9 @@ */ import { createMockDirectory } from '@backstage/backend-test-utils'; -import { collectConfigSchemas } from './collect'; +import { collectConfigSchemas, internal } from './collect'; import path from 'path'; - -// cwd must be restored -const origDir = process.cwd(); -afterAll(() => { - process.chdir(origDir); -}); +import { execSync } from 'child_process'; const mockSchema = { type: 'object', @@ -37,6 +32,36 @@ const mockSchema = { describe('collectConfigSchemas', () => { const mockDir = createMockDirectory(); + // Jest 30's module resolver doesn't find nested node_modules in mock directories. + // Use a child process for native Node.js resolution instead. + const originalResolvePackagePath = internal.resolvePackagePath; + beforeAll(() => { + internal.resolvePackagePath = (name, options) => { + const basePath = (options && options.paths?.[0]) ?? process.cwd(); + const baseDir = basePath.endsWith('.json') + ? path.dirname(basePath) + : basePath; + + try { + return execSync('node', { + input: `console.log(require.resolve(${JSON.stringify( + name, + )}, { paths: [${JSON.stringify(baseDir)}] }))`, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + } catch { + const error = new Error(`Cannot find module '${name}'`); + (error as NodeJS.ErrnoException).code = 'MODULE_NOT_FOUND'; + throw error; + } + }; + }); + + afterAll(() => { + internal.resolvePackagePath = originalResolvePackagePath; + }); + afterEach(() => { mockDir.clear(); }); diff --git a/packages/config-loader/src/schema/collect.ts b/packages/config-loader/src/schema/collect.ts index 16bd654946..5f4e54387d 100644 --- a/packages/config-loader/src/schema/collect.ts +++ b/packages/config-loader/src/schema/collect.ts @@ -37,6 +37,17 @@ const req = ? require : __non_webpack_require__; +/** + * Exported for test mocking. Jest 30's module resolver has issues with + * nested node_modules, requiring tests to use an alternative resolution strategy. + * @internal + */ +export const internal = { + resolvePackagePath(name: string, options?: { paths: string[] }): string { + return req.resolve(name, options); + }, +}; + /** * This collects all known config schemas across all dependencies of the app. */ @@ -62,11 +73,9 @@ export async function collectConfigSchemas( const { name, parentPath } = item; try { - pkgPath = req.resolve( + pkgPath = internal.resolvePackagePath( `${name}/package.json`, - parentPath && { - paths: [parentPath], - }, + parentPath ? { paths: [parentPath] } : undefined, ); } catch { // We can somewhat safely ignore packages that don't export package.json, diff --git a/packages/config-loader/src/sources/RemoteConfigSource.test.ts b/packages/config-loader/src/sources/RemoteConfigSource.test.ts index 372dc91823..acb5870c7d 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.test.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { rest } from 'msw'; +import { http, HttpResponse } from 'msw'; import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { RemoteConfigSource } from './RemoteConfigSource'; @@ -26,15 +26,15 @@ describe('RemoteConfigSource', () => { it('should load config from a remote URL', async () => { worker.use( - rest.get('http://localhost/config.yaml', (_req, res, ctx) => - res( - ctx.body(` + http.get( + 'http://localhost/config.yaml', + () => + new HttpResponse(` app: title: Example App substituted: \${VALUE} escaped: \$\${VALUE} `), - ), ), ); @@ -61,9 +61,10 @@ app: it('should load and parse config from a remote URL', async () => { worker.use( - rest.get('http://localhost/config.json', (_req, res, ctx) => - res( - ctx.body( + http.get( + 'http://localhost/config.json', + () => + new HttpResponse( JSON.stringify({ app: { title: 'Example App', @@ -72,7 +73,6 @@ app: }, }), ), - ), ), ); @@ -102,12 +102,12 @@ app: let fetched = false; worker.use( - rest.get('http://localhost/config.yaml', (_req, res, ctx) => { + http.get('http://localhost/config.yaml', () => { if (!fetched) { fetched = true; - return res(ctx.body('x: 1')); + return new HttpResponse('x: 1'); } - return res(ctx.body('x: 2')); + return new HttpResponse('x: 2'); }), ); diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 596a67a1ed..6bf73fb546 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config +## 1.3.6 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. + ## 1.3.6-next.0 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 75d15678c0..47e1e0a9e7 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config", - "version": "1.3.6-next.0", + "version": "1.3.6", "description": "Config API used by Backstage core, backend, and CLI", "backstage": { "role": "common-library" diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index dbab1f871d..2b2dbeccfd 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/core-app-api +## 1.19.3 + +### Patch Changes + +- 75683ed: Added replay functionality to `AlertApiForwarder` to buffer and replay recent alerts to new subscribers, preventing missed alerts that were posted before subscription. +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + +## 1.19.3-next.1 + +### Patch Changes + +- 75683ed: Added replay functionality to `AlertApiForwarder` to buffer and replay recent alerts to new subscribers, preventing missed alerts that were posted before subscription. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 1.19.3-next.0 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 1.19.2 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + +## 1.19.2-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/core-plugin-api@1.11.2-next.1 + ## 1.19.2-next.0 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 7cd3712cc0..cd56c90ce8 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-app-api", - "version": "1.19.2-next.0", + "version": "1.19.3", "description": "Core app API used by Backstage apps", "backstage": { "role": "web-library" diff --git a/packages/core-app-api/report.api.md b/packages/core-app-api/report.api.md index 0cecc22f4a..e039394946 100644 --- a/packages/core-app-api/report.api.md +++ b/packages/core-app-api/report.api.md @@ -20,6 +20,7 @@ import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; +import { BackstageIdentityResponse as BackstageIdentityResponse_2 } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; @@ -55,6 +56,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; +import { ProfileInfo as ProfileInfo_2 } from '@backstage/frontend-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; @@ -62,6 +64,7 @@ import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { SessionState } from '@backstage/core-plugin-api'; +import { SessionState as SessionState_2 } from '@backstage/frontend-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; @@ -511,13 +514,13 @@ export class MicrosoftAuth { // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; // (undocumented) getIdToken(options?: AuthRequestOptions): Promise; // (undocumented) - getProfile(options?: AuthRequestOptions): Promise; + getProfile(options?: AuthRequestOptions): Promise; // (undocumented) - sessionState$(): Observable; + sessionState$(): Observable; // (undocumented) signIn(): Promise; // (undocumented) diff --git a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts index 4e5397f65d..138d00c09c 100644 --- a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -17,20 +17,35 @@ import { AlertApi, AlertMessage } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; +import ObservableImpl from 'zen-observable'; /** * Base implementation for the AlertApi that simply forwards alerts to consumers. * + * Recent alerts are buffered and replayed to new subscribers to prevent + * missing alerts that were posted before subscription. + * * @public */ export class AlertApiForwarder implements AlertApi { private readonly subject = new PublishSubject(); + private readonly recentAlerts: AlertMessage[] = []; + private readonly maxBufferSize = 10; post(alert: AlertMessage) { + this.recentAlerts.push(alert); + if (this.recentAlerts.length > this.maxBufferSize) { + this.recentAlerts.shift(); + } this.subject.next(alert); } alert$(): Observable { - return this.subject; + return new ObservableImpl(subscriber => { + for (const alert of this.recentAlerts) { + subscriber.next(alert); + } + return this.subject.subscribe(subscriber); + }); } } diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index bd82d58736..2b2545f52f 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -60,12 +60,6 @@ describe('PluginProtocolResolverFetchMiddleware', () => { 'https://real.com:8080/base', 'https://real.com:8080/base?c=d&e=f#g', ], - [ - 'plugin://username:password@x?c=d&e=f#g', - 'x', - 'https://real.com:8080/base', - 'https://username:password@real.com:8080/base?c=d&e=f#g', - ], [ 'plugin://x?c=d&e=f#g', 'x', @@ -106,14 +100,14 @@ describe('PluginProtocolResolverFetchMiddleware', () => { expect(inner.mock.calls[0][0]).toBe('https://elsewhere.com'); expect(inner.mock.calls[0][1].body).toBe('123'); - await outer( - new Request('plugin://a', { - method: 'POST', - body: '123', - }), - ); + const inputRequest = new Request('plugin://a', { + method: 'POST', + body: '123', + }); + + await outer(inputRequest); expect(inner.mock.calls[1][0]).toBe('https://elsewhere.com'); - expect(inner.mock.calls[1][1].body).toEqual(Buffer.from('123', 'utf8')); + expect(inner.mock.calls[1][1]).toBe(inputRequest); }); }); diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts index 5993543c33..9d320c4033 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.test.ts @@ -83,12 +83,9 @@ describe('AppIdentityProxy', () => { it('should navigate to target URL on sign out', async () => { const proxy = new AppIdentityProxy(); proxy.setTarget(mockIdentityApi, { signOutTargetUrl: '/foo' }); - Object.defineProperty(window, 'location', { - writable: true, - value: {}, - }); + const navigateSpy = jest.spyOn(proxy as any, 'navigateToUrl'); await proxy.signOut(); - expect(window.location.href).toBe('/foo'); + expect(navigateSpy).toHaveBeenCalledWith('/foo'); }); }); diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts index 941ec90d66..161092fe9f 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -133,7 +133,11 @@ export class AppIdentityProxy implements IdentityApi { await this.#cookieAuthSignOut?.(); - window.location.href = this.signOutTargetUrl; + this.navigateToUrl(this.signOutTargetUrl); + } + + private navigateToUrl(url: string): void { + window.location.href = url; } enableCookieAuth(ctx: { diff --git a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts index de21990cb3..2d4bb82668 100644 --- a/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts +++ b/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts @@ -37,12 +37,12 @@ import ObservableImpl from 'zen-observable'; import { toInternalTranslationResource, InternalTranslationResourceLoader, -} from '../../../../../core-plugin-api/src/translation/TranslationResource'; +} from '../../../../../frontend-plugin-api/src/translation/TranslationResource'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalTranslationRef, InternalTranslationRef, -} from '../../../../../core-plugin-api/src/translation/TranslationRef'; +} from '../../../../../frontend-plugin-api/src/translation/TranslationRef'; import { Observable } from '@backstage/types'; import { DEFAULT_LANGUAGE } from '../AppLanguageApi/AppLanguageSelector'; import { createElement, Fragment, ReactNode, isValidElement } from 'react'; diff --git a/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts b/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts index 5f56793cae..1824937a02 100644 --- a/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts +++ b/packages/core-app-api/src/apis/system/ApiFactoryRegistry.ts @@ -31,11 +31,11 @@ export type ApiFactoryScope = | 'app' // Factories registered in the app, overriding default ones | 'static'; // APIs that can't be overridden, e.g. config -enum ScopePriority { - default = 10, - app = 50, - static = 100, -} +const ScopePriority = { + default: 10, + app: 50, + static: 100, +} as const; type FactoryTuple = { priority: number; diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index 095fc88627..4273587c00 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -117,16 +117,12 @@ describe('ApiProvider', () => { }).toThrow(/^API context is not available/); }).error, ).toEqual([ - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, + expect.stringContaining('Error: API context is not available'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('Error: API context is not available'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'The above error occurred in the component', ), ]); @@ -137,16 +133,12 @@ describe('ApiProvider', () => { }).toThrow(/^API context is not available/); }).error, ).toEqual([ - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('API context is not available'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, + expect.stringContaining('Error: API context is not available'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('Error: API context is not available'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'The above error occurred in the component', ), ]); }); @@ -163,16 +155,16 @@ describe('ApiProvider', () => { }).toThrow('No implementation available for apiRef{x}'); }).error, ).toEqual([ - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'The above error occurred in the component', ), ]); @@ -187,16 +179,16 @@ describe('ApiProvider', () => { }).toThrow('No implementation available for apiRef{x}'); }).error, ).toEqual([ - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error('No implementation available for apiRef{x}'), - type: 'unhandled exception', - }), - expect.stringMatching( - /^The above error occurred in the component/, + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'The above error occurred in the component', ), ]); }); diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 3981da56ad..4a146678bc 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -670,14 +670,10 @@ describe('Integration Test', () => { }); expect(errorLogs).toEqual([ - expect.objectContaining({ - detail: new Error(expectedMessage), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error(expectedMessage), - type: 'unhandled exception', - }), + expect.stringContaining(`Error: ${expectedMessage}`), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining(`Error: ${expectedMessage}`), + expect.objectContaining({ type: 'unhandled-exception' }), expect.stringContaining( 'The above error occurred in the component:', ), @@ -714,14 +710,10 @@ describe('Integration Test', () => { ).rejects.toThrow(expectedMessage); }); expect(errorLogs).toEqual([ - expect.objectContaining({ - detail: new Error(expectedMessage), - type: 'unhandled exception', - }), - expect.objectContaining({ - detail: new Error(expectedMessage), - type: 'unhandled exception', - }), + expect.stringContaining(`Error: ${expectedMessage}`), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining(`Error: ${expectedMessage}`), + expect.objectContaining({ type: 'unhandled-exception' }), expect.stringContaining( 'The above error occurred in the component:', ), diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index b8d03b5e1c..e510fcb81d 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/core-compat-api +## 0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-catalog-react@1.21.5 + +## 0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + +## 0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.5.4 + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-plugin-api@1.12.0 + ## 0.5.4-next.0 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index cedf7e661e..7262eb20bd 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.4-next.0", + "version": "0.5.6", "backstage": { "role": "web-library" }, @@ -34,8 +34,10 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx index 1cb2b757e3..7094eb798c 100644 --- a/packages/core-compat-api/src/convertLegacyApp.test.tsx +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -262,13 +262,16 @@ describe('convertLegacyApp', () => { ], }); + // Increase timeout for async rendering of complex catalog entity pages + const findOptions = { timeout: 5000 }; + // Overview const renderOverviewTest = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/test/x'], }); await expect( - renderOverviewTest.findByText('overview content'), + renderOverviewTest.findByText('overview content', {}, findOptions), ).resolves.toBeInTheDocument(); renderOverviewTest.unmount(); @@ -277,7 +280,7 @@ describe('convertLegacyApp', () => { initialRouteEntries: ['/catalog/default/other/x'], }); await expect( - renderOverviewOther.findByText('other overview content'), + renderOverviewOther.findByText('other overview content', {}, findOptions), ).resolves.toBeInTheDocument(); renderOverviewOther.unmount(); @@ -287,7 +290,7 @@ describe('convertLegacyApp', () => { initialRouteEntries: ['/catalog/default/test/x/foo'], }); await expect( - renderFooTest.findByText('foo content'), + renderFooTest.findByText('foo content', {}, findOptions), ).resolves.toBeInTheDocument(); renderFooTest.unmount(); @@ -296,7 +299,7 @@ describe('convertLegacyApp', () => { initialRouteEntries: ['/catalog/default/other/x/foo'], }); await expect( - renderFooOther.findByText('other foo content'), + renderFooOther.findByText('other foo content', {}, findOptions), ).resolves.toBeInTheDocument(); renderFooOther.unmount(); @@ -306,7 +309,7 @@ describe('convertLegacyApp', () => { initialRouteEntries: ['/catalog/default/test/x/bar'], }); await expect( - renderBarTest.findByText('bar content'), + renderBarTest.findByText('bar content', {}, findOptions), ).resolves.toBeInTheDocument(); renderBarTest.unmount(); @@ -315,7 +318,7 @@ describe('convertLegacyApp', () => { initialRouteEntries: ['/catalog/default/other/x/bar'], }); await expect( - renderBarOther.findByText('other overview content'), + renderBarOther.findByText('other overview content', {}, findOptions), ).resolves.toBeInTheDocument(); // /bar does not exist, fall back to rendering overview renderBarOther.unmount(); }); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts index f09060212c..c21ac8c5bc 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -31,13 +31,11 @@ import { createExternalRouteRef as createNewExternalRouteRef, } from '@backstage/frontend-plugin-api'; import { convertLegacyRouteRef } from './convertLegacyRouteRef'; - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; +import { + OpaqueExternalRouteRef, + OpaqueRouteRef, + OpaqueSubRouteRef, +} from '@internal/frontend'; describe('convertLegacyRouteRef', () => { it('converts old to new', () => { @@ -85,48 +83,40 @@ describe('convertLegacyRouteRef', () => { expect(ref3).toBe(ref3Converted); expect(ref4).toBe(ref4Converted); - const ref1Internal = toInternalNewRouteRef(ref1Converted); - const ref2Internal = toInternalNewRouteRef(ref2Converted); - const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); - const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted); - const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted); - const ref3Internal = toInternalNewExternalRouteRef(ref3Converted); - const ref4Internal = toInternalNewExternalRouteRef(ref4Converted); + const ref1Internal = OpaqueRouteRef.toInternal(ref1Converted); + const ref2Internal = OpaqueRouteRef.toInternal(ref2Converted); + const ref1sub1Internal = OpaqueSubRouteRef.toInternal(ref1sub1Converted); + const ref1sub2Internal = OpaqueSubRouteRef.toInternal(ref1sub2Converted); + const ref2sub1Internal = OpaqueSubRouteRef.toInternal(ref2sub1Converted); + const ref3Internal = OpaqueExternalRouteRef.toInternal(ref3Converted); + const ref4Internal = OpaqueExternalRouteRef.toInternal(ref4Converted); - expect(ref1Internal.getDescription()).toBe( - 'routeRef{type=absolute,id=ref1}', - ); + expect(ref1Internal.getDescription()).toBe('ref1'); expect(ref1Internal.getParams()).toEqual([]); - expect(ref2Internal.getDescription()).toBe( - 'routeRef{type=absolute,id=ref2}', - ); + expect(ref2Internal.getDescription()).toBe('ref2'); expect(ref2Internal.getParams()).toEqual(['p1', 'p2']); expect(ref1sub1Internal.getDescription()).toBe( - 'routeRef{type=sub,id=sub1}', + 'at /sub1 with parent routeRef{type=absolute,id=ref1}', ); expect(ref1sub1Internal.getParams()).toEqual([]); expect(ref1sub1Internal.getParent()).toBe(ref1); expect(ref1sub2Internal.getDescription()).toBe( - 'routeRef{type=sub,id=sub2}', + 'at /sub2/:p3 with parent routeRef{type=absolute,id=ref1}', ); expect(ref1sub2Internal.getParams()).toEqual(['p3']); expect(ref1sub2Internal.getParent()).toBe(ref1); expect(ref2sub1Internal.getDescription()).toBe( - 'routeRef{type=sub,id=sub1}', + 'at /sub1/:p3 with parent routeRef{type=absolute,id=ref2}', ); expect(ref2sub1Internal.getParams()).toEqual(['p1', 'p2', 'p3']); expect(ref2sub1Internal.getParent()).toBe(ref2); expect(ref3Internal.getDefaultTarget()).toBe(undefined); - expect(ref3Internal.getDescription()).toBe( - 'routeRef{type=external,id=ref3}', - ); + expect(ref3Internal.getDescription()).toBe('ref3'); expect(ref3Internal.getParams()).toEqual([]); expect(ref4Internal.getDefaultTarget()).toBe('ref2'); - expect(ref4Internal.getDescription()).toBe( - 'routeRef{type=external,id=ref4}', - ); + expect(ref4Internal.getDescription()).toBe('ref4'); expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); }); @@ -168,34 +158,34 @@ describe('convertLegacyRouteRef', () => { expect(ref3).toBe(ref3Converted); expect(ref4).toBe(ref4Converted); - expect(String(ref1Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(String(ref1Converted)).toMatch(/^routeRef\{id=undefined,at='.*'\}$/); expect(ref1Converted.params).toEqual([]); - expect(String(ref2Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(String(ref2Converted)).toMatch(/^routeRef\{id=undefined,at='.*'\}$/); expect(ref2Converted.params).toEqual(['p1', 'p2']); expect(String(ref1sub1Converted)).toMatch( - /^SubRouteRef\{at \/sub1 with parent created at '.*'\}$/, + /^subRouteRef\{path='\/sub1',parent=routeRef\{id=undefined,at='.*'\}\}$/, ); expect(ref1sub1Converted.params).toEqual([]); expect(ref1sub1Converted.parent).toBe(ref1); expect(String(ref1sub2Converted)).toMatch( - /^SubRouteRef\{at \/sub2\/:p3 with parent created at '.*'\}$/, + /^subRouteRef\{path='\/sub2\/:p3',parent=routeRef\{id=undefined,at='.*'\}\}$/, ); expect(ref1sub2Converted.params).toEqual(['p3']); expect(ref1sub2Converted.parent).toBe(ref1); expect(String(ref2sub1Converted)).toMatch( - /^SubRouteRef\{at \/sub1\/:p3 with parent created at '.*'\}$/, + /^subRouteRef\{path='\/sub1\/:p3',parent=routeRef\{id=undefined,at='.*'\}\}$/, ); expect(ref2sub1Converted.params).toEqual(['p1', 'p2', 'p3']); expect(ref2sub1Converted.parent).toBe(ref2); expect(String(ref3Converted)).toMatch( - /^ExternalRouteRef\{created at '.*'\}$/, + /^externalRouteRef\{id=undefined,at='.*'\}$/, ); expect(ref3Converted.params).toEqual([]); expect(ref3Converted.optional).toBe(true); expect(String(ref4Converted)).toMatch( - /^ExternalRouteRef\{created at '.*'\}$/, + /^externalRouteRef\{id=undefined,at='.*'\}$/, ); expect(ref4Converted.params).toEqual(['p1', 'p2']); expect(ref4Converted.optional).toBe(true); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index 40a95a81ff..e490d83a19 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -32,13 +32,11 @@ import { createSubRouteRef, createExternalRouteRef, } from '@backstage/frontend-plugin-api'; - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; +import { + OpaqueRouteRef, + OpaqueSubRouteRef, + OpaqueExternalRouteRef, +} from '@internal/frontend'; /** * Converts a legacy route ref type to the new system. @@ -184,29 +182,29 @@ function convertNewToOld( ref: RouteRef | SubRouteRef | ExternalRouteRef, ): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef { if (ref.$$type === '@backstage/RouteRef') { - const newRef = toInternalRouteRef(ref); + const newRef = OpaqueRouteRef.toInternal(ref); return Object.assign(ref, { [routeRefType]: 'absolute', params: newRef.getParams(), title: newRef.getDescription(), - } as Omit) as unknown as LegacyRouteRef; + } as Omit) as unknown as LegacyRouteRef; } if (ref.$$type === '@backstage/SubRouteRef') { - const newRef = toInternalSubRouteRef(ref); + const newRef = OpaqueSubRouteRef.toInternal(ref); return Object.assign(ref, { [routeRefType]: 'sub', parent: convertLegacyRouteRef(newRef.getParent()), params: newRef.getParams(), - } as Omit) as unknown as LegacySubRouteRef; + } as Omit) as unknown as LegacySubRouteRef; } if (ref.$$type === '@backstage/ExternalRouteRef') { - const newRef = toInternalExternalRouteRef(ref); + const newRef = OpaqueExternalRouteRef.toInternal(ref); return Object.assign(ref, { [routeRefType]: 'external', optional: true, params: newRef.getParams(), defaultTarget: newRef.getDefaultTarget(), - } as Omit) as unknown as LegacyExternalRouteRef; + } as Omit) as unknown as LegacyExternalRouteRef; } throw new Error( @@ -221,7 +219,7 @@ function convertOldToNew( if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; const legacyRefStr = String(legacyRef); - const newRef = toInternalRouteRef( + const newRef = OpaqueRouteRef.toInternal( createRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], }), @@ -247,7 +245,7 @@ function convertOldToNew( if (type === 'sub') { const legacyRef = ref as LegacySubRouteRef; const legacyRefStr = String(legacyRef); - const newRef = toInternalSubRouteRef( + const newRef = OpaqueSubRouteRef.toInternal( createSubRouteRef({ path: legacyRef.path, parent: convertLegacyRouteRef(legacyRef.parent), @@ -274,7 +272,7 @@ function convertOldToNew( if (type === 'external') { const legacyRef = ref as LegacyExternalRouteRef; const legacyRefStr = String(legacyRef); - const newRef = toInternalExternalRouteRef( + const newRef = OpaqueExternalRouteRef.toInternal( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], defaultTarget: diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 63a2846889..fd9b8f649b 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,106 @@ # @backstage/core-components +## 0.18.5 + +### Patch Changes + +- a723b8a: The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown +- c671db9: Fixed bug in Table component where the toolbar layout would break when both a title and filters were present. +- 55a9dc2: Update colour token again in ItemCardHeader to respect theme definition. + +## 0.18.5-next.0 + +### Patch Changes + +- a723b8a: The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown + +## 0.18.4 + +### Patch Changes + +- 9a942a4: Fixed bug in the `LogViewer` component where shift + click always opened a new window instead of just changing the selection. + + In addition, improved the `LogViewer` component by a few usability enhancements: + + - Added support for multiple selections using cmd/ctrl + click + - Improved the generated hash that is added to the URL to also support ranges & multiple selections + - Added an hover effect & info tooltip to the "Copy to clipboard" button to indicate its functionality + - Added some color and a separator to the line numbers to improve readability + +- 207c3c8: long words like urls now breaks to new line on warning panels instead of overflowing the container +- 4c00303: Add `tooltipClasses` prop to `OverflowTooltip` component to allow customisation of the tooltip +- 5d52dab: Add i18n support for LogViewer search control +- f6b49ce: added support for wrapLongLines option in CodeSnippet +- Updated dependencies + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + +## 0.18.4-next.2 + +### Patch Changes + +- 4c00303: Add `tooltipClasses` prop to `OverflowTooltip` component to allow customisation of the tooltip +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.1-next.0 + - @backstage/version-bridge@1.0.11 + +## 0.18.4-next.1 + +### Patch Changes + +- 9a942a4: Fixed bug in the `LogViewer` component where shift + click always opened a new window instead of just changing the selection. + + In addition, improved the `LogViewer` component by a few usability enhancements: + + - Added support for multiple selections using cmd/ctrl + click + - Improved the generated hash that is added to the URL to also support ranges & multiple selections + - Added an hover effect & info tooltip to the "Copy to clipboard" button to indicate its functionality + - Added some color and a separator to the line numbers to improve readability + +- 207c3c8: long words like urls now breaks to new line on warning panels instead of overflowing the container +- 5d52dab: Add i18n support for LogViewer search control + +## 0.18.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/version-bridge@1.0.11 + +## 0.18.3 + +### Patch Changes + +- 1c7e1ce: - Revert `viewbox` back to old values. + - Added `fullScreen` prop to `EntityCatalogGraphCard` +- 96ad674: Line numbers in LogViewer will not be selectable in UI anymore +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + +## 0.18.3-next.2 + +### Patch Changes + +- 96ad674: Line numbers in LogViewer will not be selectable in UI anymore + +## 0.18.3-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- Updated dependencies + - @backstage/core-plugin-api@1.11.2-next.1 + ## 0.18.3-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index cbd3450886..80867d9d3a 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.3-next.0", + "version": "0.18.5", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" @@ -77,6 +77,7 @@ "linkify-react": "4.3.2", "linkifyjs": "4.3.2", "lodash": "^4.17.21", + "parse5": "^6.0.0", "pluralize": "^8.0.0", "qs": "^6.9.4", "rc-progress": "3.5.1", @@ -90,6 +91,8 @@ "react-use": "^17.3.2", "react-virtualized-auto-sizer": "^1.0.11", "react-window": "^1.8.6", + "rehype-raw": "^6.0.0", + "rehype-sanitize": "^5.0.0", "remark-gfm": "^3.0.1", "zen-observable": "^0.10.0", "zod": "^3.22.4" diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 96ddebcd3a..785b97ce28 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) export const coreComponentsTranslationRef: TranslationRef< @@ -63,6 +63,7 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; + readonly 'logViewer.searchField.placeholder': 'Search'; } >; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 9c9992deff..5e842929e6 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -188,6 +188,7 @@ export interface CodeSnippetProps { showCopyCodeButton?: boolean; showLineNumbers?: boolean; text: string; + wrapLongLines?: boolean; } // Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts @@ -283,11 +284,18 @@ export interface DependencyGraphProps // @public export namespace DependencyGraphTypes { - export enum Alignment { - DOWN_LEFT = 'DL', - DOWN_RIGHT = 'DR', - UP_LEFT = 'UL', - UP_RIGHT = 'UR', + // (undocumented) + export type Alignment = (typeof Alignment)[keyof typeof Alignment]; + // (undocumented) + export namespace Alignment { + // (undocumented) + export type DOWN_LEFT = typeof Alignment.DOWN_LEFT; + // (undocumented) + export type DOWN_RIGHT = typeof Alignment.DOWN_RIGHT; + // (undocumented) + export type UP_LEFT = typeof Alignment.UP_LEFT; + // (undocumented) + export type UP_RIGHT = typeof Alignment.UP_RIGHT; } export type DependencyEdge = T & { from: string; @@ -298,25 +306,54 @@ export namespace DependencyGraphTypes { export type DependencyNode = T & { id: string; }; - export enum Direction { - BOTTOM_TOP = 'BT', - LEFT_RIGHT = 'LR', - RIGHT_LEFT = 'RL', - TOP_BOTTOM = 'TB', - } - export enum LabelPosition { + // (undocumented) + export type Direction = (typeof Direction)[keyof typeof Direction]; + // (undocumented) + export namespace Direction { // (undocumented) - CENTER = 'c', + export type BOTTOM_TOP = typeof Direction.BOTTOM_TOP; // (undocumented) - LEFT = 'l', + export type LEFT_RIGHT = typeof Direction.LEFT_RIGHT; // (undocumented) - RIGHT = 'r', + export type RIGHT_LEFT = typeof Direction.RIGHT_LEFT; + // (undocumented) + export type TOP_BOTTOM = typeof Direction.TOP_BOTTOM; } - export enum Ranker { - LONGEST_PATH = 'longest-path', - NETWORK_SIMPLEX = 'network-simplex', - TIGHT_TREE = 'tight-tree', + // (undocumented) + export type LabelPosition = + (typeof LabelPosition)[keyof typeof LabelPosition]; + // (undocumented) + export namespace LabelPosition { + // (undocumented) + export type CENTER = typeof LabelPosition.CENTER; + // (undocumented) + export type LEFT = typeof LabelPosition.LEFT; + // (undocumented) + export type RIGHT = typeof LabelPosition.RIGHT; } + const Direction: { + readonly TOP_BOTTOM: 'TB'; + readonly BOTTOM_TOP: 'BT'; + readonly LEFT_RIGHT: 'LR'; + readonly RIGHT_LEFT: 'RL'; + }; + // (undocumented) + export type Ranker = (typeof Ranker)[keyof typeof Ranker]; + // (undocumented) + export namespace Ranker { + // (undocumented) + export type LONGEST_PATH = typeof Ranker.LONGEST_PATH; + // (undocumented) + export type NETWORK_SIMPLEX = typeof Ranker.NETWORK_SIMPLEX; + // (undocumented) + export type TIGHT_TREE = typeof Ranker.TIGHT_TREE; + } + const Alignment: { + readonly UP_LEFT: 'UL'; + readonly UP_RIGHT: 'UR'; + readonly DOWN_LEFT: 'DL'; + readonly DOWN_RIGHT: 'DR'; + }; export type RenderEdgeFunction = ( props: RenderEdgeProps, ) => ReactNode; @@ -344,12 +381,22 @@ export namespace DependencyGraphTypes { name?: string | undefined; }; }; + const Ranker: { + readonly NETWORK_SIMPLEX: 'network-simplex'; + readonly TIGHT_TREE: 'tight-tree'; + readonly LONGEST_PATH: 'longest-path'; + }; export type RenderLabelFunction = ( props: RenderLabelProps, ) => ReactNode; export type RenderLabelProps = { edge: DependencyEdge; }; + const LabelPosition: { + readonly LEFT: 'l'; + readonly RIGHT: 'r'; + readonly CENTER: 'c'; + }; export type RenderNodeFunction = ( props: RenderNodeProps, ) => ReactNode; diff --git a/packages/core-components/src/components/AutoLogout/StillTherePrompt.stories.tsx b/packages/core-components/src/components/AutoLogout/StillTherePrompt.stories.tsx index 7de1bef509..a55f4031a6 100644 --- a/packages/core-components/src/components/AutoLogout/StillTherePrompt.stories.tsx +++ b/packages/core-components/src/components/AutoLogout/StillTherePrompt.stories.tsx @@ -18,6 +18,7 @@ import { StillTherePrompt, StillTherePromptProps } from './StillTherePrompt'; export default { title: 'Data Display/StillTherePrompt', component: StillTherePrompt, + tags: ['!manifest'], }; export const Open = (args: StillTherePromptProps) => ( diff --git a/packages/core-components/src/components/Avatar/Avatar.stories.tsx b/packages/core-components/src/components/Avatar/Avatar.stories.tsx index 4843e2bb73..b51da0acb4 100644 --- a/packages/core-components/src/components/Avatar/Avatar.stories.tsx +++ b/packages/core-components/src/components/Avatar/Avatar.stories.tsx @@ -28,6 +28,7 @@ const useStyles = makeStyles({ export default { title: 'Data Display/Avatar', component: Avatar, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/Chip/Chip.stories.tsx b/packages/core-components/src/components/Chip/Chip.stories.tsx index a20a6d5699..b981d34ec7 100644 --- a/packages/core-components/src/components/Chip/Chip.stories.tsx +++ b/packages/core-components/src/components/Chip/Chip.stories.tsx @@ -53,6 +53,7 @@ export default { }, }, }, + tags: ['!manifest'], }; export const Default = (args: ChipProps) => ; diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.stories.tsx index bea5cf778e..d5dacced13 100644 --- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.stories.tsx +++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.stories.tsx @@ -20,6 +20,7 @@ import { InfoCard } from '../../layout/InfoCard'; export default { title: 'Data Display/CodeSnippet', component: CodeSnippet, + tags: ['!manifest'], }; const containerStyle = { width: 300 }; diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx index 8c31bc4e6d..e79dc07ad3 100644 --- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.tsx @@ -57,6 +57,14 @@ export interface CodeSnippetProps { * Array of line numbers to highlight */ highlightedNumbers?: number[]; + /** + * Whether to style the `` block with `white-space: pre-wrap` or `white-space: pre` + * + * @remarks + * + * Default: false (`white-space: pre`) + */ + wrapLongLines?: boolean; /** * Custom styles applied to code * @@ -79,6 +87,7 @@ export function CodeSnippet(props: CodeSnippetProps) { language, showLineNumbers = false, highlightedNumbers, + wrapLongLines, customStyle, showCopyCodeButton = false, } = props; @@ -94,6 +103,7 @@ export function CodeSnippet(props: CodeSnippetProps) { style={mode} showLineNumbers={showLineNumbers} wrapLines + wrapLongLines={wrapLongLines} lineNumberStyle={{ color: theme.palette.textVerySubtle }} lineProps={(lineNumber: number) => highlightedNumbers?.includes(lineNumber) diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.stories.tsx index 39c6782207..233c41aff8 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.stories.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -19,6 +19,7 @@ import { CopyTextButton } from './CopyTextButton'; export default { title: 'Inputs/CopyTextButton', component: CopyTextButton, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx index 3ed4a25730..ae6c235638 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx @@ -20,6 +20,7 @@ import { DependencyGraphTypes as Types } from './types'; export default { title: 'Data Display/DependencyGraph', component: DependencyGraph, + tags: ['!manifest'], }; const containerStyle = { width: '100%' }; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 4b99402c3a..476c458e60 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -79,7 +79,7 @@ export interface DependencyGraphProps */ nodes: Types.DependencyNode[]; /** - * Graph {@link DependencyGraphTypes.Direction | direction} + * Graph {@link DependencyGraphTypes.(Direction:namespace) | direction} * * @remarks * @@ -87,7 +87,7 @@ export interface DependencyGraphProps */ direction?: Types.Direction; /** - * Node {@link DependencyGraphTypes.Alignment | alignment} + * Node {@link DependencyGraphTypes.(Alignment:namespace) | alignment} */ align?: Types.Alignment; /** @@ -135,7 +135,7 @@ export interface DependencyGraphProps */ acyclicer?: 'greedy'; /** - * {@link DependencyGraphTypes.Ranker | Algorithm} used to rank nodes + * {@link DependencyGraphTypes.(Ranker:namespace) | Algorithm} used to rank nodes * * @remarks * @@ -143,7 +143,7 @@ export interface DependencyGraphProps */ ranker?: Types.Ranker; /** - * {@link DependencyGraphTypes.LabelPosition | Position} of label in relation to edge + * {@link DependencyGraphTypes.(LabelPosition:namespace) | Position} of label in relation to edge * * @remarks * @@ -533,7 +533,7 @@ export function DependencyGraph( height={graphHeight} y={maxHeight / 2 - graphHeight / 2} x={maxWidth / 2 - graphWidth / 2} - viewBox={`-25 -25 ${graphWidth + 50} ${graphHeight + 50}`} + viewBox={`0 0 ${graphWidth} ${graphHeight}`} > {graphEdges.map(e => { const edge = graph.current.edge(e) as GraphEdge; diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index 4436f8d1aa..6a75df27b5 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* We want to maintain the same information as an enum, so we disable the redeclaration warning */ +/* eslint-disable @typescript-eslint/no-redeclare */ /** * Types used to customize and provide data to {@link DependencyGraph} @@ -134,23 +136,38 @@ export namespace DependencyGraphTypes { * * @public */ - export enum Direction { + export const Direction = { /** * Top to Bottom */ - TOP_BOTTOM = 'TB', + TOP_BOTTOM: 'TB', /** * Bottom to Top */ - BOTTOM_TOP = 'BT', + BOTTOM_TOP: 'BT', /** * Left to Right */ - LEFT_RIGHT = 'LR', + LEFT_RIGHT: 'LR', /** * Right to Left */ - RIGHT_LEFT = 'RL', + RIGHT_LEFT: 'RL', + } as const; + + /** + * @public + */ + export type Direction = (typeof Direction)[keyof typeof Direction]; + + /** + * @public + */ + export namespace Direction { + export type TOP_BOTTOM = typeof Direction.TOP_BOTTOM; + export type BOTTOM_TOP = typeof Direction.BOTTOM_TOP; + export type LEFT_RIGHT = typeof Direction.LEFT_RIGHT; + export type RIGHT_LEFT = typeof Direction.RIGHT_LEFT; } /** @@ -158,23 +175,38 @@ export namespace DependencyGraphTypes { * * @public */ - export enum Alignment { + export const Alignment = { /** * Up Left */ - UP_LEFT = 'UL', + UP_LEFT: 'UL', /** * Up Right */ - UP_RIGHT = 'UR', + UP_RIGHT: 'UR', /** * Down Left */ - DOWN_LEFT = 'DL', + DOWN_LEFT: 'DL', /** * Down Right */ - DOWN_RIGHT = 'DR', + DOWN_RIGHT: 'DR', + } as const; + + /** + * @public + */ + export type Alignment = (typeof Alignment)[keyof typeof Alignment]; + + /** + * @public + */ + export namespace Alignment { + export type UP_LEFT = typeof Alignment.UP_LEFT; + export type UP_RIGHT = typeof Alignment.UP_RIGHT; + export type DOWN_LEFT = typeof Alignment.DOWN_LEFT; + export type DOWN_RIGHT = typeof Alignment.DOWN_RIGHT; } /** @@ -182,15 +214,15 @@ export namespace DependencyGraphTypes { * * @public */ - export enum Ranker { + export const Ranker = { /** * {@link https://en.wikipedia.org/wiki/Network_simplex_algorithm | Network Simplex} algorithm */ - NETWORK_SIMPLEX = 'network-simplex', + NETWORK_SIMPLEX: 'network-simplex', /** * Tight Tree algorithm */ - TIGHT_TREE = 'tight-tree', + TIGHT_TREE: 'tight-tree', /** * Longest path algorithm * @@ -198,7 +230,21 @@ export namespace DependencyGraphTypes { * * Simplest and fastest */ - LONGEST_PATH = 'longest-path', + LONGEST_PATH: 'longest-path', + } as const; + + /** + * @public + */ + export type Ranker = (typeof Ranker)[keyof typeof Ranker]; + + /** + * @public + */ + export namespace Ranker { + export type NETWORK_SIMPLEX = typeof Ranker.NETWORK_SIMPLEX; + export type TIGHT_TREE = typeof Ranker.TIGHT_TREE; + export type LONGEST_PATH = typeof Ranker.LONGEST_PATH; } /** @@ -206,9 +252,24 @@ export namespace DependencyGraphTypes { * * @public */ - export enum LabelPosition { - LEFT = 'l', - RIGHT = 'r', - CENTER = 'c', + export const LabelPosition = { + LEFT: 'l', + RIGHT: 'r', + CENTER: 'c', + } as const; + + /** + * @public + */ + export type LabelPosition = + (typeof LabelPosition)[keyof typeof LabelPosition]; + + /** + * @public + */ + export namespace LabelPosition { + export type LEFT = typeof LabelPosition.LEFT; + export type RIGHT = typeof LabelPosition.RIGHT; + export type CENTER = typeof LabelPosition.CENTER; } } diff --git a/packages/core-components/src/components/Dialog/Dialog.stories.tsx b/packages/core-components/src/components/Dialog/Dialog.stories.tsx index 7171e9c543..d80fc51773 100644 --- a/packages/core-components/src/components/Dialog/Dialog.stories.tsx +++ b/packages/core-components/src/components/Dialog/Dialog.stories.tsx @@ -27,6 +27,7 @@ import CloseIcon from '@material-ui/icons/Close'; const meta = { title: 'Layout/Dialog', component: Dialog, + tags: ['!manifest'], }; export default meta; diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx index 86c7270719..c2340c913a 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx @@ -30,6 +30,7 @@ export default { control: { type: 'select' }, }, }, + tags: ['!manifest'], }; const containerStyle = { width: '70%' }; diff --git a/packages/core-components/src/components/Drawer/Drawer.stories.tsx b/packages/core-components/src/components/Drawer/Drawer.stories.tsx index 0c7e8dbfdb..abf0ab9082 100644 --- a/packages/core-components/src/components/Drawer/Drawer.stories.tsx +++ b/packages/core-components/src/components/Drawer/Drawer.stories.tsx @@ -25,6 +25,7 @@ import Close from '@material-ui/icons/Close'; export default { title: 'Layout/Drawer', component: Drawer, + tags: ['!manifest'], }; const useDrawerStyles = makeStyles((theme: Theme) => diff --git a/packages/core-components/src/components/EmptyState/EmptyState.stories.tsx b/packages/core-components/src/components/EmptyState/EmptyState.stories.tsx index 07f6ea8fa1..a185b95e88 100644 --- a/packages/core-components/src/components/EmptyState/EmptyState.stories.tsx +++ b/packages/core-components/src/components/EmptyState/EmptyState.stories.tsx @@ -21,6 +21,7 @@ import { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState'; export default { title: 'Feedback/EmptyState', component: EmptyState, + tags: ['!manifest'], }; const containerStyle = { width: '100%', height: '100vh' }; diff --git a/packages/core-components/src/components/FavoriteToggle/FavoriteToggle.stories.tsx b/packages/core-components/src/components/FavoriteToggle/FavoriteToggle.stories.tsx index af3787023a..aec3739e6c 100644 --- a/packages/core-components/src/components/FavoriteToggle/FavoriteToggle.stories.tsx +++ b/packages/core-components/src/components/FavoriteToggle/FavoriteToggle.stories.tsx @@ -29,6 +29,7 @@ export default { decorators: [ (Story: ComponentType>) => wrapInTestApp(), ], + tags: ['!manifest'], }; export const Default = () => { diff --git a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.stories.tsx b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.stories.tsx index a02944a827..25e0b407f9 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.stories.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/HeaderIconLinkRow.stories.tsx @@ -24,6 +24,7 @@ type Props = { export default { title: 'Data Display/HeaderIconLinkRow', component: HeaderIconLinkRow, + tags: ['!manifest'], }; export const Default = (args: Props) => ; diff --git a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.stories.tsx b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.stories.tsx index 956ee6fd0e..c2d630e842 100644 --- a/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.stories.tsx +++ b/packages/core-components/src/components/HeaderIconLinkRow/IconLinkVertical.stories.tsx @@ -19,6 +19,7 @@ import LinkIcon from '@material-ui/icons/Link'; export default { title: 'Core/IconLinkVertical', component: IconLinkVertical, + tags: ['!manifest'], }; export const Primary = (args: IconLinkVerticalProps) => ( diff --git a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx index 8f5904bc1d..36b4dd2026 100644 --- a/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx +++ b/packages/core-components/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx @@ -22,6 +22,7 @@ const opacityArray = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; export default { title: 'Layout/HorizontalScrollGrid', component: HorizontalScrollGrid, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/Lifecycle/Lifecycle.stories.tsx b/packages/core-components/src/components/Lifecycle/Lifecycle.stories.tsx index 1e7db283c8..9812d0b19a 100644 --- a/packages/core-components/src/components/Lifecycle/Lifecycle.stories.tsx +++ b/packages/core-components/src/components/Lifecycle/Lifecycle.stories.tsx @@ -18,6 +18,7 @@ import { Lifecycle } from './Lifecycle'; export default { title: 'Feedback/Lifecycle', component: Lifecycle, + tags: ['!manifest'], }; export const AlphaDefault = () => ( diff --git a/packages/core-components/src/components/Link/Link.stories.tsx b/packages/core-components/src/components/Link/Link.stories.tsx index cd9cb825e5..36a73b1ef4 100644 --- a/packages/core-components/src/components/Link/Link.stories.tsx +++ b/packages/core-components/src/components/Link/Link.stories.tsx @@ -48,6 +48,7 @@ export default { { mountedRoutes: { '/hello': routeRef } }, ), ], + tags: ['!manifest'], }; export const Default = () => { diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index c132d832a2..c178ba6cbf 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -50,7 +50,7 @@ describe('', () => { External Link, ); const externalLink = screen.getByRole('link', { - name: 'External Link , Opens in a new window', + name: 'External Link, Opens in a new window', }); const externalLinkIcon = container.querySelector('svg'); expect(externalLink).not.toContainElement(externalLinkIcon); @@ -62,6 +62,7 @@ describe('', () => { External Link , ); + // Note: when externalLinkIcon is present, the SVG adds whitespace to the accessible name const externalLink = screen.getByRole('link', { name: 'External Link , Opens in a new window', }); diff --git a/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx index 9d33ad3fc1..a333d2f789 100644 --- a/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx +++ b/packages/core-components/src/components/LinkButton/LinkButton.stories.tsx @@ -63,6 +63,7 @@ export default { { mountedRoutes: { '/hello': routeRef } }, ), ], + tags: ['!manifest'], }; export const Default = () => { diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx index 984477a6f9..dba406a7c8 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -24,6 +24,7 @@ export default { decorators: [ (Story: ComponentType>) => wrapInTestApp(), ], + tags: ['!manifest'], }; const exampleLog = `Starting up task with 3 steps diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx index e920d623e8..461e058d2d 100644 --- a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -15,17 +15,20 @@ */ import { KeyboardEvent } from 'react'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import IconButton from '@material-ui/core/IconButton'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import FilterListIcon from '@material-ui/icons/FilterList'; +import { coreComponentsTranslationRef } from '../../translation'; import { LogViewerSearch } from './useLogViewerSearch'; export interface LogViewerControlsProps extends LogViewerSearch {} export function LogViewerControls(props: LogViewerControlsProps) { + const { t } = useTranslationRef(coreComponentsTranslationRef); const { resultCount, resultIndexStep, toggleShouldFilter } = props; const resultIndex = props.resultIndex ?? 0; @@ -57,7 +60,7 @@ export function LogViewerControls(props: LogViewerControlsProps) { props.setSearchInput(e.target.value)} diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index e384a8e882..1936061b1c 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -21,7 +21,7 @@ import classnames from 'classnames'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { VariableSizeList, FixedSizeList } from 'react-window'; +import { FixedSizeList, VariableSizeList } from 'react-window'; import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { LogLine } from './LogLine'; @@ -29,6 +29,7 @@ import { LogViewerControls } from './LogViewerControls'; import { HEADER_SIZE, useStyles } from './styles'; import { useLogViewerSearch } from './useLogViewerSearch'; import { useLogViewerSelection } from './useLogViewerSelection'; +import Snackbar from '@material-ui/core/Snackbar'; export interface RealLogViewerProps { text: string; @@ -47,6 +48,7 @@ export function RealLogViewer(props: RealLogViewerProps) { // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); + const [showCopyInfo, setShowCopyInfo] = useState(false); const search = useLogViewerSearch(lines); const selection = useLogViewerSelection(lines); @@ -69,19 +71,39 @@ export function RealLogViewer(props: RealLogViewerProps) { } }, [listInstance, search.resultLine, lines]); + useEffect(() => { + const hash = selection.getHash(); + if (hash.length > 0) { + history.replaceState(null, '', hash); + } + }, [selection]); + useEffect(() => { if (location.hash) { - // #line-6 -> 6 - const line = parseInt(location.hash.replace(/\D/g, ''), 10); - selection.setSelection(line, false); + selection.selectAll(location.hash); } }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleSelectLine = ( line: number, - event: { shiftKey: boolean; preventDefault: () => void }, + event: { + shiftKey: boolean; + metaKey: boolean; + ctrlKey: boolean; + preventDefault: () => void; + }, ) => { - selection.setSelection(line, event.shiftKey); + event.preventDefault(); + selection.setSelection( + line, + event.shiftKey, + event.metaKey || event.ctrlKey, + ); + }; + + const handleCopySelection = (line: number) => { + selection.copySelection(line); + setShowCopyInfo(true); }; function setRowHeight(index: number, size: number) { @@ -97,90 +119,99 @@ export function RealLogViewer(props: RealLogViewerProps) { } return ( - - {({ height, width }: { height?: number; width?: number }) => { - const commonProps = { - ref: setListInstance, - className: classes.log, - height: (height || 480) - HEADER_SIZE, - width: width || 640, - itemData: search.lines, - itemCount: search.lines.length, - }; + <> + + {({ height, width }: { height?: number; width?: number }) => { + const commonProps = { + ref: setListInstance, + className: classes.log, + height: (height || 480) - HEADER_SIZE, + width: width || 640, + itemData: search.lines, + itemCount: search.lines.length, + }; - const renderItem = ({ - index, - style, - data, - }: { - index: number; - style: React.CSSProperties; - data: AnsiLine[]; - }) => { - const line = data[index]; - const { lineNumber } = line; - return ( - - {selection.shouldShowButton(lineNumber) && ( - selection.copySelection()} - > - - - )} - handleSelectLine(lineNumber, event)} - onKeyPress={event => handleSelectLine(lineNumber, event)} + const renderItem = ({ + index, + style, + data, + }: { + index: number; + style: React.CSSProperties; + data: AnsiLine[]; + }) => { + const line = data[index]; + const { lineNumber } = line; + return ( + - {lineNumber} - - + {selection.shouldShowCopyButton(lineNumber) && ( + handleCopySelection(lineNumber)} + > + + + )} + handleSelectLine(lineNumber, event)} + onKeyPress={event => handleSelectLine(lineNumber, event)} + > + {lineNumber} + + + + ); + }; + + return ( + + + + + {shouldTextWrap ? ( + + {...commonProps} + itemSize={getRowHeight} + > + {renderItem} + + ) : ( + {...commonProps} itemSize={20}> + {renderItem} + + )} ); - }; - - return ( - - - - - {shouldTextWrap ? ( - - {...commonProps} - itemSize={getRowHeight} - > - {renderItem} - - ) : ( - {...commonProps} itemSize={20}> - {renderItem} - - )} - - ); - }} - + }} + + setShowCopyInfo(false)} + message="Lines copied to clipboard" + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + /> + ); } diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index 76e045ca32..83fb3a84ad 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -89,14 +89,20 @@ export const useStyles = makeStyles( position: 'absolute', paddingTop: 0, paddingBottom: 0, + '&:hover': { + color: theme.palette.linkHover, + }, }, lineNumber: { display: 'inline-block', textAlign: 'end', width: 60, + paddingRight: theme.spacing(1), marginRight: theme.spacing(1), cursor: 'pointer', flexShrink: 0, + color: colors.blue[300], + borderRight: `1px solid ${colors.blue[700]}`, }, textHighlight: { background: alpha(theme.palette.info.main, 0.15), diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx index 5314f4e7f6..13280d38a4 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -16,7 +16,7 @@ import { PropsWithChildren } from 'react'; import { act, renderHook } from '@testing-library/react'; -import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; +import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; import { AnsiLine } from './AnsiProcessor'; import { useLogViewerSelection } from './useLogViewerSelection'; @@ -35,10 +35,26 @@ const lines = [ new AnsiLine(3, [{ text: '3', modifiers: {} }]), new AnsiLine(4, [{ text: '4', modifiers: {} }]), new AnsiLine(5, [{ text: '5', modifiers: {} }]), + new AnsiLine(6, [{ text: '6', modifiers: {} }]), + new AnsiLine(7, [{ text: '7', modifiers: {} }]), ]; +const expectSelectedLines = (rendered: any, selectedLines: number[]) => { + expect(rendered.result.current.isSelected(1)).toBe(selectedLines.includes(1)); + expect(rendered.result.current.isSelected(2)).toBe(selectedLines.includes(2)); + expect(rendered.result.current.isSelected(3)).toBe(selectedLines.includes(3)); + expect(rendered.result.current.isSelected(4)).toBe(selectedLines.includes(4)); + expect(rendered.result.current.isSelected(5)).toBe(selectedLines.includes(5)); + expect(rendered.result.current.isSelected(6)).toBe(selectedLines.includes(6)); + expect(rendered.result.current.isSelected(7)).toBe(selectedLines.includes(7)); +}; + describe('useLogViewerSelection', () => { - it('should manage a selection', () => { + beforeEach(() => { + (copyToClipboard as jest.Mock).mockClear(); + }); + + it('should select a new line when clicked', () => { const rendered = renderHook(() => useLogViewerSelection(lines), { wrapper: ({ children }: PropsWithChildren<{}>) => ( @@ -47,77 +63,290 @@ describe('useLogViewerSelection', () => { ), }); - expect(rendered.result.current.isSelected(1)).toBe(false); - expect(rendered.result.current.isSelected(2)).toBe(false); - expect(rendered.result.current.isSelected(3)).toBe(false); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, false, false)); + expectSelectedLines(rendered, [5]); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + }); - expect(rendered.result.current.shouldShowButton(1)).toBe(false); - expect(rendered.result.current.shouldShowButton(2)).toBe(false); - expect(rendered.result.current.shouldShowButton(3)).toBe(false); + it('should deselect a selected line when clicked', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - act(() => rendered.result.current.setSelection(2, false)); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, []); + }); - expect(rendered.result.current.isSelected(1)).toBe(false); - expect(rendered.result.current.isSelected(2)).toBe(true); - expect(rendered.result.current.isSelected(3)).toBe(false); + it('should select a new line on shift+click if nothing is selected', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - expect(rendered.result.current.shouldShowButton(1)).toBe(false); - expect(rendered.result.current.shouldShowButton(2)).toBe(true); - expect(rendered.result.current.shouldShowButton(3)).toBe(false); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2]); + }); - act(() => rendered.result.current.setSelection(3, false)); + it('should deselect a single line on shift+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - expect(rendered.result.current.isSelected(1)).toBe(false); - expect(rendered.result.current.isSelected(2)).toBe(false); - expect(rendered.result.current.isSelected(3)).toBe(true); - expect(rendered.result.current.isSelected(4)).toBe(false); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, []); + }); - expect(rendered.result.current.shouldShowButton(1)).toBe(false); - expect(rendered.result.current.shouldShowButton(2)).toBe(false); - expect(rendered.result.current.shouldShowButton(3)).toBe(true); - expect(rendered.result.current.shouldShowButton(4)).toBe(false); + it('should select a range below on shift+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - act(() => rendered.result.current.setSelection(1, true)); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5]); + }); - expect(rendered.result.current.isSelected(1)).toBe(true); - expect(rendered.result.current.isSelected(2)).toBe(true); - expect(rendered.result.current.isSelected(3)).toBe(true); - expect(rendered.result.current.isSelected(4)).toBe(false); + it('should select a range above on shift+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - expect(rendered.result.current.shouldShowButton(1)).toBe(true); - expect(rendered.result.current.shouldShowButton(2)).toBe(false); - expect(rendered.result.current.shouldShowButton(3)).toBe(true); - expect(rendered.result.current.shouldShowButton(4)).toBe(false); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [5]); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5]); + }); - act(() => rendered.result.current.setSelection(4, true)); + it('should reduce a selection on shift+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); - expect(rendered.result.current.isSelected(1)).toBe(false); - expect(rendered.result.current.isSelected(2)).toBe(false); - expect(rendered.result.current.isSelected(3)).toBe(true); - expect(rendered.result.current.isSelected(4)).toBe(true); - expect(rendered.result.current.isSelected(5)).toBe(false); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(7, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5, 6, 7]); + act(() => rendered.result.current.setSelection(4, true, false)); + expectSelectedLines(rendered, [2, 3, 4]); + act(() => rendered.result.current.setSelection(2, true, false)); + expectSelectedLines(rendered, [2]); + }); - expect(rendered.result.current.shouldShowButton(1)).toBe(false); - expect(rendered.result.current.shouldShowButton(2)).toBe(false); - expect(rendered.result.current.shouldShowButton(3)).toBe(true); - expect(rendered.result.current.shouldShowButton(4)).toBe(true); - expect(rendered.result.current.shouldShowButton(5)).toBe(false); + it('should add a new selection on cmd/ctrl+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, true)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, false, true)); + expectSelectedLines(rendered, [2, 5]); + }); + + it('should merge selections on cmd/ctrl+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(1, false, true)); + expectSelectedLines(rendered, [1]); + act(() => rendered.result.current.setSelection(3, true, false)); + expectSelectedLines(rendered, [1, 2, 3]); + act(() => rendered.result.current.setSelection(5, false, true)); + expectSelectedLines(rendered, [1, 2, 3, 5]); + act(() => rendered.result.current.setSelection(7, true, false)); + expectSelectedLines(rendered, [1, 2, 3, 5, 6, 7]); + act(() => rendered.result.current.setSelection(4, false, true)); + expectSelectedLines(rendered, [1, 2, 3, 4, 5, 6, 7]); + }); + + it('should split a selection on cmd/ctrl+click', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(1, false, true)); + expectSelectedLines(rendered, [1]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [1, 2, 3, 4, 5]); + act(() => rendered.result.current.setSelection(3, false, true)); + expectSelectedLines(rendered, [1, 2, 4, 5]); + }); + + it('should copy a selected line', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); expect(copyToClipboard).not.toHaveBeenCalled(); - act(() => rendered.result.current.copySelection()); - expect(copyToClipboard).toHaveBeenLastCalledWith('3\n4'); + act(() => rendered.result.current.copySelection(2)); + expect(copyToClipboard).toHaveBeenLastCalledWith('2'); + }); - act(() => rendered.result.current.setSelection(2, true)); - act(() => rendered.result.current.setSelection(4, true)); + it('should copy a selected range', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + rendered.rerender(); - act(() => rendered.result.current.copySelection()); - expect(copyToClipboard).toHaveBeenCalledWith('2\n3\n4'); + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5]); - act(() => rendered.result.current.setSelection(2, false)); - act(() => rendered.result.current.setSelection(4, false)); - act(() => rendered.result.current.setSelection(4, false)); - act(() => rendered.result.current.setSelection(5, true)); - act(() => rendered.result.current.copySelection()); - expect(copyToClipboard).toHaveBeenCalledWith('5'); + expect(copyToClipboard).not.toHaveBeenCalled(); + act(() => rendered.result.current.copySelection(2)); + expect(copyToClipboard).toHaveBeenCalledWith('2\n3\n4\n5'); + }); + + it('should copy the correct selection', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + rendered.rerender(); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5]); + + act(() => rendered.result.current.setSelection(7, false, true)); + expectSelectedLines(rendered, [2, 3, 4, 5, 7]); + + expect(copyToClipboard).not.toHaveBeenCalled(); + act(() => rendered.result.current.copySelection(2)); + expect(copyToClipboard).toHaveBeenCalledWith('2\n3\n4\n5'); + act(() => rendered.result.current.copySelection(7)); + expect(copyToClipboard).toHaveBeenCalledWith('7'); + }); + + it('should add a single line selection to the hash', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + rendered.rerender(); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + + expect(rendered.result.current.getHash()).toBe('#lines-2'); + }); + + it('should add a range selection to the hash', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + rendered.rerender(); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(2, false, false)); + expectSelectedLines(rendered, [2]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [2, 3, 4, 5]); + + expect(rendered.result.current.getHash()).toBe('#lines-2-5'); + }); + + it('should add multiple selections to the hash', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }); + rendered.rerender(); + + expectSelectedLines(rendered, []); + act(() => rendered.result.current.setSelection(3, false, false)); + expectSelectedLines(rendered, [3]); + act(() => rendered.result.current.setSelection(5, true, false)); + expectSelectedLines(rendered, [3, 4, 5]); + act(() => rendered.result.current.setSelection(1, false, true)); + expectSelectedLines(rendered, [1, 3, 4, 5]); + act(() => rendered.result.current.setSelection(7, false, true)); + expectSelectedLines(rendered, [1, 3, 4, 5, 7]); + + expect(rendered.result.current.getHash()).toBe('#lines-3-5,1,7'); }); }); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx index 97fee53c8a..d59a552777 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx @@ -19,11 +19,14 @@ import { useEffect, useState } from 'react'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; import { AnsiLine } from './AnsiProcessor'; +type Selection = { + start: number; + end: number; +}; + export function useLogViewerSelection(lines: AnsiLine[]) { const errorApi = useApi(errorApiRef); - const [sel, setSelection] = useState<{ start: number; end: number }>(); - const start = sel ? Math.min(sel.start, sel.end) : undefined; - const end = sel ? Math.max(sel.start, sel.end) : undefined; + const [selections, setSelections] = useState([]); const [{ error }, copyToClipboard] = useCopyToClipboard(); @@ -33,38 +36,202 @@ export function useLogViewerSelection(lines: AnsiLine[]) { } }, [error, errorApi]); + const findClosestSelection = ( + allSelections: Selection[], + line: number, + ): Selection | undefined => { + if (selections.length === 0) { + return undefined; + } + let minDistance = Number.MAX_SAFE_INTEGER; + let closestSelection: Selection | undefined = undefined; + + allSelections.forEach(s => { + const distance = Math.min( + Math.abs(s.start - line), + Math.abs(s.end - line), + ); + if (distance < minDistance) { + minDistance = distance; + closestSelection = s; + } + }); + + return closestSelection; + }; + + const mergeNeighbouringSelections = ( + allSelections: Selection[], + line: number, + ): Selection[] => { + // Merge selections if they're next to each other + const neighboringSelections = allSelections.filter( + s => s.start - 1 === line || s.end + 1 === line, + ); + if (neighboringSelections.length === 0) { + return allSelections; + } + const newSelection = { + start: Math.min(line, ...neighboringSelections.map(s => s.start)), + end: Math.max(line, ...neighboringSelections.map(s => s.end)), + }; + + return [ + ...allSelections.filter( + s => + !neighboringSelections.includes(s) && + !(s.start === line && s.end === line), + ), + newSelection, + ]; + }; + return { - shouldShowButton(line: number) { - return start === line || end === line; + shouldShowCopyButton(line: number) { + // show copy button at the beginning of each selection + return selections.some(s => s.start === line); }, isSelected(line: number) { - if (!sel) { + if (!selections) { return false; } - return start! <= line && line <= end!; + // check if line is in any selection range + return selections.some( + s => s.start <= line && (s.end ?? s.start) >= line, + ); }, - setSelection(line: number, add: boolean) { - if (add) { - setSelection(s => - s ? { start: s.start, end: line } : { start: line, end: line }, + setSelection(line: number, addRange: boolean, addNewSelection: boolean) { + setSelections(currentSelections => { + const clickedSelection = currentSelections.find( + s => s.start <= line && s.end >= line, ); - } else { - setSelection(s => - s?.start === line && s?.end === line - ? undefined - : { start: line, end: line }, + const otherSelections = currentSelections.filter( + s => s !== clickedSelection, ); - } + + if (!addRange && !addNewSelection) { + // Normal click -> select only this line if nothing or multiple lines are selected + if ( + !clickedSelection || + clickedSelection.start !== clickedSelection.end + ) { + return [{ start: line, end: line }]; + } + // Clear selection if single line is selected + return []; + } + + if (addRange) { + // Shift+click -> extend/reduce selection + if (currentSelections.length === 0) { + // No existing selection -> create new selection + return [{ start: line, end: line }]; + } + + if (clickedSelection) { + // Clicked inside an existing selection -> reduce selection + if (clickedSelection.start === clickedSelection.end) { + // Single line selection -> remove it + return otherSelections; + } + // Reduce selection + return [ + ...otherSelections, + { start: clickedSelection.start, end: line }, + ]; + } + + // Extend the closest selection to the new line + const closestSelection = findClosestSelection( + currentSelections, + line, + ); + if (!closestSelection) { + // Can't actually happen + return currentSelections; + } + if (closestSelection.start < line) { + // Add lines before the selection + return mergeNeighbouringSelections( + [ + ...otherSelections.filter(s => s !== closestSelection), + { start: closestSelection.start, end: line }, + ], + line, + ); + } + // Add lines after the selection + return mergeNeighbouringSelections( + [ + ...otherSelections.filter(s => s !== closestSelection), + { start: line, end: closestSelection!.end }, + ], + line, + ); + } + + if (addNewSelection) { + // Ctrl/Cmd+click -> add new selection + if (!clickedSelection) { + // Just add new selection + return mergeNeighbouringSelections( + [...currentSelections, { start: line, end: line }], + line, + ); + } + if (clickedSelection.start === clickedSelection.end) { + // Single line selection -> remove it + return otherSelections; + } + // Multi line selection -> split it + return [ + ...otherSelections, + ...(clickedSelection.start < line + ? [{ start: clickedSelection.start, end: line - 1 }] + : []), + ...(clickedSelection.end > line + ? [{ start: line + 1, end: clickedSelection.end }] + : []), + ]; + } + + return []; + }); }, - copySelection() { - if (sel) { - const copyText = lines - .slice(Math.min(sel.start, sel.end) - 1, Math.max(sel.start, sel.end)) - .map(l => l.chunks.map(c => c.text).join('')) - .join('\n'); - copyToClipboard(copyText); - setSelection(undefined); + copySelection(line: number) { + const selection = selections.find(s => s.start === line); + if (!selection) { + return; } + const copyText = lines + .slice(selection.start - 1, selection.end) + .map(l => l.chunks.map(c => c.text).join('')) + .join('\n'); + copyToClipboard(copyText); + }, + getHash() { + if (selections.length === 0) { + return ''; + } + const parts = selections.map(s => { + if (s.start === s.end) { + return `${s.start}`; + } + return `${s.start}-${s.end}`; + }); + return `#lines-${parts.join(',')}`; + }, + selectAll(hash: string) { + const match = hash.match(/#lines-([\d,-]+)/); + const s: Selection[] = []; + if (match) { + const ranges = match[1].split(','); + ranges.forEach(r => { + const [start, end] = r.split('-').map(Number); + s.push({ start, end: end ?? start }); + }); + } + setSelections(s); }, }; } diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx index b6770e7d37..79523470af 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.stories.tsx @@ -19,6 +19,7 @@ import { MarkdownContent } from './MarkdownContent'; export default { title: 'Data Display/MarkdownContent', component: MarkdownContent, + tags: ['!manifest'], }; const markdownGithubFlavored = @@ -45,6 +46,29 @@ const markdownGithubFlavored = '* [ ] to do\n' + '* [x] done'; +const markdownGithubFlavoredWithHTML = + '# GFM with HTML\n' + + '\n' + + 'This is a paragraph with bold text and italic text.\n' + + '\n' + + '
    \n' + + '\n' + + 'Here is a list:\n' + + '\n' + + '
      \n' + + '
    • First item
    • \n' + + '
    • Second item with a link
    • \n' + + '
    • Third item
    • \n' + + '
    \n' + + '\n' + + 'And a code block:\n' + + '\n' + + '
    \n' +
    +  'function greet() {\n' +
    +  '  console.log("Hello, world!");\n' +
    +  '}\n' +
    +  '
    \n'; + const markdown = '# Choreas Iovis\n' + '\n' + @@ -117,3 +141,7 @@ export const MarkdownContentCommonMark = () => ( export const MarkdownContentGithubFlavoredCommonMark = () => ( ); + +export const MarkdownContentGithubFlavoredWithHTML = () => ( + +); diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx index 6f20f776e3..4dd40edcbf 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx @@ -163,4 +163,52 @@ describe('', () => { 'the-fitnessgram-pacer-test-is-a-multistage-aerobic-capacity-test', ); }); + + it('render MarkdownContent component with br tags for new lines in GFM dialect', async () => { + await renderInTestApp( + , + ); + + const line1 = screen.getByText(/Line 1/); + const line2 = screen.getByText(/Line 2/); + const line3 = screen.getByText(/Line 3/); + + expect(line1.nextSibling?.nodeName).toBe('BR'); + expect(line2.previousSibling?.nodeName).toBe('BR'); + expect(line2.nextSibling?.nodeName).toBe('BR'); + expect(line3.previousSibling?.nodeName).toBe('BR'); + }); + + it('render MarkdownContent component without allowing inline styles in GFM dialect', async () => { + await renderInTestApp( + , + ); + + const divElement = screen.getByText( + 'This is a custom HTML block with inline styles.', + ); + expect(divElement).toBeInTheDocument(); + expect(divElement).not.toHaveStyle('color: blue'); + expect(divElement).not.toHaveStyle('border: 1px solid black'); + expect(divElement).not.toHaveStyle('padding: 10px'); + }); + + it('render MarkdownContent component without disallowed elements in GFM dialect', async () => { + const { container } = await renderInTestApp( + , + ); + + expect(screen.getByText('Safe Content')).toBeInTheDocument(); + expect(container.querySelector('script')).toBeNull(); + expect(container.querySelector('style')).toBeNull(); + }); }); diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index 5ad23cce09..6d978ebb6f 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -20,6 +20,9 @@ import gfm from 'remark-gfm'; import { Children, createElement } from 'react'; import { CodeSnippet } from '../CodeSnippet'; import { HeadingProps } from 'react-markdown/lib/ast-to-react'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; +import type { PluggableList } from 'react-markdown/lib/react-markdown'; export type MarkdownContentClassKey = 'markdown'; @@ -108,6 +111,30 @@ const components: Options['components'] = { h6: headingRenderer, }; +const gfmRehypePlugins: PluggableList = [ + [ + rehypeRaw, + { + tagFiter: true, + }, + ], + [ + rehypeSanitize, + { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + code: [ + ...(defaultSchema.attributes?.code ?? []), + // for syntax highlighting classes in code blocks + // breaks the codesnippet component override above if omitted + ['className'], + ], + }, + }, + ], +]; + /** * Renders markdown with the default dialect {@link https://github.github.com/gfm/ | gfm - GitHub flavored Markdown} to backstage theme styled HTML. * @@ -127,6 +154,7 @@ export function MarkdownContent(props: Props) { return ( {props.text} diff --git a/packages/core-components/src/components/Progress/Progress.stories.tsx b/packages/core-components/src/components/Progress/Progress.stories.tsx index 7c7932101e..7120f699bf 100644 --- a/packages/core-components/src/components/Progress/Progress.stories.tsx +++ b/packages/core-components/src/components/Progress/Progress.stories.tsx @@ -19,6 +19,7 @@ import { Progress } from './Progress'; export default { title: 'Feedback/Progress', component: Progress, + tags: ['!manifest'], }; export const progress = () => ; diff --git a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx index 4dcb788289..9578a35864 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx @@ -21,6 +21,7 @@ const containerStyle = { width: 300 }; export default { title: 'Data Display/Gauge', component: Gauge, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index 0377c37ef0..6b673a65d5 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -34,6 +34,7 @@ const Wrapper = ({ children }: PropsWithChildren<{}>) => ( export default { title: 'Data Display/Progress Card', component: GaugeCard, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx index 295856bd51..eebfb7356c 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx @@ -21,6 +21,7 @@ const containerStyle = { width: 300 }; export default { title: 'Data Display/LinearGauge', component: LinearGauge, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/ResponseErrorPanel/ResponseErrorPanel.stories.tsx b/packages/core-components/src/components/ResponseErrorPanel/ResponseErrorPanel.stories.tsx index 7eab08f55f..050ae2ce29 100644 --- a/packages/core-components/src/components/ResponseErrorPanel/ResponseErrorPanel.stories.tsx +++ b/packages/core-components/src/components/ResponseErrorPanel/ResponseErrorPanel.stories.tsx @@ -20,6 +20,7 @@ import { ErrorPanelProps } from '../ErrorPanel'; export default { title: 'Data Display/ResponseErrorPanel', component: ResponseErrorPanel, + tags: ['!manifest'], }; export const Default = (args: ErrorPanelProps) => ( diff --git a/packages/core-components/src/components/Select/Select.stories.tsx b/packages/core-components/src/components/Select/Select.stories.tsx index f3eaea1021..d6a11244a4 100644 --- a/packages/core-components/src/components/Select/Select.stories.tsx +++ b/packages/core-components/src/components/Select/Select.stories.tsx @@ -19,6 +19,7 @@ import { SelectComponent as Select, SelectProps } from './Select'; export default { title: 'Inputs/Select', component: Select, + tags: ['!manifest'], }; const SELECT_ITEMS = [ diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx index a50183b179..d99e1b0a39 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.stories.tsx @@ -22,6 +22,7 @@ import { SimpleStepperStep } from './SimpleStepperStep'; export default { title: 'Navigation/SimpleStepper', component: SimpleStepper, + tags: ['!manifest'], }; const defaultArgs = { diff --git a/packages/core-components/src/components/Status/Status.stories.tsx b/packages/core-components/src/components/Status/Status.stories.tsx index 1237a02432..1404ae4373 100644 --- a/packages/core-components/src/components/Status/Status.stories.tsx +++ b/packages/core-components/src/components/Status/Status.stories.tsx @@ -28,6 +28,7 @@ import { InfoCard } from '../../layout/InfoCard'; export default { title: 'Data Display/Status', component: StatusOK, + tags: ['!manifest'], }; const data = [ diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx index 8a30300685..2ee11fd23d 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -41,6 +41,7 @@ const metadata = { export default { title: 'Data Display/Structured Metadata Table', component: StructuredMetadataTable, + tags: ['!manifest'], }; const Wrapper = ({ children }: PropsWithChildren<{}>) => ( diff --git a/packages/core-components/src/components/TabbedLayout/TabbedLayout.stories.tsx b/packages/core-components/src/components/TabbedLayout/TabbedLayout.stories.tsx index a4060d9d43..b0a6754fd2 100644 --- a/packages/core-components/src/components/TabbedLayout/TabbedLayout.stories.tsx +++ b/packages/core-components/src/components/TabbedLayout/TabbedLayout.stories.tsx @@ -20,6 +20,7 @@ import { TabbedLayout } from './TabbedLayout'; export default { title: 'Navigation/TabbedLayout', component: TabbedLayout, + tags: ['!manifest'], }; const Wrapper = ({ children }: PropsWithChildren<{}>) => ( diff --git a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx index 1be58d2dcf..3e4ab1df7c 100644 --- a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx @@ -47,18 +47,20 @@ describe('TabbedLayout', () => { }); expect(error).toEqual([ + expect.stringContaining( + 'Error: Child of TabbedLayout must be an TabbedLayout.Route', + ), expect.objectContaining({ - detail: new Error( - 'Child of TabbedLayout must be an TabbedLayout.Route', - ), + type: 'unhandled-exception', }), + expect.stringContaining( + 'Error: Child of TabbedLayout must be an TabbedLayout.Route', + ), expect.objectContaining({ - detail: new Error( - 'Child of TabbedLayout must be an TabbedLayout.Route', - ), + type: 'unhandled-exception', }), - expect.stringMatching( - /The above error occurred in the component/, + expect.stringContaining( + 'The above error occurred in the component', ), ]); }); diff --git a/packages/core-components/src/components/Table/Table.stories.tsx b/packages/core-components/src/components/Table/Table.stories.tsx index ac8303cde7..c17726d5fd 100644 --- a/packages/core-components/src/components/Table/Table.stories.tsx +++ b/packages/core-components/src/components/Table/Table.stories.tsx @@ -23,6 +23,7 @@ import { Table, TableColumn, TableFilter } from './Table'; export default { title: 'Data Display/Table', component: Table, + tags: ['!manifest'], }; const useStyles = makeStyles(theme => ({ diff --git a/packages/core-components/src/components/Table/Table.test.tsx b/packages/core-components/src/components/Table/Table.test.tsx index dfae66d1c2..dab5a462c2 100644 --- a/packages/core-components/src/components/Table/Table.test.tsx +++ b/packages/core-components/src/components/Table/Table.test.tsx @@ -68,7 +68,7 @@ describe('
    ', () => {
    , ); expect(rendered.getByText('second value, first row')).toHaveStyle({ - color: 'blue', + color: 'rgb(0, 0, 255)', // blue }); }); @@ -85,7 +85,7 @@ describe('
    ', () => {
    , ); expect(rendered.getByText('second value, first row')).toHaveStyle({ - color: 'blue', + color: 'rgb(0, 0, 255)', // blue 'font-weight': 700, }); }); @@ -115,10 +115,10 @@ describe('
    ', () => {
    , ); expect(rendered.getByText('second value, first row')).toHaveStyle({ - color: 'green', + color: 'rgb(0, 128, 0)', // green }); expect(rendered.getByText('second value, second row')).toHaveStyle({ - color: 'red', + color: 'rgb(255, 0, 0)', // red }); }); @@ -135,11 +135,11 @@ describe('
    ', () => {
    , ); expect(rendered.getByText('second value, first row')).toHaveStyle({ - color: 'green', + color: 'rgb(0, 128, 0)', // green 'font-weight': 700, }); expect(rendered.getByText('second value, second row')).toHaveStyle({ - color: 'red', + color: 'rgb(255, 0, 0)', // red 'font-weight': 700, }); }); @@ -165,11 +165,11 @@ describe('
    ', () => { expect(rendered.getByText(column1.title).closest('th')).not.toHaveStyle( { - backgroundColor: 'pink', + backgroundColor: 'rgb(255, 192, 203)', // pink }, ); expect(rendered.getByText(column2.title).closest('th')).toHaveStyle({ - backgroundColor: 'pink', + backgroundColor: 'rgb(255, 192, 203)', // pink }); }); diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 0287cfdf40..59220abbaf 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -186,6 +186,7 @@ const useFilterStyles = makeStyles( display: 'flex', alignItems: 'center', justifyContent: 'space-between', + flexWrap: 'wrap', }, title: { fontWeight: theme.typography.fontWeightBold, diff --git a/packages/core-components/src/components/TrendLine/TrendLine.stories.tsx b/packages/core-components/src/components/TrendLine/TrendLine.stories.tsx index 2f1e9bc4f1..710c0448f4 100644 --- a/packages/core-components/src/components/TrendLine/TrendLine.stories.tsx +++ b/packages/core-components/src/components/TrendLine/TrendLine.stories.tsx @@ -21,6 +21,7 @@ import { InfoCard } from '../../layout/InfoCard'; export default { title: 'Data Display/TrendLine', component: TrendLine, + tags: ['!manifest'], }; const containerStyle = { width: 700 }; diff --git a/packages/core-components/src/components/WarningPanel/WarningPanel.stories.tsx b/packages/core-components/src/components/WarningPanel/WarningPanel.stories.tsx index 7e45f3c398..9fa950dc0c 100644 --- a/packages/core-components/src/components/WarningPanel/WarningPanel.stories.tsx +++ b/packages/core-components/src/components/WarningPanel/WarningPanel.stories.tsx @@ -22,6 +22,7 @@ import { Link } from '../Link'; export default { title: 'Feedback/Warning Panel', component: WarningPanel, + tags: ['!manifest'], }; export const Default = () => ( diff --git a/packages/core-components/src/components/WarningPanel/WarningPanel.tsx b/packages/core-components/src/components/WarningPanel/WarningPanel.tsx index 7d9c4d472d..2e05784fd6 100644 --- a/packages/core-components/src/components/WarningPanel/WarningPanel.tsx +++ b/packages/core-components/src/components/WarningPanel/WarningPanel.tsx @@ -96,6 +96,7 @@ const useStyles = makeStyles( fontWeight: theme.typography.fontWeightBold, }, markdownContent: { + wordBreak: 'break-word', '& p': { display: 'inline', }, diff --git a/packages/core-components/src/layout/BottomLink/BottomLink.stories.tsx b/packages/core-components/src/layout/BottomLink/BottomLink.stories.tsx index 93607c35f5..345030b80c 100644 --- a/packages/core-components/src/layout/BottomLink/BottomLink.stories.tsx +++ b/packages/core-components/src/layout/BottomLink/BottomLink.stories.tsx @@ -19,6 +19,7 @@ import { BottomLink } from '../BottomLink'; export default { title: 'Layout/BottomLink', component: BottomLink, + tags: ['!manifest'], }; export const Default = (args: { link: string; title: string }) => ( diff --git a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx index f962e4673e..cec62304ee 100644 --- a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx @@ -31,6 +31,7 @@ import { Breadcrumbs } from './Breadcrumbs'; export default { title: 'Layout/Breadcrumbs', component: Breadcrumbs, + tags: ['!manifest'], }; export const InHeader = () => ( diff --git a/packages/core-components/src/layout/Content/Content.stories.tsx b/packages/core-components/src/layout/Content/Content.stories.tsx index 8879c3c51d..eb6947b3f7 100644 --- a/packages/core-components/src/layout/Content/Content.stories.tsx +++ b/packages/core-components/src/layout/Content/Content.stories.tsx @@ -18,6 +18,7 @@ import { Content } from './Content'; export default { title: 'Layout/Content', component: Content, + tags: ['!manifest'], }; type Props = { diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.stories.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.stories.tsx index 6b4ced0edd..989fba81ce 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.stories.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.stories.tsx @@ -20,6 +20,7 @@ import { ContentHeader } from '../ContentHeader'; export default { title: 'Layout/ContentHeader', component: ContentHeader, + tags: ['!manifest'], }; type ContentHeaderProps = { diff --git a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.test.tsx b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.test.tsx index 4011fd2125..65e8a55801 100644 --- a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.test.tsx +++ b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.test.tsx @@ -67,18 +67,20 @@ describe('', () => { }); expect(error).toEqual([ + expect.stringContaining('Error: Bomb'), expect.objectContaining({ - detail: new Error('Bomb'), + type: 'unhandled-exception', }), + expect.stringContaining('Error: Bomb'), expect.objectContaining({ - detail: new Error('Bomb'), + type: 'unhandled-exception', }), - expect.stringMatching( - /^The above error occurred in the component:/, + expect.stringContaining( + 'The above error occurred in the component:', ), - expect.stringMatching(/^ErrorBoundary/), - expect.stringMatching(/Warning: findDOMNode/), // React warning, unfortunate but currently true + expect.stringContaining('ErrorBoundary'), + expect.stringContaining('Warning: findDOMNode'), // React warning, unfortunate but currently true ]); - expect(error.length).toEqual(5); + expect(error.length).toEqual(7); }); }); diff --git a/packages/core-components/src/layout/Header/Header.stories.tsx b/packages/core-components/src/layout/Header/Header.stories.tsx index 3b9c67fce5..a87c89544a 100644 --- a/packages/core-components/src/layout/Header/Header.stories.tsx +++ b/packages/core-components/src/layout/Header/Header.stories.tsx @@ -36,6 +36,7 @@ export default { control: { type: 'select' }, }, }, + tags: ['!manifest'], }; const labels = ( diff --git a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.stories.tsx b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.stories.tsx index 0fc4139dfb..b087c08d06 100644 --- a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.stories.tsx +++ b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.stories.tsx @@ -18,6 +18,7 @@ import { HeaderActionMenu, HeaderActionMenuProps } from './HeaderActionMenu'; export default { title: 'Layout/HeaderActionMenu', component: HeaderActionMenu, + tags: ['!manifest'], }; export const Default = (args: HeaderActionMenuProps) => ( diff --git a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.stories.tsx b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.stories.tsx index dd93b0768c..60028eb89c 100644 --- a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.stories.tsx +++ b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.stories.tsx @@ -18,6 +18,7 @@ import { HeaderLabel } from './HeaderLabel'; export default { title: 'Layout/HeaderLabel', component: HeaderLabel, + tags: ['!manifest'], }; type HeaderLabelProps = { diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.stories.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.stories.tsx index 08ea2a0648..b551bb1d04 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.stories.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.stories.tsx @@ -18,6 +18,7 @@ import { HeaderTabs } from './HeaderTabs'; export default { title: 'Layout/HeaderTabs', component: HeaderTabs, + tags: ['!manifest'], }; export const SingleTab = (args: any) => ; diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.stories.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.stories.tsx index 72bc79b674..10400412c6 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.stories.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.stories.tsx @@ -23,6 +23,7 @@ import { InfoCard, Props } from './InfoCard'; export default { title: 'Layout/Information Card', component: InfoCard, + tags: ['!manifest'], }; const text = ( diff --git a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx index def451d418..4ab03c305f 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx @@ -27,6 +27,7 @@ import { ItemCardHeader } from './ItemCardHeader'; export default { title: 'Layout/Item Cards', + tags: ['!manifest'], }; const text = diff --git a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx index 115c9e89e1..cb1010b2c7 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx @@ -30,7 +30,7 @@ export type ItemCardHeaderClassKey = 'root'; const styles = (theme: Theme) => createStyles({ root: { - color: theme.palette.text.primary, + color: theme.getPageTheme({ themeId: 'card' }).fontColor, padding: theme.spacing(2, 2, 3), backgroundImage: theme.getPageTheme({ themeId: 'card' }).backgroundImage, backgroundPosition: 0, diff --git a/packages/core-components/src/layout/Page/Page.stories.tsx b/packages/core-components/src/layout/Page/Page.stories.tsx index aa0aedbe5d..2ec3afebb7 100644 --- a/packages/core-components/src/layout/Page/Page.stories.tsx +++ b/packages/core-components/src/layout/Page/Page.stories.tsx @@ -40,6 +40,7 @@ import { Page } from '../Page'; export default { title: 'Plugins/Examples', component: Page, + tags: ['!manifest'], }; interface TableData { diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 00fe6f96ae..bfa1e15d5e 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -97,11 +97,11 @@ const useStyles = makeStyles( { name: 'BackstageSidebar' }, ); -enum State { - Closed, - Idle, - Open, -} +const State = { + Closed: 0, + Idle: 1, + Open: 2, +} as const; /** @public */ export type SidebarProps = { @@ -144,7 +144,9 @@ const DesktopSidebar = (props: DesktopSidebarProps) => { theme => theme.breakpoints.down('md'), { noSsr: true }, ); - const [state, setState] = useState(State.Closed); + const [state, setState] = useState<(typeof State)[keyof typeof State]>( + State.Closed, + ); const hoverTimerRef = useRef(); const { isPinned, toggleSidebarPinState } = useSidebarPinState(); diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 3121228ccd..35a0793409 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -96,7 +96,7 @@ describe('Items', () => { it('should render a button with custom style', async () => { expect( await screen.findByRole('button', { name: /create/i }), - ).toHaveStyle(`background-color: transparent`); + ).toHaveStyle(`background-color: rgba(0, 0, 0, 0)`); // transparent }); it('should send button clicks to analytics', async () => { diff --git a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx index f02fad5095..d5720d00a7 100644 --- a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx @@ -48,6 +48,7 @@ export default { (Story: ComponentType>) => wrapInTestApp(, { mountedRoutes: { '/': routeRef } }), ], + tags: ['!manifest'], }; const handleSearch = (input: string) => { diff --git a/packages/core-components/src/layout/Sidebar/localStorage.ts b/packages/core-components/src/layout/Sidebar/localStorage.ts index 78ec355306..b62252e1df 100644 --- a/packages/core-components/src/layout/Sidebar/localStorage.ts +++ b/packages/core-components/src/layout/Sidebar/localStorage.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -enum LocalStorageKeys { - SIDEBAR_PIN_STATE = 'sidebarPinState', -} +const LocalStorageKeys = { + SIDEBAR_PIN_STATE: 'sidebarPinState', +} as const; export const LocalStorage = { getSidebarPinState(): boolean { diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx index 7c83dad083..c9019107ec 100644 --- a/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx +++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx @@ -36,6 +36,7 @@ export default { ), ], + tags: ['!manifest'], }; export const Default = () => { diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index aa644fa909..3d0e62747e 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -127,5 +127,10 @@ export const coreComponentsTranslationRef = createTranslationRef({ title: 'You do not appear to be signed in. Please try reloading the browser page.', }, + logViewer: { + searchField: { + placeholder: 'Search', + }, + }, }, }); diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 98ffb44043..8c6c617bcb 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/core-plugin-api +## 1.12.1 + +### Patch Changes + +- 358c6f7: The `useApp` and `useRouteRef` functions are now forwards compatible with the new frontend system. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## 1.12.1-next.0 + +### Patch Changes + +- 358c6f7: The `useApp` and `useRouteRef` functions are now forwards compatible with the new frontend system. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 1.12.0 + +### Minor Changes + +- 83439b1: All route references are now forwards compatible with the new frontend system, i.e. `@backstage/frontend-plugin-api`. This means they no longer need to be converted with `convertLegacyRouteRef` or `convertLegacyRouteRefs` from `@backstage/core-compat-api`. + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + +## 1.11.2-next.1 + +### Patch Changes + +- b2bef92: Convert all enums to erasable-syntax compliant patterns + ## 1.11.2-next.0 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index c80b6af1e8..ef4982e07e 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-plugin-api", - "version": "1.11.2-next.0", + "version": "1.12.1", "description": "Core API used by Backstage plugins", "backstage": { "role": "web-library" @@ -51,9 +51,11 @@ "dependencies": { "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", - "history": "^5.0.0" + "history": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/core-plugin-api/report-alpha.api.md b/packages/core-plugin-api/report-alpha.api.md index 6243c76cff..b0aea73868 100644 --- a/packages/core-plugin-api/report-alpha.api.md +++ b/packages/core-plugin-api/report-alpha.api.md @@ -3,253 +3,54 @@ > 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 { Expand } from '@backstage/types'; -import { ExpandRecursive } from '@backstage/types'; -import { JSX as JSX_2 } from 'react'; -import { Observable } from '@backstage/types'; -import { TranslationMessages as TranslationMessages_2 } from '@backstage/core-plugin-api/alpha'; -import { TranslationRef as TranslationRef_2 } from '@backstage/core-plugin-api/alpha'; +import { AppLanguageApi } from '@backstage/frontend-plugin-api'; +import { appLanguageApiRef } from '@backstage/frontend-plugin-api'; +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { createTranslationRef } from '@backstage/frontend-plugin-api'; +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { TranslationApi } from '@backstage/frontend-plugin-api'; +import { translationApiRef } from '@backstage/frontend-plugin-api'; +import { TranslationFunction } from '@backstage/frontend-plugin-api'; +import { TranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationMessagesOptions } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { TranslationRefOptions } from '@backstage/frontend-plugin-api'; +import { TranslationResource } from '@backstage/frontend-plugin-api'; +import { TranslationResourceOptions } from '@backstage/frontend-plugin-api'; +import { TranslationSnapshot } from '@backstage/frontend-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) -export type AppLanguageApi = { - getAvailableLanguages(): { - languages: string[]; - }; - setLanguage(language?: string): void; - getLanguage(): { - language: string; - }; - language$(): Observable<{ - language: string; - }>; -}; +export { AppLanguageApi }; -// @alpha (undocumented) -export const appLanguageApiRef: ApiRef; +export { appLanguageApiRef }; -// @alpha -export function createTranslationMessages< - TId extends string, - TMessages extends { - [key in string]: string; - }, - TFull extends boolean, ->( - options: TranslationMessagesOptions, -): TranslationMessages; +export { createTranslationMessages }; -// @alpha (undocumented) -export function createTranslationRef< - TId extends string, - const TNestedMessages extends AnyNestedMessages, - TTranslations extends { - [language in string]: () => Promise<{ - default: { - [key in keyof FlattenedMessages]: string | null; - }; - }>; - }, ->( - config: TranslationRefOptions, -): TranslationRef>; +export { createTranslationRef }; -// @alpha (undocumented) -export function createTranslationResource< - TId extends string, - TMessages extends { - [key in string]: string; - }, - TTranslations extends { - [language in string]: () => Promise<{ - default: - | TranslationMessages_2 - | { - [key in keyof TMessages]: string | null; - }; - }>; - }, ->( - options: TranslationResourceOptions, -): TranslationResource; +export { createTranslationResource }; -// @alpha (undocumented) -export type TranslationApi = { - getTranslation< - TMessages extends { - [key in string]: string; - }, - >( - translationRef: TranslationRef, - ): TranslationSnapshot; - translation$< - TMessages extends { - [key in string]: string; - }, - >( - translationRef: TranslationRef, - ): Observable>; -}; +export { TranslationApi }; -// @alpha (undocumented) -export const translationApiRef: ApiRef; +export { translationApiRef }; -// @alpha (undocumented) -export type TranslationFunction< - TMessages extends { - [key in string]: string; - }, -> = CollapsedMessages extends infer IMessages extends { - [key in string]: string; -} - ? { - ( - key: TKey, - ...[args]: TranslationFunctionOptions< - NestedMessageKeys, - PluralKeys, - IMessages, - string - > - ): IMessages[TKey]; - ( - key: TKey, - ...[args]: TranslationFunctionOptions< - NestedMessageKeys, - PluralKeys, - IMessages, - string | JSX_2.Element - > - ): JSX_2.Element; - } - : never; +export { TranslationFunction }; -// @alpha -export interface TranslationMessages< - TId extends string = string, - TMessages extends { - [key in string]: string; - } = { - [key in string]: string; - }, - TFull extends boolean = boolean, -> { - // (undocumented) - $$type: '@backstage/TranslationMessages'; - full: TFull; - id: TId; - messages: TMessages; -} +export { TranslationMessages }; -// @alpha -export interface TranslationMessagesOptions< - TId extends string, - TMessages extends { - [key in string]: string; - }, - TFull extends boolean, -> { - // (undocumented) - full?: TFull; - // (undocumented) - messages: false extends TFull - ? { - [key in keyof TMessages]?: string | null; - } - : { - [key in keyof TMessages]: string | null; - }; - // (undocumented) - ref: TranslationRef_2; -} +export { TranslationMessagesOptions }; -// @alpha (undocumented) -export interface TranslationRef< - TId extends string = string, - TMessages extends { - [key in string]: string; - } = { - [key in string]: string; - }, -> { - // (undocumented) - $$type: '@backstage/TranslationRef'; - // (undocumented) - id: TId; - // (undocumented) - T: TMessages; -} +export { TranslationRef }; -// @alpha (undocumented) -export interface TranslationRefOptions< - TId extends string, - TNestedMessages extends AnyNestedMessages, - TTranslations extends { - [language in string]: () => Promise<{ - default: { - [key in keyof FlattenedMessages]: string | null; - }; - }>; - }, -> { - // (undocumented) - id: TId; - // (undocumented) - messages: TNestedMessages; - // (undocumented) - translations?: TTranslations; -} +export { TranslationRefOptions }; -// @alpha (undocumented) -export interface TranslationResource { - // (undocumented) - $$type: '@backstage/TranslationResource'; - // (undocumented) - id: TId; -} +export { TranslationResource }; -// @alpha (undocumented) -export interface TranslationResourceOptions< - TId extends string, - TMessages extends { - [key in string]: string; - }, - TTranslations extends { - [language in string]: () => Promise<{ - default: - | TranslationMessages_2 - | { - [key in keyof TMessages]: string | null; - }; - }>; - }, -> { - // (undocumented) - ref: TranslationRef_2; - // (undocumented) - translations: TTranslations; -} +export { TranslationResourceOptions }; -// @alpha (undocumented) -export type TranslationSnapshot< - TMessages extends { - [key in string]: string; - }, -> = - | { - ready: false; - } - | { - ready: true; - t: TranslationFunction; - }; +export { TranslationSnapshot }; -// @alpha (undocumented) -export const useTranslationRef: ( - translationRef: TranslationRef, -) => { - t: TranslationFunction; -}; +export { useTranslationRef }; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index b40088dc78..dc5a042488 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -3,33 +3,87 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AlertApi } from '@backstage/frontend-plugin-api'; +import { alertApiRef } from '@backstage/frontend-plugin-api'; +import { AlertMessage } from '@backstage/frontend-plugin-api'; +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyApiRef } from '@backstage/frontend-plugin-api'; +import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ApiHolder } from '@backstage/frontend-plugin-api'; +import { ApiRef } from '@backstage/frontend-plugin-api'; +import { ApiRefConfig } from '@backstage/frontend-plugin-api'; +import { AppTheme } from '@backstage/frontend-plugin-api'; +import { AppThemeApi } from '@backstage/frontend-plugin-api'; +import { appThemeApiRef } from '@backstage/frontend-plugin-api'; +import { atlassianAuthApiRef } from '@backstage/frontend-plugin-api'; +import { AuthProviderInfo } from '@backstage/frontend-plugin-api'; +import { AuthRequestOptions } from '@backstage/frontend-plugin-api'; +import { BackstageIdentityApi } from '@backstage/frontend-plugin-api'; +import { BackstageIdentityResponse } from '@backstage/frontend-plugin-api'; import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api'; +import { BackstageUserIdentity } from '@backstage/frontend-plugin-api'; +import { bitbucketAuthApiRef } from '@backstage/frontend-plugin-api'; +import { bitbucketServerAuthApiRef } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; -import { Config } from '@backstage/config'; +import { ConfigApi } from '@backstage/frontend-plugin-api'; +import { configApiRef } from '@backstage/frontend-plugin-api'; +import { createApiFactory } from '@backstage/frontend-plugin-api'; +import { createApiRef } from '@backstage/frontend-plugin-api'; +import { DiscoveryApi } from '@backstage/frontend-plugin-api'; +import { discoveryApiRef } from '@backstage/frontend-plugin-api'; +import { ErrorApi } from '@backstage/frontend-plugin-api'; +import { ErrorApiError } from '@backstage/frontend-plugin-api'; +import { ErrorApiErrorContext } from '@backstage/frontend-plugin-api'; +import { errorApiRef } from '@backstage/frontend-plugin-api'; +import { FeatureFlag } from '@backstage/frontend-plugin-api'; +import { FeatureFlagsApi } from '@backstage/frontend-plugin-api'; +import { featureFlagsApiRef } from '@backstage/frontend-plugin-api'; +import { FeatureFlagsSaveOptions } from '@backstage/frontend-plugin-api'; +import { FeatureFlagState } from '@backstage/frontend-plugin-api'; +import { FetchApi } from '@backstage/frontend-plugin-api'; +import { fetchApiRef } from '@backstage/frontend-plugin-api'; +import { githubAuthApiRef } from '@backstage/frontend-plugin-api'; +import { gitlabAuthApiRef } from '@backstage/frontend-plugin-api'; +import { googleAuthApiRef } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/frontend-plugin-api'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/frontend-plugin-api'; import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; -import { JsonValue } from '@backstage/types'; +import { identityApiRef } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react/jsx-runtime'; -import { Observable } from '@backstage/types'; +import { microsoftAuthApiRef } from '@backstage/frontend-plugin-api'; +import { OAuthApi } from '@backstage/frontend-plugin-api'; +import { OAuthRequestApi } from '@backstage/frontend-plugin-api'; +import { oauthRequestApiRef } from '@backstage/frontend-plugin-api'; +import { OAuthRequester } from '@backstage/frontend-plugin-api'; +import { OAuthRequesterOptions } from '@backstage/frontend-plugin-api'; +import { OAuthScope } from '@backstage/frontend-plugin-api'; +import { oktaAuthApiRef } from '@backstage/frontend-plugin-api'; +import { oneloginAuthApiRef } from '@backstage/frontend-plugin-api'; +import { OpenIdConnectApi } from '@backstage/frontend-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/frontend-plugin-api'; +import { PendingOAuthRequest } from '@backstage/frontend-plugin-api'; +import { ProfileInfo } from '@backstage/frontend-plugin-api'; +import { ProfileInfoApi } from '@backstage/frontend-plugin-api'; import { PropsWithChildren } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; +import { SessionApi } from '@backstage/frontend-plugin-api'; +import { SessionState } from '@backstage/frontend-plugin-api'; +import { StorageApi } from '@backstage/frontend-plugin-api'; +import { storageApiRef } from '@backstage/frontend-plugin-api'; +import { StorageValueSnapshot } from '@backstage/frontend-plugin-api'; +import { TypesToApiRefs } from '@backstage/frontend-plugin-api'; +import { useApi } from '@backstage/frontend-plugin-api'; +import { useApiHolder } from '@backstage/frontend-plugin-api'; +import { vmwareCloudAuthApiRef } from '@backstage/frontend-plugin-api'; +import { withApis } from '@backstage/frontend-plugin-api'; -// @public -export type AlertApi = { - post(alert: AlertMessage): void; - alert$(): Observable; -}; +export { AlertApi }; -// @public -export const alertApiRef: ApiRef; +export { alertApiRef }; -// @public -export type AlertMessage = { - message: string; - severity?: 'success' | 'info' | 'warning' | 'error'; - display?: 'permanent' | 'transient'; -}; +export { AlertMessage }; // @public export type AnalyticsApi = { @@ -76,17 +130,9 @@ export type AnalyticsTracker = { ) => void; }; -// @public -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { - [key in string]: unknown; - } ->; +export { AnyApiFactory }; -// @public -export type AnyApiRef = ApiRef; +export { AnyApiRef }; // @public export type AnyExternalRoutes = { @@ -108,34 +154,13 @@ export type AnyRoutes = { [name: string]: RouteRef | SubRouteRef; }; -// @public -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { - [name in string]: unknown; - }, -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; +export { ApiFactory }; -// @public -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; +export { ApiHolder }; -// @public -export type ApiRef = { - id: string; - T: T; -}; +export { ApiRef }; -// @public -export type ApiRefConfig = { - id: string; -}; +export { ApiRefConfig }; // @public export type AppComponents = { @@ -160,30 +185,13 @@ export type AppContext = { getComponents(): AppComponents; }; -// @public -export type AppTheme = { - id: string; - title: string; - variant: 'light' | 'dark'; - icon?: React.ReactElement; - Provider(props: { children: ReactNode }): JSX.Element | null; -}; +export { AppTheme }; -// @public -export type AppThemeApi = { - getInstalledThemes(): AppTheme[]; - activeThemeId$(): Observable; - getActiveThemeId(): string | undefined; - setActiveThemeId(themeId?: string): void; -}; +export { AppThemeApi }; -// @public -export const appThemeApiRef: ApiRef; +export { appThemeApiRef }; -// @public -export const atlassianAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { atlassianAuthApiRef }; // @public export function attachComponentData

    ( @@ -192,33 +200,13 @@ export function attachComponentData

    ( data: unknown, ): void; -// @public -export type AuthProviderInfo = { - id: string; - title: string; - icon: IconComponent; - message?: string; -}; +export { AuthProviderInfo }; -// @public -export type AuthRequestOptions = { - optional?: boolean; - instantPopup?: boolean; -}; +export { AuthRequestOptions }; -// @public -export type BackstageIdentityApi = { - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; -}; +export { BackstageIdentityApi }; -// @public -export type BackstageIdentityResponse = { - token: string; - expiresAt?: Date; - identity: BackstageUserIdentity; -}; +export { BackstageIdentityResponse }; // @public export type BackstagePlugin< @@ -234,22 +222,11 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; -// @public -export type BackstageUserIdentity = { - type: 'user'; - userEntityRef: string; - ownershipEntityRefs: string[]; -}; +export { BackstageUserIdentity }; -// @public -export const bitbucketAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { bitbucketAuthApiRef }; -// @public -export const bitbucketServerAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { bitbucketServerAuthApiRef }; // @public export type BootErrorPageProps = PropsWithChildren<{ @@ -273,29 +250,13 @@ export type ComponentLoader = sync: T; }; -// @public -export type ConfigApi = Config; +export { ConfigApi }; -// @public -export const configApiRef: ApiRef; +export { configApiRef }; -// @public -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { - [name in string]: unknown; - }, ->(factory: ApiFactory): ApiFactory; +export { createApiFactory }; -// @public -export function createApiFactory( - api: ApiRef, - instance: Impl, -): ApiFactory; - -// @public -export function createApiRef(config: ApiRefConfig): ApiRef; +export { createApiRef }; // @public export function createComponentExtension< @@ -363,13 +324,9 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// @public -export type DiscoveryApi = { - getBaseUrl(pluginId: string): Promise; -}; +export { DiscoveryApi }; -// @public -export const discoveryApiRef: ApiRef; +export { discoveryApiRef }; // @public export interface ElementCollection { @@ -385,29 +342,13 @@ export interface ElementCollection { }): ElementCollection; } -// @public -export type ErrorApi = { - post(error: ErrorApiError, context?: ErrorApiErrorContext): void; - error$(): Observable<{ - error: ErrorApiError; - context?: ErrorApiErrorContext; - }>; -}; +export { ErrorApi }; -// @public -export type ErrorApiError = { - name: string; - message: string; - stack?: string; -}; +export { ErrorApiError }; -// @public -export type ErrorApiErrorContext = { - hidden?: boolean; -}; +export { ErrorApiErrorContext }; -// @public -export const errorApiRef: ApiRef; +export { errorApiRef }; // @public export type ErrorBoundaryFallbackProps = PropsWithChildren<{ @@ -429,50 +370,28 @@ export type ExternalRouteRef< $$routeRefType: 'external'; params: ParamKeys; optional?: Optional; + readonly $$type: '@backstage/ExternalRouteRef'; + readonly T: Params; }; -// @public -export type FeatureFlag = { - name: string; - pluginId: string; - description?: string; -}; +export { FeatureFlag }; -// @public -export interface FeatureFlagsApi { - getRegisteredFlags(): FeatureFlag[]; - isActive(name: string): boolean; - registerFlag(flag: FeatureFlag): void; - save(options: FeatureFlagsSaveOptions): void; -} +export { FeatureFlagsApi }; -// @public -export const featureFlagsApiRef: ApiRef; +export { featureFlagsApiRef }; // @public export type FeatureFlagsHooks = { register(name: string): void; }; -// @public -export type FeatureFlagsSaveOptions = { - states: Record; - merge?: boolean; -}; +export { FeatureFlagsSaveOptions }; -// @public -export enum FeatureFlagState { - Active = 1, - None = 0, -} +export { FeatureFlagState }; -// @public -export type FetchApi = { - fetch: typeof fetch; -}; +export { FetchApi }; -// @public -export const fetchApiRef: ApiRef; +export { fetchApiRef }; // @public export function getComponentData( @@ -480,46 +399,17 @@ export function getComponentData( type: string, ): T | undefined; -// @public -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { githubAuthApiRef }; -// @public -export const gitlabAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { gitlabAuthApiRef }; -// @public -export const googleAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { googleAuthApiRef }; -// @public -export type IconComponent = ComponentType<{ - fontSize?: 'medium' | 'large' | 'small' | 'inherit'; -}>; +export { IconComponent }; -// @public -export type IdentityApi = { - getProfileInfo(): Promise; - getBackstageIdentity(): Promise; - getCredentials(): Promise<{ - token?: string; - }>; - signOut(): Promise; -}; +export { IdentityApi }; -// @public -export const identityApiRef: ApiRef; +export { identityApiRef }; // @public @deprecated export type MakeSubRouteRef< @@ -539,75 +429,27 @@ export type MergeParams< P2 extends AnyParams, > = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); -// @public -export const microsoftAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { microsoftAuthApiRef }; -// @public -export type OAuthApi = { - getAccessToken( - scope?: OAuthScope, - options?: AuthRequestOptions, - ): Promise; -}; +export { OAuthApi }; -// @public -export type OAuthRequestApi = { - createAuthRequester( - options: OAuthRequesterOptions, - ): OAuthRequester; - authRequest$(): Observable; -}; +export { OAuthRequestApi }; -// @public -export const oauthRequestApiRef: ApiRef; +export { oauthRequestApiRef }; -// @public -export type OAuthRequester = ( - scopes: Set, -) => Promise; +export { OAuthRequester }; -// @public -export type OAuthRequesterOptions = { - provider: AuthProviderInfo; - onAuthRequest(scopes: Set): Promise; -}; +export { OAuthRequesterOptions }; -// @public -export type OAuthScope = string | string[]; +export { OAuthScope }; -// @public -export const oktaAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { oktaAuthApiRef }; -// @public -export const oneloginAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { oneloginAuthApiRef }; -// @public -export type OpenIdConnectApi = { - getIdToken(options?: AuthRequestOptions): Promise; -}; +export { OpenIdConnectApi }; -// @public -export const openshiftAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export { openshiftAuthApiRef }; // @public @deprecated export type OptionalParams< @@ -641,12 +483,7 @@ export type PathParams = { [name in ParamNames]: string; }; -// @public -export type PendingOAuthRequest = { - provider: AuthProviderInfo; - reject(): void; - trigger(): Promise; -}; +export { PendingOAuthRequest }; // @public export type PluginConfig< @@ -665,17 +502,9 @@ export type PluginFeatureFlagConfig = { name: string; }; -// @public -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; +export { ProfileInfo }; -// @public -export type ProfileInfoApi = { - getProfile(options?: AuthRequestOptions): Promise; -}; +export { ProfileInfoApi }; // @public export type RouteFunc = ( @@ -686,52 +515,24 @@ export type RouteFunc = ( export type RouteRef = { $$routeRefType: 'absolute'; params: ParamKeys; + readonly $$type: '@backstage/RouteRef'; + readonly T: Params; }; -// @public -export type SessionApi = { - signIn(): Promise; - signOut(): Promise; - sessionState$(): Observable; -}; +export { SessionApi }; -// @public -export enum SessionState { - SignedIn = 'SignedIn', - SignedOut = 'SignedOut', -} +export { SessionState }; // @public export type SignInPageProps = PropsWithChildren<{ onSignInSuccess(identityApi: IdentityApi_2): void; }>; -// @public -export interface StorageApi { - forBucket(name: string): StorageApi; - observe$( - key: string, - ): Observable>; - remove(key: string): Promise; - set(key: string, data: T): Promise; - snapshot(key: string): StorageValueSnapshot; -} +export { StorageApi }; -// @public -export const storageApiRef: ApiRef; +export { storageApiRef }; -// @public -export type StorageValueSnapshot = - | { - key: string; - presence: 'unknown' | 'absent'; - value?: undefined; - } - | { - key: string; - presence: 'present'; - value: TValue; - }; +export { StorageValueSnapshot }; // @public export type SubRouteRef = { @@ -739,21 +540,18 @@ export type SubRouteRef = { parent: RouteRef; path: string; params: ParamKeys; + readonly $$type: '@backstage/SubRouteRef'; + readonly T: Params; }; -// @public -export type TypesToApiRefs = { - [key in keyof T]: ApiRef; -}; +export { TypesToApiRefs }; // @public export function useAnalytics(): AnalyticsTracker; -// @public -export function useApi(apiRef: ApiRef): T; +export { useApi }; -// @public -export function useApiHolder(): ApiHolder; +export { useApiHolder }; // @public export const useApp: () => AppContext; @@ -780,22 +578,7 @@ export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params; -// @public -export const vmwareCloudAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; +export { vmwareCloudAuthApiRef }; -// @public -export function withApis( - apis: TypesToApiRefs, -): ( - WrappedComponent: ComponentType, -) => { - (props: PropsWithChildren>): JSX_2.Element; - displayName: string; -}; +export { withApis }; ``` diff --git a/packages/core-plugin-api/src/alpha.ts b/packages/core-plugin-api/src/alpha.ts index ebcbcf34c0..f823b909b1 100644 --- a/packages/core-plugin-api/src/alpha.ts +++ b/packages/core-plugin-api/src/alpha.ts @@ -14,5 +14,26 @@ * limitations under the License. */ -export * from './translation'; -export * from './apis/alpha'; +// Translation exports +export { + type TranslationMessages, + type TranslationMessagesOptions, + createTranslationMessages, + type TranslationResource, + type TranslationResourceOptions, + createTranslationResource, + type TranslationRef, + type TranslationRefOptions, + createTranslationRef, + useTranslationRef, +} from '@backstage/frontend-plugin-api'; + +// API definition exports +export { + appLanguageApiRef, + type AppLanguageApi, + translationApiRef, + type TranslationApi, + type TranslationFunction, + type TranslationSnapshot, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/analytics/useAnalytics.test.tsx b/packages/core-plugin-api/src/analytics/useAnalytics.test.tsx index 037e585430..45483ecb83 100644 --- a/packages/core-plugin-api/src/analytics/useAnalytics.test.tsx +++ b/packages/core-plugin-api/src/analytics/useAnalytics.test.tsx @@ -16,9 +16,12 @@ import { renderHook } from '@testing-library/react'; import { useAnalytics } from './useAnalytics'; -import { useApi } from '../apis'; +import { useApi } from '@backstage/frontend-plugin-api'; -jest.mock('../apis'); +jest.mock('@backstage/frontend-plugin-api', () => ({ + ...jest.requireActual('@backstage/frontend-plugin-api'), + useApi: jest.fn(), +})); const mocked = (f: Function) => f as jest.Mock; diff --git a/packages/core-plugin-api/src/analytics/useAnalytics.tsx b/packages/core-plugin-api/src/analytics/useAnalytics.tsx index 50cec8f790..6297f81aa2 100644 --- a/packages/core-plugin-api/src/analytics/useAnalytics.tsx +++ b/packages/core-plugin-api/src/analytics/useAnalytics.tsx @@ -15,12 +15,8 @@ */ import { useAnalyticsContext } from './AnalyticsContext'; -import { - analyticsApiRef, - AnalyticsTracker, - AnalyticsApi, - useApi, -} from '../apis'; +import { analyticsApiRef, AnalyticsTracker, AnalyticsApi } from '../apis'; +import { useApi } from '@backstage/frontend-plugin-api'; import { useRef } from 'react'; import { Tracker } from './Tracker'; diff --git a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts index afdcb6a617..a451195476 100644 --- a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts @@ -14,43 +14,8 @@ * limitations under the License. */ -import { createApiRef, ApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Message handled by the {@link AlertApi}. - * - * @public - */ -export type AlertMessage = { - message: string; - // Severity will default to success since that is what material ui defaults the value to. - severity?: 'success' | 'info' | 'warning' | 'error'; - display?: 'permanent' | 'transient'; -}; - -/** - * The alert API is used to report alerts to the app, and display them to the user. - * - * @public - */ -export type AlertApi = { - /** - * Post an alert for handling by the application. - */ - post(alert: AlertMessage): void; - - /** - * Observe alerts posted by other parts of the application. - */ - alert$(): Observable; -}; - -/** - * The {@link ApiRef} of {@link AlertApi}. - * - * @public - */ -export const alertApiRef: ApiRef = createApiRef({ - id: 'core.alert', -}); +export { + type AlertApi, + type AlertMessage, + alertApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts index ef190d6c21..c6f4eadd05 100644 --- a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -122,7 +122,7 @@ export type AnalyticsApi = { }; /** - * The {@link ApiRef} of {@link AnalyticsApi}. + * The `ApiRef` of {@link AnalyticsApi}. * * @public */ diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index e771fad597..2ad399d301 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,74 +14,8 @@ * limitations under the License. */ -import { ReactNode } from 'react'; -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Describes a theme provided by the app. - * - * @public - */ -export type AppTheme = { - /** - * ID used to remember theme selections. - */ - id: string; - - /** - * Title of the theme - */ - title: string; - - /** - * Theme variant - */ - variant: 'light' | 'dark'; - - /** - * An Icon for the theme mode setting. - */ - icon?: React.ReactElement; - - Provider(props: { children: ReactNode }): JSX.Element | null; -}; - -/** - * The AppThemeApi gives access to the current app theme, and allows switching - * to other options that have been registered as a part of the App. - * - * @public - */ -export type AppThemeApi = { - /** - * Get a list of available themes. - */ - getInstalledThemes(): AppTheme[]; - - /** - * Observe the currently selected theme. A value of undefined means no specific theme has been selected. - */ - activeThemeId$(): Observable; - - /** - * Get the current theme ID. Returns undefined if no specific theme is selected. - */ - getActiveThemeId(): string | undefined; - - /** - * Set a specific theme to use in the app, overriding the default theme selection. - * - * Clear the selection by passing in undefined. - */ - setActiveThemeId(themeId?: string): void; -}; - -/** - * The {@link ApiRef} of {@link AppThemeApi}. - * - * @public - */ -export const appThemeApiRef: ApiRef = createApiRef({ - id: 'core.apptheme', -}); +export { + type AppTheme, + type AppThemeApi, + appThemeApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts b/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts index d3bada9e46..e5e8b4559c 100644 --- a/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts @@ -13,22 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { Config } from '@backstage/config'; -/** - * The Config API is used to provide a mechanism to access the - * runtime configuration of the system. - * - * @public - */ -export type ConfigApi = Config; - -/** - * The {@link ApiRef} of {@link ConfigApi}. - * - * @public - */ -export const configApiRef: ApiRef = createApiRef({ - id: 'core.config', -}); +export { type ConfigApi, configApiRef } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts index d23fe3db6c..20c8639340 100644 --- a/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -13,43 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -/** - * The discovery API is used to provide a mechanism for plugins to - * discover the endpoint to use to talk to their backend counterpart. - * - * @remarks - * - * The purpose of the discovery API is to allow for many different deployment - * setups and routing methods through a central configuration, instead - * of letting each individual plugin manage that configuration. - * - * Implementations of the discovery API can be a simple as a URL pattern - * using the pluginId, but could also have overrides for individual plugins, - * or query a separate discovery service. - * - * @public - */ -export type DiscoveryApi = { - /** - * Returns the HTTP base backend URL for a given plugin, without a trailing slash. - * - * This method must always be called just before making a request, as opposed to - * fetching the URL when constructing an API client. That is to ensure that more - * flexible routing patterns can be supported. - * - * For example, asking for the URL for `auth` may return something - * like `https://backstage.example.com/api/auth` - */ - getBaseUrl(pluginId: string): Promise; -}; - -/** - * The {@link ApiRef} of {@link DiscoveryApi}. - * - * @public - */ -export const discoveryApiRef: ApiRef = createApiRef({ - id: 'core.discovery', -}); +export { + type DiscoveryApi, + discoveryApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 9c73d94cac..8f3f9632eb 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -14,78 +14,9 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { Observable } from '@backstage/types'; - -/** - * Mirrors the JavaScript Error class, for the purpose of - * providing documentation and optional fields. - * - * @public - */ -export type ErrorApiError = { - name: string; - message: string; - stack?: string; -}; - -/** - * Provides additional information about an error that was posted to the application. - * - * @public - */ -export type ErrorApiErrorContext = { - /** - * If set to true, this error should not be displayed to the user. - * - * Hidden errors are typically not displayed in the UI, but the ErrorApi - * implementation may still report them to error tracking services - * or other utilities that care about all errors. - * - * @defaultValue false - */ - hidden?: boolean; -}; - -/** - * The error API is used to report errors to the app, and display them to the user. - * - * @remarks - * - * Plugins can use this API as a method of displaying errors to the user, but also - * to report errors for collection by error reporting services. - * - * If an error can be displayed inline, e.g. as feedback in a form, that should be - * preferred over relying on this API to display the error. The main use of this API - * for displaying errors should be for asynchronous errors, such as a failing background process. - * - * Even if an error is displayed inline, it should still be reported through this API - * if it would be useful to collect or log it for debugging purposes, but with - * the hidden flag set. For example, an error arising from form field validation - * should probably not be reported, while a failed REST call would be useful to report. - * - * @public - */ -export type ErrorApi = { - /** - * Post an error for handling by the application. - */ - post(error: ErrorApiError, context?: ErrorApiErrorContext): void; - - /** - * Observe errors posted by other parts of the application. - */ - error$(): Observable<{ - error: ErrorApiError; - context?: ErrorApiErrorContext; - }>; -}; - -/** - * The {@link ApiRef} of {@link ErrorApi}. - * - * @public - */ -export const errorApiRef: ApiRef = createApiRef({ - id: 'core.error', -}); +export { + type ErrorApiError, + type ErrorApiErrorContext, + type ErrorApi, + errorApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index 66260afa32..9b192e784d 100644 --- a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -14,97 +14,10 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; - -/** - * Feature flag descriptor. - * - * @public - */ -export type FeatureFlag = { - name: string; - pluginId: string; - description?: string; -}; - -/** - * Enum representing the state of a feature flag (inactive/active). - * - * @public - */ -export enum FeatureFlagState { - /** - * Feature flag inactive (disabled). - */ - None = 0, - /** - * Feature flag active (enabled). - */ - Active = 1, -} - -/** - * Options to use when saving feature flags. - * - * @public - */ -export type FeatureFlagsSaveOptions = { - /** - * The new feature flag states to save. - */ - states: Record; - - /** - * Whether the saves states should be merged into the existing ones, or replace them. - * - * Defaults to false. - */ - merge?: boolean; -}; - -/** - * The feature flags API is used to toggle functionality to users across plugins and Backstage. - * - * @remarks - * - * Plugins can use this API to register feature flags that they have available - * for users to enable/disable, and this API will centralize the current user's - * state of which feature flags they would like to enable. - * - * This is ideal for Backstage plugins, as well as your own App, to trial incomplete - * or unstable upcoming features. Although there will be a common interface for users - * to enable and disable feature flags, this API acts as another way to enable/disable. - * - * @public - */ -export interface FeatureFlagsApi { - /** - * Registers a new feature flag. Once a feature flag has been registered it - * can be toggled by users, and read back to enable or disable features. - */ - registerFlag(flag: FeatureFlag): void; - - /** - * Get a list of all registered flags. - */ - getRegisteredFlags(): FeatureFlag[]; - - /** - * Whether the feature flag with the given name is currently activated for the user. - */ - isActive(name: string): boolean; - - /** - * Save the user's choice of feature flag states. - */ - save(options: FeatureFlagsSaveOptions): void; -} - -/** - * The {@link ApiRef} of {@link FeatureFlagsApi}. - * - * @public - */ -export const featureFlagsApiRef: ApiRef = createApiRef({ - id: 'core.featureflags', -}); +export { + type FeatureFlag, + type FeatureFlagsApi, + type FeatureFlagsSaveOptions, + FeatureFlagState, + featureFlagsApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts index aba0e53bb7..7fdf949e58 100644 --- a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,38 +14,4 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; - -/** - * A wrapper for the fetch API, that has additional behaviors such as the - * ability to automatically inject auth information where necessary. - * - * @public - */ -export type FetchApi = { - /** - * The `fetch` implementation. - */ - fetch: typeof fetch; -}; - -/** - * The {@link ApiRef} of {@link FetchApi}. - * - * @remarks - * - * This is a wrapper for the fetch API, that has additional behaviors such as - * the ability to automatically inject auth information where necessary. - * - * Note that the default behavior of this API (unless overridden by your org), - * is to require that the user is already signed in so that it has auth - * information to inject. Therefore, using the default implementation of this - * utility API e.g. on the `SignInPage` or similar, would cause issues. In - * special circumstances like those, you can use the regular system `fetch` - * instead. - * - * @public - */ -export const fetchApiRef: ApiRef = createApiRef({ - id: 'core.fetch', -}); +export { type FetchApi, fetchApiRef } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 1b127a1971..0648f96059 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -13,44 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { BackstageUserIdentity, ProfileInfo } from './auth'; -/** - * The Identity API used to identify and get information about the signed in user. - * - * @public - */ -export type IdentityApi = { - /** - * The profile of the signed in user. - */ - getProfileInfo(): Promise; - - /** - * User identity information within Backstage. - */ - getBackstageIdentity(): Promise; - - /** - * Provides credentials in the form of a token which proves the identity of the signed in user. - * - * The token will be undefined if the signed in user does not have a verified - * identity, such as a demo user or mocked user for e2e tests. - */ - getCredentials(): Promise<{ token?: string }>; - - /** - * Sign out the current user - */ - signOut(): Promise; -}; - -/** - * The {@link ApiRef} of {@link IdentityApi}. - * - * @public - */ -export const identityApiRef: ApiRef = createApiRef({ - id: 'core.identity', -}); +export { + type IdentityApi, + identityApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 75bf3a3864..c217b77fb5 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,118 +14,10 @@ * limitations under the License. */ -import { Observable } from '@backstage/types'; -import { ApiRef, createApiRef } from '../system'; -import { AuthProviderInfo } from './auth'; - -/** - * Describes how to handle auth requests. Both how to show them to the user, and what to do when - * the user accesses the auth request. - * - * @public - */ -export type OAuthRequesterOptions = { - /** - * Information about the auth provider, which will be forwarded to auth requests. - */ - provider: AuthProviderInfo; - - /** - * Implementation of the auth flow, which will be called synchronously when - * trigger() is called on an auth requests. - */ - onAuthRequest(scopes: Set): Promise; -}; - -/** - * Function used to trigger new auth requests for a set of scopes. - * - * @remarks - * - * The returned promise will resolve to the same value returned by the onAuthRequest in the - * {@link OAuthRequesterOptions}. Or rejected, if the request is rejected. - * - * This function can be called multiple times before the promise resolves. All calls - * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the - * union of all requested scopes. - * - * @public - */ -export type OAuthRequester = ( - scopes: Set, -) => Promise; - -/** - * An pending auth request for a single auth provider. The request will remain in this pending - * state until either reject() or trigger() is called. - * - * @remarks - * - * Any new requests for the same provider are merged into the existing pending request, meaning - * there will only ever be a single pending request for a given provider. - * - * @public - */ -export type PendingOAuthRequest = { - /** - * Information about the auth provider, as given in the AuthRequesterOptions - */ - provider: AuthProviderInfo; - - /** - * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". - */ - reject(): void; - - /** - * Trigger the auth request to continue the auth flow, by for example showing a popup. - * - * Synchronously calls onAuthRequest with all scope currently in the request. - */ - trigger(): Promise; -}; - -/** - * Provides helpers for implemented OAuth login flows within Backstage. - * - * @public - */ -export type OAuthRequestApi = { - /** - * A utility for showing login popups or similar things, and merging together multiple requests for - * different scopes into one request that includes all scopes. - * - * The passed in options provide information about the login provider, and how to handle auth requests. - * - * The returned AuthRequester function is used to request login with new scopes. These requests - * are merged together and forwarded to the auth handler, as soon as a consumer of auth requests - * triggers an auth flow. - * - * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. - */ - createAuthRequester( - options: OAuthRequesterOptions, - ): OAuthRequester; - - /** - * Observers pending auth requests. The returned observable will emit all - * current active auth request, at most one for each created auth requester. - * - * Each request has its own info about the login provider, forwarded from the auth requester options. - * - * Depending on user interaction, the request should either be rejected, or used to trigger the auth handler. - * If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError". - * If a auth is triggered, and the auth handler resolves successfully, then all currently pending - * AuthRequester calls will resolve to the value returned by the onAuthRequest call. - */ - authRequest$(): Observable; -}; - -/** - * The {@link ApiRef} of {@link OAuthRequestApi}. - * - * @public - */ -export const oauthRequestApiRef: ApiRef = createApiRef({ - id: 'core.oauthrequest', -}); +export { + type OAuthRequesterOptions, + type OAuthRequester, + type PendingOAuthRequest, + type OAuthRequestApi, + oauthRequestApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 1506e5f143..4f3af7b611 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -14,97 +14,8 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; -import { JsonValue, Observable } from '@backstage/types'; - -/** - * A snapshot in time of the current known value of a storage key. - * - * @public - */ -export type StorageValueSnapshot = - | { - key: string; - presence: 'unknown' | 'absent'; - value?: undefined; - } - | { - key: string; - presence: 'present'; - value: TValue; - }; - -/** - * Provides a key-value persistence API. - * - * @public - */ -export interface StorageApi { - /** - * Create a bucket to store data in. - * - * @param name - Namespace for the storage to be stored under, - * will inherit previous namespaces too - */ - forBucket(name: string): StorageApi; - - /** - * Remove persistent data. - * - * @param key - Unique key associated with the data. - */ - remove(key: string): Promise; - - /** - * Save persistent data, and emit messages to anyone that is using - * {@link StorageApi.observe$} for this key. - * - * @param key - Unique key associated with the data. - * @param data - The data to be stored under the key. - */ - set(key: string, data: T): Promise; - - /** - * Observe the value over time for a particular key in the current bucket. - * - * @remarks - * - * The observable will only emit values when the value changes in the underlying - * storage, although multiple values with the same shape may be emitted in a row. - * - * If a {@link StorageApi.snapshot} of a key is retrieved and the presence is - * `'unknown'`, then you are guaranteed to receive a snapshot with a known - * presence, as long as you observe the key within the same tick. - * - * Since the emitted values are shared across all subscribers, it is important - * not to mutate the returned values. The values may be frozen as a precaution. - * - * @param key - Unique key associated with the data - */ - observe$( - key: string, - ): Observable>; - - /** - * Returns an immediate snapshot value for the given key, if possible. - * - * @remarks - * - * Combine with {@link StorageApi.observe$} to get notified of value changes. - * - * Note that this method is synchronous, and some underlying storages may be - * unable to retrieve a value using this method - the result may or may not - * consistently have a presence of 'unknown'. Use {@link StorageApi.observe$} - * to be sure to receive an actual value eventually. - */ - snapshot(key: string): StorageValueSnapshot; -} - -/** - * The {@link ApiRef} of {@link StorageApi}. - * - * @public - */ -export const storageApiRef: ApiRef = createApiRef({ - id: 'core.storage', -}); +export { + type StorageValueSnapshot, + type StorageApi, + storageApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index f8686cb128..a60c84c635 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -13,481 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* We want to maintain the same information as an enum, so we disable the redeclaration warning */ +/* eslint-disable @typescript-eslint/no-redeclare */ -import { ApiRef, createApiRef } from '../system'; -import { IconComponent } from '../../icons/types'; -import { Observable } from '@backstage/types'; - -/** - * This file contains declarations for common interfaces of auth-related APIs. - * The declarations should be used to signal which type of authentication and - * authorization methods each separate auth provider supports. - * - * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, - * would be declared as follows: - * - * const googleAuthApiRef = createApiRef({ ... }) - */ - -/** - * Information about the auth provider. - * - * @remarks - * - * This information is used both to connect the correct auth provider in the backend, as - * well as displaying the provider to the user. - * - * @public - */ -export type AuthProviderInfo = { - /** - * The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`. - */ - id: string; - - /** - * Title for the auth provider, for example "GitHub" - */ - title: string; - - /** - * Icon for the auth provider. - */ - icon: IconComponent; - - /** - * Optional user friendly messaage to display for the auth provider. - */ - message?: string; -}; - -/** - * An array of scopes, or a scope string formatted according to the - * auth provider, which is typically a space separated list. - * - * @remarks - * - * See the documentation for each auth provider for the list of scopes - * supported by each provider. - * - * @public - */ -export type OAuthScope = string | string[]; - -/** - * Configuration of an authentication request. - * - * @public - */ -export type AuthRequestOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty response will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @defaultValue false - */ - optional?: boolean; - - /** - * If this is set to true, the request will bypass the regular oauth login modal - * and open the login popup directly. - * - * The method must be called synchronously from a user action for this to work in all browsers. - * - * @defaultValue false - */ - instantPopup?: boolean; -}; - -/** - * This API provides access to OAuth 2 credentials. It lets you request access tokens, - * which can be used to act on behalf of the user when talking to APIs. - * - * @public - */ -export type OAuthApi = { - /** - * Requests an OAuth 2 Access Token, optionally with a set of scopes. The access token allows - * you to make requests on behalf of the user, and the copes may grant you broader access, depending - * on the auth provider. - * - * Each auth provider has separate handling of scope, so you need to look at the documentation - * for each one to know what scope you need to request. - * - * This method is cheap and should be called each time an access token is used. Do not for example - * store the access token in React component state, as that could cause the token to expire. Instead - * fetch a new access token for each request. - * - * Be sure to include all required scopes when requesting an access token. When testing your implementation - * it is best to log out the Backstage session and then visit your plugin page directly, as - * you might already have some required scopes in your existing session. Not requesting the correct - * scopes can lead to 403 or other authorization errors, which can be tricky to debug. - * - * If the user has not yet granted access to the provider and the set of requested scopes, the user - * will be prompted to log in. The returned promise will not resolve until the user has - * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. - */ - getAccessToken( - scope?: OAuthScope, - options?: AuthRequestOptions, - ): Promise; -}; - -/** - * This API provides access to OpenID Connect credentials. It lets you request ID tokens, - * which can be passed to backend services to prove the user's identity. - * - * @public - */ -export type OpenIdConnectApi = { - /** - * Requests an OpenID Connect ID Token. - * - * This method is cheap and should be called each time an ID token is used. Do not for example - * store the id token in React component state, as that could cause the token to expire. Instead - * fetch a new id token for each request. - * - * If the user has not yet logged in to Google inside Backstage, the user will be prompted - * to log in. The returned promise will not resolve until the user has successfully logged in. - * The returned promise can be rejected, but only if the user rejects the login request. - */ - getIdToken(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to profile information of the user from an auth provider. - * - * @public - */ -export type ProfileInfoApi = { - /** - * Get profile information for the user as supplied by this auth provider. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. - */ - getProfile(options?: AuthRequestOptions): Promise; -}; - -/** - * This API provides access to the user's identity within Backstage. - * - * @remarks - * - * An auth provider that implements this interface can be used to sign-in to backstage. It is - * not intended to be used directly from a plugin, but instead serves as a connection between - * this authentication method and the app's {@link IdentityApi} - * - * @public - */ -export type BackstageIdentityApi = { - /** - * Get the user's identity within Backstage. This should normally not be called directly, - * use the {@link IdentityApi} instead. - * - * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. - */ - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; -}; - -/** - * User identity information within Backstage. - * - * @public - */ -export type BackstageUserIdentity = { - /** - * The type of identity that this structure represents. In the frontend app - * this will currently always be 'user'. - */ - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; -}; - -/** - * Token and Identity response, with the users claims in the Identity. - * - * @public - */ -export type BackstageIdentityResponse = { - /** - * The token used to authenticate the user within Backstage. - */ - token: string; - - /** - * The time at which the token expires. If not set, it can be assumed that the token does not expire. - */ - expiresAt?: Date; - - /** - * Identity information derived from the token. - */ - identity: BackstageUserIdentity; -}; - -/** - * Profile information of the user. - * - * @public - */ -export type ProfileInfo = { - /** - * Email ID. - */ - email?: string; - - /** - * Display name that can be presented to the user. - */ - displayName?: string; - - /** - * URL to an avatar image of the user. - */ - picture?: string; -}; - -/** - * Session state values passed to subscribers of the SessionApi. - * - * @public - */ -export enum SessionState { - /** - * User signed in. - */ - SignedIn = 'SignedIn', - /** - * User not signed in. - */ - SignedOut = 'SignedOut', -} - -/** - * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. - * - * @public - */ -export type SessionApi = { - /** - * Sign in with a minimum set of permissions. - */ - signIn(): Promise; - - /** - * Sign out from the current session. This will reload the page. - */ - signOut(): Promise; - - /** - * Observe the current state of the auth session. Emits the current state on subscription. - */ - sessionState$(): Observable; -}; - -/** - * Provides authentication towards Google APIs and identities. - * - * @public - * @remarks - * - * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. - * - * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, - * email and expiration information. Do not rely on any other fields, as they might not be present. - */ -export const googleAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.google', -}); - -/** - * Provides authentication towards GitHub APIs. - * - * @public - * @remarks - * - * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} - * for a full list of supported scopes. - */ -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.github', -}); - -/** - * Provides authentication towards Okta APIs. - * - * @public - * @remarks - * - * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} - * for a full list of supported scopes. - */ -export const oktaAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.okta', -}); - -/** - * Provides authentication towards GitLab APIs. - * - * @public - * @remarks - * - * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} - * for a full list of supported scopes. - */ -export const gitlabAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.gitlab', -}); - -/** - * Provides authentication towards Microsoft APIs and identities. - * - * @public - * @remarks - * - * For more info and a full list of supported scopes, see: - * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} - * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} - */ -export const microsoftAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.microsoft', -}); - -/** - * Provides authentication towards OneLogin APIs. - * - * @public - */ -export const oneloginAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.onelogin', -}); - -/** - * Provides authentication towards Bitbucket APIs. - * - * @public - * @remarks - * - * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} - * for a full list of supported scopes. - */ -export const bitbucketAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - 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. - * - * @public - * @remarks - * - * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} - * for a full list of supported scopes. - */ -export const atlassianAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.atlassian', -}); - -/** - * Provides authentication towards VMware Cloud APIs and identities. - * - * @public - * @remarks - * - * For more info about VMware Cloud identity and access management: - * - {@link https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-53D39337-D93A-4B84-BD18-DDF43C21479A.html} - */ -export const vmwareCloudAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.vmware-cloud', -}); - -/** - * Provides authentication towards OpenShift APIs and identities. - * - * @public - * @remarks - * - * See {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients} - * on how to configure the OAuth clients and - * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} - * for available scopes. - */ -export const openshiftAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.openshift', -}); +export { + type AuthProviderInfo, + type OAuthScope, + type AuthRequestOptions, + type OAuthApi, + type OpenIdConnectApi, + type ProfileInfoApi, + type BackstageIdentityApi, + type BackstageUserIdentity, + type BackstageIdentityResponse, + type ProfileInfo, + SessionState, + type SessionApi, + googleAuthApiRef, + githubAuthApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + microsoftAuthApiRef, + oneloginAuthApiRef, + bitbucketAuthApiRef, + bitbucketServerAuthApiRef, + atlassianAuthApiRef, + vmwareCloudAuthApiRef, + openshiftAuthApiRef, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/system/ApiRef.ts b/packages/core-plugin-api/src/apis/system/ApiRef.ts index adedff9f73..ffd074b9a7 100644 --- a/packages/core-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/core-plugin-api/src/apis/system/ApiRef.ts @@ -14,51 +14,5 @@ * limitations under the License. */ -import type { ApiRef } from './types'; - -/** - * API reference configuration - holds an ID of the referenced API. - * - * @public - */ -export type ApiRefConfig = { - id: string; -}; - -class ApiRefImpl implements ApiRef { - constructor(private readonly config: ApiRefConfig) { - const valid = config.id - .split('.') - .flatMap(part => part.split('-')) - .every(part => part.match(/^[a-z][a-z0-9]*$/)); - if (!valid) { - throw new Error( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, - ); - } - } - - get id(): string { - return this.config.id; - } - - // Utility for getting type of an api, using `typeof apiRef.T` - get T(): T { - throw new Error(`tried to read ApiRef.T of ${this}`); - } - - toString() { - return `apiRef{${this.config.id}}`; - } -} - -/** - * Creates a reference to an API. - * - * @param config - The descriptor of the API to reference. - * @returns An API reference. - * @public - */ -export function createApiRef(config: ApiRefConfig): ApiRef { - return new ApiRefImpl(config); -} +export { createApiRef } from '@backstage/frontend-plugin-api'; +export type { ApiRefConfig } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/system/helpers.ts b/packages/core-plugin-api/src/apis/system/helpers.ts index eaffed3003..07acaa6a53 100644 --- a/packages/core-plugin-api/src/apis/system/helpers.ts +++ b/packages/core-plugin-api/src/apis/system/helpers.ts @@ -14,61 +14,4 @@ * limitations under the License. */ -import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; - -/** - * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed - * to another function. - * - * @remarks - * - * This function doesn't actually do anything, it's only used to infer types. - * - * @public - */ -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown }, ->(factory: ApiFactory): ApiFactory; -/** - * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed - * to another function. - * - * @param api - Ref of the API that will be produced by the factory. - * @param instance - Implementation of the API to use. - * @public - */ -export function createApiFactory( - api: ApiRef, - instance: Impl, -): ApiFactory; -/** - * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed - * to another function. - * - * @remarks - * - * Creates factory from {@link ApiRef} or returns the factory itself if provided. - * - * @param factory - Existing factory or {@link ApiRef}. - * @param instance - The instance to be returned by the factory. - * @public - */ -export function createApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown }, ->( - factory: ApiFactory | ApiRef, - instance?: Impl, -): ApiFactory { - if ('id' in factory) { - return { - api: factory, - deps: {} as TypesToApiRefs, - factory: () => instance!, - }; - } - return factory; -} +export { createApiFactory } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/system/types.ts b/packages/core-plugin-api/src/apis/system/types.ts index 96614c320c..3b4ba449d3 100644 --- a/packages/core-plugin-api/src/apis/system/types.ts +++ b/packages/core-plugin-api/src/apis/system/types.ts @@ -14,61 +14,11 @@ * limitations under the License. */ -/** - * API reference. - * - * @public - */ -export type ApiRef = { - id: string; - T: T; -}; - -/** - * Catch-all {@link ApiRef} type. - * - * @public - */ -export type AnyApiRef = ApiRef; - -/** - * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. - * - * @public - */ -export type TypesToApiRefs = { [key in keyof T]: ApiRef }; - -/** - * Provides lookup of APIs through their {@link ApiRef}s. - * - * @public - */ -export type ApiHolder = { - get(api: ApiRef): T | undefined; -}; - -/** - * Describes type returning API implementations. - * - * @public - */ -export type ApiFactory< - Api, - Impl extends Api, - Deps extends { [name in string]: unknown }, -> = { - api: ApiRef; - deps: TypesToApiRefs; - factory(deps: Deps): Impl; -}; - -/** - * Catch-all {@link ApiFactory} type. - * - * @public - */ -export type AnyApiFactory = ApiFactory< - unknown, - unknown, - { [key in string]: unknown } ->; +export type { + ApiRef, + AnyApiRef, + TypesToApiRefs, + ApiHolder, + ApiFactory, + AnyApiFactory, +} from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/apis/system/useApi.tsx b/packages/core-plugin-api/src/apis/system/useApi.tsx index 30e39033f7..a2caec8aea 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.tsx @@ -14,81 +14,4 @@ * limitations under the License. */ -import { ComponentType, PropsWithChildren } from 'react'; -import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; -import { useVersionedContext } from '@backstage/version-bridge'; -import { NotImplementedError } from '@backstage/errors'; - -/** - * React hook for retrieving {@link ApiHolder}, an API catalog. - * - * @public - */ -export function useApiHolder(): ApiHolder { - const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); - if (!versionedHolder) { - throw new NotImplementedError('API context is not available'); - } - - const apiHolder = versionedHolder.atVersion(1); - if (!apiHolder) { - throw new NotImplementedError('ApiContext v1 not available'); - } - return apiHolder; -} - -/** - * React hook for retrieving APIs. - * - * @param apiRef - Reference of the API to use. - * @public - */ -export function useApi(apiRef: ApiRef): T { - const apiHolder = useApiHolder(); - - const api = apiHolder.get(apiRef); - if (!api) { - throw new NotImplementedError(`No implementation available for ${apiRef}`); - } - return api; -} - -/** - * Wrapper for giving component an API context. - * - * @param apis - APIs for the context. - * @public - */ -export function withApis(apis: TypesToApiRefs) { - return function withApisWrapper( - WrappedComponent: ComponentType, - ) { - const Hoc = (props: PropsWithChildren>) => { - const apiHolder = useApiHolder(); - - const impls = {} as T; - - for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref = apis[key]; - - const api = apiHolder.get(ref); - if (!api) { - throw new NotImplementedError( - `No implementation available for ${ref}`, - ); - } - impls[key] = api; - } - } - - return ; - }; - const displayName = - WrappedComponent.displayName || WrappedComponent.name || 'Component'; - - Hoc.displayName = `withApis(${displayName})`; - - return Hoc; - }; -} +export { useApiHolder, useApi, withApis } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/app/useApp.test.tsx b/packages/core-plugin-api/src/app/useApp.test.tsx index 34c956138e..2cee7f6f3c 100644 --- a/packages/core-plugin-api/src/app/useApp.test.tsx +++ b/packages/core-plugin-api/src/app/useApp.test.tsx @@ -15,20 +15,129 @@ */ import { renderHook } from '@testing-library/react'; +import { PropsWithChildren } from 'react'; import { createVersionedContextForTesting } from '@backstage/version-bridge'; +import { + appTreeApiRef, + iconsApiRef, + AppTreeApi, + IconsApi, + AppTree, + AppNode, +} from '@backstage/frontend-plugin-api'; import { useApp } from './useApp'; +import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; -describe('v1 consumer', () => { - const context = createVersionedContextForTesting('app-context'); +describe('useApp', () => { + describe('old system', () => { + const context = createVersionedContextForTesting('app-context'); - afterEach(() => { - context.reset(); + afterEach(() => { + context.reset(); + }); + + it('should provide an app context', () => { + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + {children} + ); + context.set({ 1: 'context-value' }); + + const renderedHook = renderHook(() => useApp(), { wrapper }); + expect(renderedHook.result.current).toBe('context-value'); + }); }); - it('should provide an app context', () => { - context.set({ 1: 'context-value' }); + describe('new system', () => { + const mockIcon = () => null; + const mockIconsApi: IconsApi = { + getIcon: jest.fn((key: string) => + key === 'test-icon' ? mockIcon : undefined, + ), + listIconKeys: jest.fn(() => ['test-icon']), + }; - const renderedHook = renderHook(() => useApp()); - expect(renderedHook.result.current).toBe('context-value'); + const mockPlugin = { + id: 'test-plugin', + }; + + const mockAppNode: AppNode = { + spec: { + id: 'test-node', + attachTo: { id: 'root', input: 'children' }, + extension: {} as any, + disabled: false, + plugin: mockPlugin as any, + }, + edges: { + attachments: new Map(), + }, + }; + + const mockAppTree: AppTree = { + root: mockAppNode, + nodes: new Map([['test-node', mockAppNode]]), + orphans: [], + }; + + const mockAppTreeApi: AppTreeApi = { + getTree: jest.fn(() => ({ tree: mockAppTree })), + getNodesByRoutePath: jest.fn(() => ({ nodes: [] })), + }; + + it('should provide an app context from new app system', () => { + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ); + + const renderedHook = renderHook(() => useApp(), { wrapper }); + + const appContext = renderedHook.result.current; + expect(appContext).toBeDefined(); + expect(appContext.getPlugins()).toHaveLength(1); + expect(appContext.getPlugins()[0].getId()).toBe('test-plugin'); + expect(appContext.getSystemIcon('test-icon')).toBe(mockIcon); + expect(appContext.getSystemIcons()).toEqual({ 'test-icon': mockIcon }); + expect(appContext.getComponents().Progress).toBeDefined(); + expect(appContext.getComponents().NotFoundErrorPage).toBeDefined(); + }); + + it('should error on missing appTreeApi or iconsApi', () => { + withLogCollector(['error'], () => { + expect(() => + renderHook(() => useApp(), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + {children} + ), + }), + ).toThrow('App context is not available'); + + expect(() => + renderHook(() => useApp(), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }), + ).toThrow('App context is not available'); + + expect(() => + renderHook(() => useApp(), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ), + }), + ).toThrow('App context is not available'); + }); + }); }); }); diff --git a/packages/core-plugin-api/src/app/useApp.tsx b/packages/core-plugin-api/src/app/useApp.tsx index 05723ee387..fde807c317 100644 --- a/packages/core-plugin-api/src/app/useApp.tsx +++ b/packages/core-plugin-api/src/app/useApp.tsx @@ -14,18 +14,159 @@ * limitations under the License. */ +import { useMemo } from 'react'; import { useVersionedContext } from '@backstage/version-bridge'; +import { + appTreeApiRef, + iconsApiRef, + useApiHolder, + ErrorDisplay, + NotFoundErrorPage, + Progress, + createFrontendPlugin, + FrontendPlugin, + ApiHolder, +} from '@backstage/frontend-plugin-api'; +import { + AppComponents, + IconComponent, + BackstagePlugin, +} from '@backstage/core-plugin-api'; +import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; +const legacyPluginStore = getOrCreateGlobalSingleton( + 'legacy-plugin-compatibility-store', + () => new WeakMap(), +); + +function toLegacyPlugin(plugin: FrontendPlugin): BackstagePlugin { + let legacy = legacyPluginStore.get(plugin); + if (legacy) { + return legacy; + } + + const errorMsg = 'Not implemented in legacy plugin compatibility layer'; + const notImplemented = () => { + throw new Error(errorMsg); + }; + + legacy = { + getId(): string { + return plugin.id; + }, + get routes() { + return {}; + }, + get externalRoutes() { + return {}; + }, + getApis: notImplemented, + getFeatureFlags: notImplemented, + provide: notImplemented, + }; + + legacyPluginStore.set(plugin, legacy); + return legacy; +} + +function toNewPlugin(plugin: BackstagePlugin): FrontendPlugin { + return createFrontendPlugin({ + pluginId: plugin.getId(), + }); +} + +function useOptionalApiHolder(): ApiHolder | undefined { + try { + return useApiHolder(); + } catch { + return undefined; + } +} + /** * React hook providing {@link AppContext}. * * @public */ export const useApp = (): AppContextV1 => { + const apiHolder = useOptionalApiHolder(); + const appTreeApi = apiHolder?.get(appTreeApiRef); + const iconsApi = apiHolder?.get(iconsApiRef); const versionedContext = useVersionedContext<{ 1: AppContextV1 }>( 'app-context', ); + + const newAppContext = useMemo(() => { + if (!appTreeApi) { + return undefined; + } + + if (!iconsApi) { + return undefined; + } + + const { tree } = appTreeApi.getTree(); + + let gatheredPlugins: BackstagePlugin[] | undefined = undefined; + + const ErrorBoundaryFallbackWrapper: AppComponents['ErrorBoundaryFallback'] = + ({ plugin, ...rest }) => ( + + ); + + return { + getPlugins(): BackstagePlugin[] { + if (gatheredPlugins) { + return gatheredPlugins; + } + + const pluginSet = new Set(); + for (const node of tree.nodes.values()) { + const plugin = node.spec.plugin; + if (plugin) { + pluginSet.add(toLegacyPlugin(plugin)); + } + } + gatheredPlugins = Array.from(pluginSet); + + return gatheredPlugins; + }, + + getSystemIcon(key: string): IconComponent | undefined { + return iconsApi.getIcon(key); + }, + + getSystemIcons(): Record { + return Object.fromEntries( + iconsApi.listIconKeys().map(key => [key, iconsApi.getIcon(key)!]), + ); + }, + + getComponents(): AppComponents { + return { + NotFoundErrorPage: NotFoundErrorPage, + BootErrorPage() { + throw new Error( + 'The BootErrorPage app component should not be accessed by plugins', + ); + }, + Progress: Progress, + Router() { + throw new Error( + 'The Router app component should not be accessed by plugins', + ); + }, + ErrorBoundaryFallback: ErrorBoundaryFallbackWrapper, + }; + }, + }; + }, [appTreeApi, iconsApi]); + + if (newAppContext) { + return newAppContext; + } + if (!versionedContext) { throw new Error('App context is not available'); } diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index 75bb4ae443..59fe0d118d 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -26,7 +26,6 @@ import { createReactExtension, createRoutableExtension, } from './extensions'; -import { ForwardedError } from '@backstage/errors'; jest.mock('../app'); @@ -117,7 +116,13 @@ describe('extensions', () => { render(); }); screen.getByText('Error in my-plugin'); - expect(errors[0]).toMatchObject({ detail: new Error('Test error') }); + expect(errors).toEqual([ + expect.stringContaining('Error: Test error'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('Error: Test error'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('The above error occurred in'), + ]); }); it('should handle failed lazy loads', async () => { @@ -154,12 +159,17 @@ describe('extensions', () => { screen.getByText( 'Error in my-plugin: Error: Failed lazy loading of the BrokenComponent extension, try to reload the page; caused by Error: Test error', ); - expect(errors[0]).toMatchObject({ - detail: new ForwardedError( - 'Failed lazy loading of the BrokenComponent extension, try to reload the page', - new Error('Test error'), + expect(errors).toEqual([ + expect.stringContaining( + 'Error: Failed lazy loading of the BrokenComponent extension, try to reload the page', ), - }); + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'Error: Failed lazy loading of the BrokenComponent extension, try to reload the page', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('The above error occurred in'), + ]); }); it('should wrap extended component with analytics context', async () => { diff --git a/packages/core-plugin-api/src/icons/types.ts b/packages/core-plugin-api/src/icons/types.ts index 2b00e1456f..2e64828584 100644 --- a/packages/core-plugin-api/src/icons/types.ts +++ b/packages/core-plugin-api/src/icons/types.ts @@ -14,24 +14,4 @@ * limitations under the License. */ -import { ComponentType } from 'react'; - -/** - * IconComponent is the common icon type used throughout Backstage when - * working with and rendering generic icons, including the app system icons. - * - * @remarks - * - * The type is based on SvgIcon from Material UI, but we do not want the plugin-api - * package to have a dependency on Material UI, nor do we want the props to be as broad - * as the SvgIconProps interface. - * - * If you have the need to forward additional props from SvgIconProps, you can - * open an issue or submit a PR to the main Backstage repo. When doing so please - * also describe your use-case and reasoning of the addition. - * - * @public - */ -export type IconComponent = ComponentType<{ - fontSize?: 'medium' | 'large' | 'small' | 'inherit'; -}>; +export type { IconComponent } from '@backstage/frontend-plugin-api'; diff --git a/packages/core-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/core-plugin-api/src/routing/ExternalRouteRef.test.ts index 05e7a23d5d..8c3a5f0802 100644 --- a/packages/core-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/core-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -16,6 +16,7 @@ import { AnyParams, ExternalRouteRef } from './types'; import { createExternalRouteRef } from './ExternalRouteRef'; +import { RouteResolutionApi, RouteFunc } from '@backstage/frontend-plugin-api'; describe('ExternalRouteRef', () => { it('should be created', () => { @@ -24,7 +25,9 @@ describe('ExternalRouteRef', () => { }); expect(routeRef.params).toEqual([]); expect(routeRef.optional).toBe(false); - expect(String(routeRef)).toBe('routeRef{type=external,id=my-route-ref}'); + expect(String(routeRef)).toMatch( + /^routeRef\{type=external,id=my-route-ref\}$/, + ); }); it('should be created as optional', () => { @@ -109,4 +112,48 @@ describe('ExternalRouteRef', () => { // To avoid complains about missing expectations and unused vars expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); }); + + describe('with new frontend system', () => { + const routeResolutionApi = { resolve: jest.fn() } as RouteResolutionApi; + + function expectType(): ( + v: U, + ) => [T, U] extends [U, T] ? { ok(): void } : { invalid: U } { + return () => ({ ok() {} } as any); + } + + it('should resolve routes correctly', () => { + expectType | undefined>()( + routeResolutionApi.resolve(createExternalRouteRef({ id: '1' })), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createExternalRouteRef({ id: '1', optional: true }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createExternalRouteRef({ id: '1', optional: false }), + ), + ).ok(); + + expectType | undefined>()( + routeResolutionApi.resolve( + createExternalRouteRef({ id: '1', params: ['x'] }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createExternalRouteRef({ id: '1', params: ['x'], optional: true }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createExternalRouteRef({ id: '1', params: ['x'], optional: false }), + ), + ).ok(); + + expect(1).toBe(1); + }); + }); }); diff --git a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts index 16e2f9bc72..5d33aa7a67 100644 --- a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts @@ -52,12 +52,45 @@ export class ExternalRouteRefImpl< } toString() { + if (this.#nfsId) { + return `externalRouteRef{id=${this.#nfsId},legacyId=${this.id}}`; + } return `routeRef{type=external,id=${this.id}}`; } getDefaultTarget() { return this.defaultTarget; } + + // NFS implementation below + readonly $$type = '@backstage/ExternalRouteRef'; + readonly version = 'v1'; + readonly T = undefined as any; + + #nfsId: string | undefined = undefined; + + getParams(): string[] { + return this.params as string[]; + } + getDescription(): string { + if (this.#nfsId) { + return this.#nfsId; + } + return this.id; + } + setId(newId: string) { + if (!newId) { + throw new Error(`ExternalRouteRef id must be a non-empty string`); + } + if (this.#nfsId && this.#nfsId !== newId) { + throw new Error( + `ExternalRouteRef was referenced twice as both '${ + this.#nfsId + }' and '${newId}'`, + ); + } + this.#nfsId = newId; + } } /** @@ -106,5 +139,5 @@ export function createExternalRouteRef< (options.params ?? []) as ParamKeys>, Boolean(options.optional) as Optional, options?.defaultTarget, - ); + ) as ExternalRouteRef, Optional>; } diff --git a/packages/core-plugin-api/src/routing/RouteRef.test.ts b/packages/core-plugin-api/src/routing/RouteRef.test.ts index 489900d892..29ab228b9f 100644 --- a/packages/core-plugin-api/src/routing/RouteRef.test.ts +++ b/packages/core-plugin-api/src/routing/RouteRef.test.ts @@ -16,6 +16,7 @@ import { AnyParams, RouteRef, ParamKeys } from './types'; import { createRouteRef } from './RouteRef'; +import { RouteResolutionApi, RouteFunc } from '@backstage/frontend-plugin-api'; describe('RouteRef', () => { it('should be created', () => { @@ -23,7 +24,9 @@ describe('RouteRef', () => { id: 'my-route-ref', }); expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=absolute,id=my-route-ref}'); + expect(String(routeRef)).toMatch( + /^routeRef\{type=absolute,id=my-route-ref\}$/, + ); }); it('should be created with params', () => { @@ -95,4 +98,37 @@ describe('RouteRef', () => { expect(true).toBeDefined(); }); + + describe('with new frontend system', () => { + const routeResolutionApi = { resolve: jest.fn() } as RouteResolutionApi; + + function expectType(): ( + v: U, + ) => [T, U] extends [U, T] ? { ok(): void } : { invalid: U } { + return () => ({ ok() {} } as any); + } + + it('should resolve routes correctly', () => { + expectType | undefined>()( + routeResolutionApi.resolve(createRouteRef({ id: '1' })), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve(createRouteRef({ id: '1' })), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve(createRouteRef({ id: '1', params: [] })), + ).ok(); + + expectType | undefined>()( + routeResolutionApi.resolve(createRouteRef({ id: '1', params: ['x'] })), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createRouteRef({ id: '1', params: ['x', 'y'] }), + ), + ).ok(); + + expect(1).toBe(1); + }); + }); }); diff --git a/packages/core-plugin-api/src/routing/RouteRef.ts b/packages/core-plugin-api/src/routing/RouteRef.ts index d78f3ddb8d..c8465dafde 100644 --- a/packages/core-plugin-api/src/routing/RouteRef.ts +++ b/packages/core-plugin-api/src/routing/RouteRef.ts @@ -45,8 +45,40 @@ export class RouteRefImpl } toString() { + if (this.#nfsId) { + return `routeRef{id=${this.#nfsId},legacyId=${this.id}}`; + } return `routeRef{type=absolute,id=${this.id}}`; } + + // NFS implementation below + readonly $$type = '@backstage/RouteRef'; + readonly version = 'v1'; + readonly T = undefined as any; + readonly alias = undefined; + + #nfsId: string | undefined = undefined; + + getParams() { + return this.params; + } + getDescription() { + if (this.#nfsId) { + return this.#nfsId; + } + return this.id; + } + setId(newId: string) { + if (!newId) { + throw new Error(`RouteRef id must be a non-empty string`); + } + if (this.#nfsId && this.#nfsId !== newId) { + throw new Error( + `RouteRef was referenced twice as both '${this.#nfsId}' and '${newId}'`, + ); + } + this.#nfsId = newId; + } } /** @@ -72,5 +104,5 @@ export function createRouteRef< return new RouteRefImpl( config.id, (config.params ?? []) as ParamKeys>, - ); + ) as RouteRef>; } diff --git a/packages/core-plugin-api/src/routing/SubRouteRef.test.ts b/packages/core-plugin-api/src/routing/SubRouteRef.test.ts index 1db429e178..ccec4c97d6 100644 --- a/packages/core-plugin-api/src/routing/SubRouteRef.test.ts +++ b/packages/core-plugin-api/src/routing/SubRouteRef.test.ts @@ -17,6 +17,7 @@ import { AnyParams, SubRouteRef } from './types'; import { createSubRouteRef } from './SubRouteRef'; import { createRouteRef } from './RouteRef'; +import { RouteResolutionApi, RouteFunc } from '@backstage/frontend-plugin-api'; const parent = createRouteRef({ id: 'parent' }); const parentX = createRouteRef({ id: 'parent-x', params: ['x'] }); @@ -31,7 +32,7 @@ describe('SubRouteRef', () => { expect(routeRef.path).toBe('/foo'); expect(routeRef.parent).toBe(parent); expect(routeRef.params).toEqual([]); - expect(String(routeRef)).toBe('routeRef{type=sub,id=my-route-ref}'); + expect(String(routeRef)).toMatch(/^routeRef\{type=sub,id=my-route-ref\}$/); }); it('should be created with params', () => { @@ -125,4 +126,51 @@ describe('SubRouteRef', () => { // To avoid complains about missing expectations and unused vars expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); }); + + describe('with new frontend system', () => { + const routeResolutionApi = { resolve: jest.fn() } as RouteResolutionApi; + + function expectType(): ( + v: U, + ) => [T, U] extends [U, T] ? { ok(): void } : { invalid: U } { + return () => ({ ok() {} } as any); + } + + it('should resolve routes correctly', () => { + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent, path: '/foo' }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent: parentX, path: '/foo' }), + ), + ).ok(); + + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent, path: '/:y' }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent: parentX, path: '/:y' }), + ), + ).ok(); + + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent, path: '/:y/:z' }), + ), + ).ok(); + expectType | undefined>()( + routeResolutionApi.resolve( + createSubRouteRef({ id: '1', parent: parentX, path: '/:y/:z' }), + ), + ).ok(); + + expect(1).toBe(1); + }); + }); }); diff --git a/packages/core-plugin-api/src/routing/SubRouteRef.ts b/packages/core-plugin-api/src/routing/SubRouteRef.ts index ed9ded41d4..8db4c1947b 100644 --- a/packages/core-plugin-api/src/routing/SubRouteRef.ts +++ b/packages/core-plugin-api/src/routing/SubRouteRef.ts @@ -56,6 +56,21 @@ export class SubRouteRefImpl toString() { return `routeRef{type=sub,id=${this.id}}`; } + + // NFS implementation below + readonly $$type = '@backstage/SubRouteRef'; + readonly version = 'v1'; + readonly T = undefined as any; + + getParams(): string[] { + return this.params as string[]; + } + getParent(): RouteRef { + return this.parent; + } + getDescription(): string { + return `at ${this.path} with parent ${this.parent}`; + } } /** @@ -155,7 +170,7 @@ export function createSubRouteRef< path, parent, params as ParamKeys>, - ) as SubRouteRef>>; + ); // But skip type checking of the return value itself, because the conditional // type checking of the parent parameter overlap is tricky to express. diff --git a/packages/core-plugin-api/src/routing/types.ts b/packages/core-plugin-api/src/routing/types.ts index 4598574afb..b0a4d83918 100644 --- a/packages/core-plugin-api/src/routing/types.ts +++ b/packages/core-plugin-api/src/routing/types.ts @@ -98,6 +98,11 @@ export type RouteRef = { /** @deprecated access to this property will be removed in the future */ params: ParamKeys; + + /** Compatibility field for new frontend system */ + readonly $$type: '@backstage/RouteRef'; + /** Compatibility field for new frontend system */ + readonly T: Params; }; /** @@ -120,6 +125,11 @@ export type SubRouteRef = { /** @deprecated access to this property will be removed in the future */ params: ParamKeys; + + /** Compatibility field for new frontend system */ + readonly $$type: '@backstage/SubRouteRef'; + /** Compatibility field for new frontend system */ + readonly T: Params; }; /** @@ -142,6 +152,11 @@ export type ExternalRouteRef< params: ParamKeys; optional?: Optional; + + /** Compatibility field for new frontend system */ + readonly $$type: '@backstage/ExternalRouteRef'; + /** Compatibility field for new frontend system */ + readonly T: Params; }; /** diff --git a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx index b96fc5fc23..325690b6eb 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx @@ -18,140 +18,203 @@ import { renderHook } from '@testing-library/react'; import { PropsWithChildren } from 'react'; import { MemoryRouter, Router } from 'react-router-dom'; import { createVersionedContextForTesting } from '@backstage/version-bridge'; +import { + routeResolutionApiRef, + RouteResolutionApi, + RouteFunc, +} from '@backstage/frontend-plugin-api'; import { useRouteRef } from './useRouteRef'; import { createRouteRef } from './RouteRef'; +import { createExternalRouteRef } from './ExternalRouteRef'; import { createBrowserHistory } from 'history'; +import { TestApiProvider } from '@backstage/test-utils'; -describe('v1 consumer', () => { - const context = createVersionedContextForTesting('routing-context'); +describe('useRouteRef', () => { + describe('old app system', () => { + const context = createVersionedContextForTesting('routing-context'); - afterEach(() => { - context.reset(); - }); - - it('should resolve routes', () => { - const resolve = jest.fn(() => () => '/hello'); - context.set({ 1: { resolve } }); - - const routeRef = createRouteRef({ id: 'ref1' }); - - const renderedHook = renderHook(() => useRouteRef(routeRef), { - wrapper: ({ children }: PropsWithChildren<{}>) => ( - - ), + afterEach(() => { + context.reset(); }); - const routeFunc = renderedHook.result.current; - expect(routeFunc()).toBe('/hello'); - expect(resolve).toHaveBeenCalledWith( - routeRef, - expect.objectContaining({ - pathname: '/my-page', - }), - ); - }); + it('should resolve routes', () => { + const resolve = jest.fn(() => () => '/hello'); + context.set({ 1: { resolve } }); - it('re-resolves the routeFunc when the search parameters change', () => { - const resolve = jest.fn(() => () => '/hello'); - context.set({ 1: { resolve } }); + const routeRef = createRouteRef({ id: 'ref1' }); - const routeRef = createRouteRef({ id: 'ref1' }); - const history = createBrowserHistory(); - history.push('/my-page'); + const renderedHook = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + ), + }); - const { rerender } = renderHook(() => useRouteRef(routeRef), { - wrapper: ({ children }: PropsWithChildren<{}>) => ( - - ), + const routeFunc = renderedHook.result.current; + expect(routeFunc()).toBe('/hello'); + expect(resolve).toHaveBeenCalledWith( + routeRef, + expect.objectContaining({ + pathname: '/my-page', + }), + ); }); - expect(resolve).toHaveBeenCalledTimes(1); + it('re-resolves the routeFunc when the search parameters change', () => { + const resolve = jest.fn(() => () => '/hello'); + context.set({ 1: { resolve } }); - history.push('/my-new-page'); - rerender(); + const routeRef = createRouteRef({ id: 'ref1' }); + const history = createBrowserHistory(); + history.push('/my-page'); - expect(resolve).toHaveBeenCalledTimes(2); - }); + const { rerender } = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + ), + }); - it('does not re-resolve the routeFunc the location pathname does not change', () => { - const resolve = jest.fn(() => () => '/hello'); - context.set({ 1: { resolve } }); + expect(resolve).toHaveBeenCalledTimes(1); - const routeRef = createRouteRef({ id: 'ref1' }); - const history = createBrowserHistory(); - history.push('/my-page'); + history.push('/my-new-page'); + rerender(); - const { rerender } = renderHook(() => useRouteRef(routeRef), { - wrapper: ({ children }: PropsWithChildren<{}>) => ( - - ), + expect(resolve).toHaveBeenCalledTimes(2); }); - expect(resolve).toHaveBeenCalledTimes(1); + it('does not re-resolve the routeFunc the location pathname does not change', () => { + const resolve = jest.fn(() => () => '/hello'); + context.set({ 1: { resolve } }); - history.push('/my-page'); - rerender(); + const routeRef = createRouteRef({ id: 'ref1' }); + const history = createBrowserHistory(); + history.push('/my-page'); - expect(resolve).toHaveBeenCalledTimes(1); - }); + const { rerender } = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + ), + }); - it('does not re-resolve the routeFunc when the search parameter changes', () => { - const resolve = jest.fn(() => () => '/hello'); - context.set({ 1: { resolve } }); + expect(resolve).toHaveBeenCalledTimes(1); - const routeRef = createRouteRef({ id: 'ref1' }); - const history = createBrowserHistory(); - history.push('/my-page'); + history.push('/my-page'); + rerender(); - const { rerender } = renderHook(() => useRouteRef(routeRef), { - wrapper: ({ children }: PropsWithChildren<{}>) => ( - - ), + expect(resolve).toHaveBeenCalledTimes(1); }); - expect(resolve).toHaveBeenCalledTimes(1); + it('does not re-resolve the routeFunc when the search parameter changes', () => { + const resolve = jest.fn(() => () => '/hello'); + context.set({ 1: { resolve } }); - history.push('/my-page?foo=bar'); - rerender(); + const routeRef = createRouteRef({ id: 'ref1' }); + const history = createBrowserHistory(); + history.push('/my-page'); - expect(resolve).toHaveBeenCalledTimes(1); - }); + const { rerender } = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + ), + }); - it('does not re-resolve the routeFunc when the hash parameter changes', () => { - const resolve = jest.fn(() => () => '/hello'); - context.set({ 1: { resolve } }); + expect(resolve).toHaveBeenCalledTimes(1); - const routeRef = createRouteRef({ id: 'ref1' }); - const history = createBrowserHistory(); - history.push('/my-page'); + history.push('/my-page?foo=bar'); + rerender(); - const { rerender } = renderHook(() => useRouteRef(routeRef), { - wrapper: ({ children }: PropsWithChildren<{}>) => ( - - ), + expect(resolve).toHaveBeenCalledTimes(1); }); - expect(resolve).toHaveBeenCalledTimes(1); + it('does not re-resolve the routeFunc when the hash parameter changes', () => { + const resolve = jest.fn(() => () => '/hello'); + context.set({ 1: { resolve } }); - history.push('/my-page#foo'); - rerender(); + const routeRef = createRouteRef({ id: 'ref1' }); + const history = createBrowserHistory(); + history.push('/my-page'); - expect(resolve).toHaveBeenCalledTimes(1); + const { rerender } = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: PropsWithChildren<{}>) => ( + + ), + }); + + expect(resolve).toHaveBeenCalledTimes(1); + + history.push('/my-page#foo'); + rerender(); + + expect(resolve).toHaveBeenCalledTimes(1); + }); + }); + + describe('new app system', () => { + it('should resolve routes using routeResolutionApi', () => { + const routeRef = createRouteRef({ id: 'ref1' }); + const mockRouteFunc: RouteFunc = jest.fn(() => '/new-route'); + + const mockRouteResolutionApi: RouteResolutionApi = { + resolve: jest.fn(() => mockRouteFunc), + }; + + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ); + + const renderedHook = renderHook(() => useRouteRef(routeRef), { wrapper }); + + const routeFunc = renderedHook.result.current; + expect(routeFunc).toBe(mockRouteFunc); + expect(mockRouteResolutionApi.resolve).toHaveBeenCalledWith( + expect.anything(), + { sourcePath: '/my-page' }, + ); + expect(routeFunc()).toBe('/new-route'); + }); + + it('should handle optional external route refs', () => { + const externalRouteRef = createExternalRouteRef({ + id: 'external-ref', + optional: true, + }); + + const mockRouteResolutionApi: RouteResolutionApi = { + resolve: jest.fn(() => undefined), + }; + + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + + {children} + + ); + + const renderedHook = renderHook(() => useRouteRef(externalRouteRef), { + wrapper, + }); + + expect(renderedHook.result.current).toBeUndefined(); + }); }); }); diff --git a/packages/core-plugin-api/src/routing/useRouteRef.tsx b/packages/core-plugin-api/src/routing/useRouteRef.tsx index cf292b110f..48908ada5d 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.tsx @@ -17,6 +17,11 @@ import { useMemo } from 'react'; import { matchRoutes, useLocation } from 'react-router-dom'; import { useVersionedContext } from '@backstage/version-bridge'; +import { + RouteResolutionApi, + routeResolutionApiRef, + useApi, +} from '@backstage/frontend-plugin-api'; import { AnyParams, ExternalRouteRef, @@ -38,6 +43,14 @@ export interface RouteResolver { ): RouteFunc | undefined; } +function useRouteResolutionApi(): RouteResolutionApi | undefined { + try { + return useApi(routeResolutionApiRef); + } catch { + return undefined; + } +} + /** * React hook for constructing URLs to routes. * @@ -86,30 +99,52 @@ export function useRouteRef( | ExternalRouteRef, ): RouteFunc | undefined { const { pathname } = useLocation(); + const routeResolutionApi = useRouteResolutionApi(); const versionedContext = useVersionedContext<{ 1: RouteResolver }>( 'routing-context', ); - if (!versionedContext) { - throw new Error('Routing context is not available'); - } - const resolver = versionedContext.atVersion(1); - const routeFunc = useMemo( + const resolver = versionedContext?.atVersion(1); + + const newRouteFunc = useMemo(() => { + if (!routeResolutionApi) { + return null; + } + + try { + return routeResolutionApi?.resolve(routeRef, { + sourcePath: pathname, + }); + } catch { + return null; + } + }, [routeResolutionApi, routeRef, pathname]); + + const legacyRouteFunc = useMemo( () => resolver && resolver.resolve(routeRef, { pathname }), [resolver, routeRef, pathname], ); - if (!versionedContext) { - throw new Error('useRouteRef used outside of routing context'); + if (newRouteFunc !== null) { + const isOptional = 'optional' in routeRef && routeRef.optional; + if (!newRouteFunc && !isOptional) { + throw new Error(`No path for ${routeRef}`); + } + return newRouteFunc; } + + if (!versionedContext) { + throw new Error('Routing context is not available'); + } + if (!resolver) { throw new Error('RoutingContext v1 not available'); } const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { + if (!legacyRouteFunc && !isOptional) { throw new Error(`No path for ${routeRef}`); } - return routeFunc; + return legacyRouteFunc; } diff --git a/packages/core-plugin-api/src/translation/index.ts b/packages/core-plugin-api/src/translation/index.ts deleted file mode 100644 index 121ec62c72..0000000000 --- a/packages/core-plugin-api/src/translation/index.ts +++ /dev/null @@ -1,32 +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. - */ - -export { - type TranslationMessages, - type TranslationMessagesOptions, - createTranslationMessages, -} from './TranslationMessages'; -export { - type TranslationResource, - type TranslationResourceOptions, - createTranslationResource, -} from './TranslationResource'; -export { - type TranslationRef, - type TranslationRefOptions, - createTranslationRef, -} from './TranslationRef'; -export { useTranslationRef } from './useTranslationRef'; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 446a3a557e..c5c8a4d403 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,105 @@ # @backstage/create-app +## 0.7.8 + +### Patch Changes + +- fea3e39: Bumped create-app version. +- 9f1ee3e: Bumped create-app version. +- 880310b: Bumped create-app version. +- f1fe6fe: Updated Dockerfile to use Node 24 and Debian Trixie +- Updated dependencies + - @backstage/cli-common@0.1.17 + +## 0.7.8-next.3 + +### Patch Changes + +- Bumped create-app version. + +## 0.7.8-next.2 + +### Patch Changes + +- f1fe6fe: Updated Dockerfile to use Node 24 and Debian Trixie + +## 0.7.8-next.1 + +### Patch Changes + +- Bumped create-app version. + +## 0.7.8-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## 0.7.7 + +### Patch Changes + +- 336db00: Bumped create-app version. +- 2bae83a: Updated engines to support Node 22 or 24 +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16 + +## 0.7.7-next.2 + +### Patch Changes + +- 2bae83a: Updated engines to support Node 22 or 24 +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-common@0.1.16-next.2 + +## 0.7.7-next.1 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## 0.7.7-next.0 + +### Patch Changes + +- c8c2329: Add proxy configuration from env-vars to create-app tasks +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + +## 0.7.6 + +### Patch Changes + +- 2c1fe37: Bumped create-app version. +- 20fae88: Bumped create-app version. +- 9f939a6: Added `@backstage/plugin-app-visualizer` to the app in the `--next` template. +- fc7cbfc: Added the new `@backstage/cli/templates/catalog-provider-module` template to the explicit template configuration for the `next-app` template. + +## 0.7.6-next.3 + +### Patch Changes + +- Bumped create-app version. + +## 0.7.6-next.2 + +### Patch Changes + +- 9f939a6: Added `@backstage/plugin-app-visualizer` to the app in the `--next` template. + +## 0.7.6-next.1 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.6-next.0 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 2f5ac3e944..8b0c2bf836 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.6-next.0", + "version": "0.7.8", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" @@ -58,7 +58,7 @@ "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0", "@types/inquirer": "^8.1.3", - "@types/node": "^20.16.0", + "@types/node": "^22.13.14", "@types/recursive-readdir": "^2.2.0", "msw": "^2.0.0", "nodemon": "^3.0.1" diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 230590e99c..0ab31dd5f4 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { bootstrapEnvProxyAgents } from '@backstage/cli-common'; + +bootstrapEnvProxyAgents(); import chalk from 'chalk'; import fs from 'fs-extra'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 6785fb9b52..51fa8dff84 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -53,6 +53,7 @@ import { version as repoTools } from '../../../repo-tools/package.json'; import { version as ui } from '../../../ui/package.json'; import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.json'; +import { version as pluginAppVisualizer } from '../../../../plugins/app-visualizer/package.json'; import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginAuthBackendModuleGithubProvider } from '../../../../plugins/auth-backend-module-github-provider/package.json'; @@ -117,6 +118,7 @@ export const packageVersions = { '@backstage/repo-tools': repoTools, '@backstage/plugin-api-docs': pluginApiDocs, '@backstage/plugin-app-backend': pluginAppBackend, + '@backstage/plugin-app-visualizer': pluginAppVisualizer, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-auth-backend-module-github-provider': pluginAuthBackendModuleGithubProvider, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 8399c6505a..c1e4a51140 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "engines": { - "node": "20 || 22" + "node": "22 || 24" }, "scripts": { "start": "backstage-cli repo start", @@ -32,6 +32,10 @@ "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@playwright/test": "^1.32.3", + "@jest/environment-jsdom-abstract": "^30.0.0", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jsdom": "^27.1.0", "node-gyp": "^10.0.0", "prettier": "^2.3.2", "typescript": "~5.8.0" diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index ee80245d8b..a70705fba1 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:22-bookworm-slim +FROM node:24-trixie-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 33e76aba3b..55a591067b 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "engines": { - "node": "20 || 22" + "node": "22 || 24" }, "scripts": { "start": "backstage-cli repo start", diff --git a/packages/create-app/templates/next-app/packages/app/package.json.hbs b/packages/create-app/templates/next-app/packages/app/package.json.hbs index bc02fdc21a..726983fd69 100644 --- a/packages/create-app/templates/next-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/next-app/packages/app/package.json.hbs @@ -21,6 +21,7 @@ "@backstage/frontend-defaults": "^{{ version '@backstage/frontend-defaults'}}", "@backstage/frontend-plugin-api": "^{{ version '@backstage/frontend-plugin-api'}}", "@backstage/integration-react": "^{{ version '@backstage/integration-react'}}", + "@backstage/plugin-app-visualizer": "^{{ version '@backstage/plugin-app-visualizer'}}", "@backstage/plugin-catalog": "^{{ version '@backstage/plugin-catalog'}}", "@backstage/plugin-notifications": "^{{ version '@backstage/plugin-notifications'}}", "@backstage/plugin-org": "^{{ version '@backstage/plugin-org'}}", diff --git a/packages/create-app/templates/next-app/packages/backend/Dockerfile b/packages/create-app/templates/next-app/packages/backend/Dockerfile index ee80245d8b..a70705fba1 100644 --- a/packages/create-app/templates/next-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/next-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:22-bookworm-slim +FROM node:24-trixie-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index f9847fed5c..a0d5776c2c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,109 @@ # @backstage/dev-utils +## 1.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/core-components@0.18.5 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-catalog-react@1.21.5 + +## 1.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + +## 1.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.0 + +## 1.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/theme@0.7.1 + - @backstage/plugin-catalog-react@1.21.4 + +## 1.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/ui@0.10.0 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/app-defaults@1.7.3 + - @backstage/integration-react@1.2.13 + +## 1.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + +## 1.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/catalog-model@1.7.6 + +## 1.1.17 + +### Patch Changes + +- b29a856: Fixed styling of the dev app by adding a lazy import of `@backstage/ui/css/styles.css`. +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/catalog-model@1.7.6 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + +## 1.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + ## 1.1.17-next.0 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index cb20f0cd8e..51dbf30279 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.17-next.0", + "version": "1.1.19", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index a10481c28d..3c71781e80 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,46 @@ # e2e-test +## 0.2.36 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.17 + - @backstage/create-app@0.7.8 + +## 0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.8-next.0 + - @backstage/cli-common@0.1.16 + - @backstage/errors@1.2.7 + +## 0.2.35 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.7 + - @backstage/cli-common@0.1.16 + +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.16-next.0 + - @backstage/create-app@0.7.7-next.0 + - @backstage/errors@1.2.7 + +## 0.2.34 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.6 + ## 0.2.34-next.0 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 116c219359..ecebb7f929 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,6 +1,6 @@ { "name": "e2e-test", - "version": "0.2.34-next.0", + "version": "0.2.36", "description": "E2E test for verifying Backstage packages", "backstage": { "role": "cli" @@ -47,7 +47,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/fs-extra": "^11.0.0", - "@types/node": "^20.16.0", + "@types/node": "^22.13.14", "nodemon": "^3.0.1" } } diff --git a/packages/e2e-test/src/commands/index.ts b/packages/e2e-test/src/commands/index.ts index fd0ba59c30..cbcb9f3907 100644 --- a/packages/e2e-test/src/commands/index.ts +++ b/packages/e2e-test/src/commands/index.ts @@ -15,12 +15,12 @@ */ import { Command } from 'commander'; -import { run } from './run'; +import { runCommand } from './runCommand'; export function registerCommands(program: Command) { program .command('run') .option('--keep', 'Do not remove the temporary dir after tests complete') .description('Run e2e tests') - .action(run); + .action(runCommand); } diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/runCommand.ts similarity index 94% rename from packages/e2e-test/src/commands/run.ts rename to packages/e2e-test/src/commands/runCommand.ts index 6df58349df..112937dbe7 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -22,18 +22,12 @@ import killTree from 'tree-kill'; import { resolve as resolvePath, join as joinPath } from 'path'; import path from 'path'; -import { - spawnPiped, - runPlain, - waitFor, - waitForExit, - print, -} from '../lib/helpers'; +import { waitFor, print } from '../lib/helpers'; import mysql from 'mysql2/promise'; import pgtools from 'pgtools'; -import { findPaths } from '@backstage/cli-common'; +import { findPaths, runOutput, run } from '@backstage/cli-common'; import { OptionValues } from 'commander'; // eslint-disable-next-line no-restricted-syntax @@ -46,7 +40,7 @@ const templatePackagePaths = [ 'packages/create-app/templates/default-app/packages/backend/package.json.hbs', ]; -export async function run(opts: OptionValues) { +export async function runCommand(opts: OptionValues) { const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); print(`CLI E2E test root: ${rootDir}\n`); @@ -67,7 +61,7 @@ export async function run(opts: OptionValues) { await createPlugin({ appDir, pluginId, select: 'backend-plugin' }); print(`Running 'yarn test:e2e' in newly created app with new plugin`); - await runPlain(['yarn', 'test:e2e'], { + await runOutput(['yarn', 'test:e2e'], { cwd: appDir, env: { ...process.env, CI: undefined }, }); @@ -75,13 +69,13 @@ export async function run(opts: OptionValues) { await switchToReact17(appDir); print(`Running 'yarn install' to install React 17`); - await runPlain(['yarn', 'install'], { cwd: appDir }); + await runOutput(['yarn', 'install'], { cwd: appDir }); print(`Running 'yarn tsc' with React 17`); - await runPlain(['yarn', 'tsc'], { cwd: appDir }); + await runOutput(['yarn', 'tsc'], { cwd: appDir }); print(`Running 'yarn test:e2e' with React 17`); - await runPlain(['yarn', 'test:e2e'], { + await runOutput(['yarn', 'test:e2e'], { cwd: appDir, env: { ...process.env, CI: undefined }, }); @@ -190,7 +184,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { appendDeps(require('@backstage/create-app/package.json')); print(`Preparing workspace`); - await runPlain([ + await runOutput([ 'yarn', 'backstage-cli', 'build-workspace', @@ -209,7 +203,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { } print('Installing workspace dependencies'); - await runPlain(['yarn', 'workspaces', 'focus', '--all', '--production'], { + await runOutput(['yarn', 'workspaces', 'focus', '--all', '--production'], { cwd: workspaceDir, }); @@ -257,7 +251,7 @@ async function createApp( workspaceDir: string, rootDir: string, ) { - const child = spawnPiped( + const child = run( [ 'node', resolvePath(workspaceDir, 'packages/create-app/bin/backstage-create-app'), @@ -265,6 +259,7 @@ async function createApp( ], { cwd: rootDir, + stdio: ['pipe', 'pipe', 'pipe'], }, ); @@ -278,7 +273,7 @@ async function createApp( child.stdin?.write(`${appName}\n`); print('Waiting for app create script to be done'); - await waitForExit(child); + await child.waitForExit(); const appDir = resolvePath(rootDir, appName); @@ -318,7 +313,7 @@ async function createApp( 'test:all', ]) { print(`Running 'yarn ${cmd}' in newly created app`); - await runPlain(['yarn', cmd], { cwd: appDir }); + await runOutput(['yarn', cmd], { cwd: appDir }); } return appDir; @@ -378,10 +373,11 @@ async function createPlugin(options: { select: string; }) { const { appDir, pluginId, select } = options; - const child = spawnPiped( + const child = run( ['yarn', 'new', '--select', select, '--option', `pluginId=${pluginId}`], { cwd: appDir, + stdio: ['pipe', 'pipe', 'pipe'], }, ); @@ -392,7 +388,7 @@ async function createPlugin(options: { }); print('Waiting for plugin create script to be done'); - await waitForExit(child); + await child.waitForExit(); const pluginDir = resolvePath( appDir, @@ -401,11 +397,11 @@ async function createPlugin(options: { ); print(`Running 'yarn tsc' in root for newly created plugin`); - await runPlain(['yarn', 'tsc'], { cwd: appDir }); + await runOutput(['yarn', 'tsc'], { cwd: appDir }); for (const cmd of [['lint'], ['test', '--no-watch']]) { print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); - await runPlain(['yarn', ...cmd], { cwd: pluginDir }); + await runOutput(['yarn', ...cmd], { cwd: pluginDir }); } } finally { child.kill(); @@ -494,7 +490,7 @@ async function dropClientDatabases(client: string) { * Start serving the newly created backend and make sure that all db migrations works correctly */ async function testBackendStart(appDir: string, ...args: string[]) { - const child = spawnPiped(['yarn', 'workspace', 'backend', 'start', ...args], { + const child = run(['yarn', 'workspace', 'backend', 'start', ...args], { cwd: appDir, // Windows does not like piping stdin here, the child process will hang when requiring the 'process' module stdio: ['ignore', 'pipe', 'pipe'], @@ -586,7 +582,7 @@ async function testBackendStart(appDir: string, ...args: string[]) { } try { - await waitForExit(child); + await child.waitForExit(); } catch (error) { if (!successful) { throw new Error(`Backend failed to startup: ${stderr}`); diff --git a/packages/e2e-test/src/index.ts b/packages/e2e-test/src/index.ts index f3c63ae175..df299628b2 100644 --- a/packages/e2e-test/src/index.ts +++ b/packages/e2e-test/src/index.ts @@ -18,7 +18,6 @@ import { program } from 'commander'; import chalk from 'chalk'; import { registerCommands } from './commands'; import { version } from '../package.json'; -import { exitWithError } from './lib/helpers'; async function main(argv: string[]) { program.name('e2e-test').version(version); @@ -36,4 +35,12 @@ async function main(argv: string[]) { program.parse(argv); } -main(process.argv).catch(exitWithError); +main(process.argv).catch(err => { + process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); + + if (typeof err.code === 'number') { + process.exit(err.code); + } else { + process.exit(1); + } +}); diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index fc5aefcff9..d310812794 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -14,77 +14,6 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; -import { - spawn, - execFile as execFileCb, - SpawnOptions, - ChildProcess, -} from 'child_process'; -import { promisify } from 'util'; - -const execFile = promisify(execFileCb); - -export function spawnPiped(cmd: string[], options?: SpawnOptions) { - function pipeWithPrefix(stream: NodeJS.WriteStream, prefix = '') { - return (data: Buffer) => { - const prefixedMsg = data - .toString('utf8') - .trimEnd() - .replace(/^/gm, prefix); - stream.write(`${prefixedMsg}\n`, 'utf8'); - }; - } - - const child = spawn(cmd[0], cmd.slice(1), { - stdio: 'pipe', - shell: true, - ...options, - }); - child.on('error', exitWithError); - - const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' '); - child.stdout?.on( - 'data', - pipeWithPrefix(process.stdout, `[${logPrefix}].out: `), - ); - child.stderr?.on( - 'data', - pipeWithPrefix(process.stderr, `[${logPrefix}].err: `), - ); - - return child; -} - -export async function runPlain(cmd: string[], options?: SpawnOptions) { - try { - const { stdout } = await execFile(cmd[0], cmd.slice(1), { - ...options, - shell: true, - }); - return stdout.trim(); - } catch (error) { - assertError(error); - if (error.stdout) { - process.stdout.write(error.stdout as Buffer); - } - if (error.stderr) { - process.stderr.write(error.stderr as Buffer); - } - throw error; - } -} - -export function exitWithError(err: Error & { code?: unknown }) { - process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); - - if (typeof err.code === 'number') { - process.exit(err.code); - } else { - process.exit(1); - } -} - /** * Waits for fn() to be true * Checks every 100ms @@ -108,22 +37,6 @@ export function waitFor(fn: () => boolean, maxSeconds: number = 120) { }); } -export async function waitForExit(child: ChildProcess) { - if (child.exitCode !== null) { - throw new Error(`Child already exited with code ${child.exitCode}`); - } - await new Promise((resolve, reject) => - child.once('exit', code => { - if (code) { - reject(new Error(`Child exited with code ${code}`)); - } else { - print('Child finished'); - resolve(); - } - }), - ); -} - export function print(msg: string) { return process.stdout.write(`${msg}\n`); } diff --git a/packages/errors/src/errors/common.test.ts b/packages/errors/src/errors/common.test.ts index dfa9743616..4c496614ab 100644 --- a/packages/errors/src/errors/common.test.ts +++ b/packages/errors/src/errors/common.test.ts @@ -23,7 +23,7 @@ describe('common', () => { const error = new E('abcdef'); expect(error.name).toBe(name); expect(error.message).toBe('abcdef'); - expect(error.stack).toContain(__filename); + expect(error.stack).toContain('common.test.ts'); expect(error.toString()).toContain(name); expect(error.toString()).toContain('abcdef'); } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 65acedd942..a823de9ee5 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/eslint-plugin +## 0.2.0 + +### Minor Changes + +- 926389b: Added `@backstage/no-ui-css-imports-in-non-frontend` rule, which ensures that CSS from `@backstage/ui` is not imported outside of the frontend app. + ## 0.2.0-next.0 ### Minor Changes diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index f7dc9eebcc..13db908e16 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/eslint-plugin", - "version": "0.2.0-next.0", + "version": "0.2.0", "description": "Backstage ESLint plugin", "publishConfig": { "access": "public" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 38ec33c33a..7e58084dd1 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/frontend-app-api +## 0.14.0 + +### Minor Changes + +- 3bd2a1a: BREAKING: The ability for plugins to override APIs has been restricted to only allow overrides of APIs within the same plugin. For example, a plugin can no longer override any of the core APIs provided by the `app` plugin, this must be done with an `app` module instead. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/frontend-defaults@0.3.5 + +## 0.13.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-defaults@0.3.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.13.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-defaults@0.3.4 + +## 0.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/frontend-defaults@0.3.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.13.2 + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/frontend-defaults@0.3.3 + ## 0.13.2-next.0 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 9bdf329f0f..3fa96d606a 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.13.2-next.0", + "version": "0.14.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index bd0709813b..06b5e55278 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -109,6 +109,14 @@ export type AppErrorTypes = { node: AppNode; }; }; + API_FACTORY_CONFLICT: { + context: { + node: AppNode; + apiRefId: string; + pluginId: string; + existingPluginId: string; + }; + }; ROUTE_DUPLICATE: { context: { routeId: string; diff --git a/packages/frontend-app-api/src/routing/RouteAliasResolver.ts b/packages/frontend-app-api/src/routing/RouteAliasResolver.ts index 64e5eb7ce8..badf27f964 100644 --- a/packages/frontend-app-api/src/routing/RouteAliasResolver.ts +++ b/packages/frontend-app-api/src/routing/RouteAliasResolver.ts @@ -16,8 +16,7 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { RouteRefsById } from './collectRouteIds'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef'; +import { OpaqueRouteRef } from '@internal/frontend'; /** * @internal @@ -41,7 +40,7 @@ export function createRouteAliasResolver( let currentRef = routeRef; for (let i = 0; i < 100; i++) { - const alias = toInternalRouteRef(currentRef).alias; + const alias = OpaqueRouteRef.toInternal(currentRef).alias; if (alias) { if (pluginId) { const [aliasPluginId] = alias.split('.'); diff --git a/packages/frontend-app-api/src/routing/RouteResolver.test.ts b/packages/frontend-app-api/src/routing/RouteResolver.test.ts index 1b23ac7131..7b9105f2ae 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.test.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.test.ts @@ -22,7 +22,12 @@ import { RouteRef, SubRouteRef, } from '@backstage/frontend-plugin-api'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { + BackstagePlugin, + createRouteRef as createLegacyRouteRef, + createSubRouteRef as createLegacySubRouteRef, + createExternalRouteRef as createLegacyExternalRouteRef, +} from '@backstage/core-plugin-api'; import { RouteResolver } from './RouteResolver'; import { MATCH_ALL_ROUTE } from './extractRouteInfoFromAppNode'; import { @@ -416,4 +421,196 @@ describe('RouteResolver', () => { '/my-parent/a%2F%23%26%3Fb', ); }); + + describe('with legacy route refs', () => { + const legacyRef1 = createLegacyRouteRef({ id: 'ref1' }); + const legacyRef2 = createLegacyRouteRef({ id: 'ref2', params: ['x'] }); + const legacyRef3 = createLegacyRouteRef({ id: 'ref3', params: ['y'] }); + const legacySubRef1 = createLegacySubRouteRef({ + id: 'sub1', + parent: legacyRef1, + path: '/foo', + }); + const legacySubRef2 = createLegacySubRouteRef({ + id: 'sub2', + parent: legacyRef1, + path: '/foo/:a', + }); + const legacySubRef3 = createLegacySubRouteRef({ + id: 'sub3', + parent: legacyRef2, + path: '/bar', + }); + const legacySubRef4 = createLegacySubRouteRef({ + id: 'sub4', + parent: legacyRef2, + path: '/bar/:a', + }); + const legacyExternalRef1 = createLegacyExternalRouteRef({ + id: 'external1', + }); + const legacyExternalRef2 = createLegacyExternalRouteRef({ + id: 'external2', + params: ['x'], + }); + + it('should not resolve anything with an empty resolver', () => { + const r = new RouteResolver( + new Map(), + new Map(), + [], + new Map(), + '', + emptyResolver, + new Map(), + ); + + expect(r.resolve(legacyRef1, src('/'))?.()).toBe(undefined); + expect(r.resolve(legacyRef2, src('/'))?.({ x: '1x' })).toBe(undefined); + expect(r.resolve(legacySubRef1, src('/'))?.()).toBe(undefined); + expect(r.resolve(legacySubRef2, src('/'))?.({ a: '2a' })).toBe(undefined); + expect(r.resolve(legacySubRef3, src('/'))?.({ x: '3x' })).toBe(undefined); + expect(r.resolve(legacySubRef4, src('/'))?.({ x: '4x', a: '4a' })).toBe( + undefined, + ); + expect(r.resolve(legacyExternalRef1, src('/'))?.()).toBe(undefined); + expect(r.resolve(legacyExternalRef2, src('/'))?.({ x: '5x' })).toBe( + undefined, + ); + }); + + it('should resolve an absolute route', () => { + const r = new RouteResolver( + new Map([[legacyRef1, 'my-route']]), + new Map(), + [{ routeRefs: new Set([legacyRef1]), path: 'my-route', ...rest }], + new Map(), + '', + emptyResolver, + new Map(), + ); + + expect(r.resolve(legacyRef1, src('/'))?.()).toBe('/my-route'); + expect(r.resolve(legacyRef2, src('/'))?.({ x: '1x' })).toBe(undefined); + expect(r.resolve(legacySubRef1, src('/'))?.()).toBe('/my-route/foo'); + expect(r.resolve(legacySubRef2, src('/'))?.({ a: '2a' })).toBe( + '/my-route/foo/2a', + ); + expect(r.resolve(legacySubRef3, src('/'))?.({ x: '3x' })).toBe(undefined); + expect(r.resolve(legacySubRef4, src('/'))?.({ x: '4x', a: '4a' })).toBe( + undefined, + ); + expect(r.resolve(legacyExternalRef1, src('/'))?.()).toBe(undefined); + expect(r.resolve(legacyExternalRef2, src('/'))?.({ x: '5x' })).toBe( + undefined, + ); + }); + + it('should resolve an absolute route with a param and with a parent', () => { + const r = new RouteResolver( + new Map([ + [legacyRef1, 'my-route'], + [legacyRef2, 'my-parent/:x'], + ]), + new Map([[legacyRef2, legacyRef1]]), + [ + { + routeRefs: new Set([legacyRef2]), + path: 'my-parent/:x', + ...rest, + children: [ + MATCH_ALL_ROUTE, + { routeRefs: new Set([legacyRef1]), path: 'my-route', ...rest }, + ], + }, + ], + new Map([ + [legacyExternalRef1, legacyRef1], + [legacyExternalRef2, legacySubRef3], + ]), + '', + emptyResolver, + new Map(), + ); + + expect(r.resolve(legacyRef1, src('/'))?.()).toBe('/my-route'); + expect(r.resolve(legacyRef2, src('/'))?.({ x: '1x' })).toBe( + '/my-route/my-parent/1x', + ); + expect(r.resolve(legacySubRef1, src('/'))?.()).toBe('/my-route/foo'); + expect(r.resolve(legacySubRef2, src('/'))?.({ a: '2a' })).toBe( + '/my-route/foo/2a', + ); + expect(r.resolve(legacySubRef3, src('/'))?.({ x: '3x' })).toBe( + '/my-route/my-parent/3x/bar', + ); + expect(r.resolve(legacySubRef4, src('/'))?.({ x: '4x', a: '4a' })).toBe( + '/my-route/my-parent/4x/bar/4a', + ); + expect(r.resolve(legacyExternalRef1, src('/'))?.()).toBe('/my-route'); + expect(r.resolve(legacyExternalRef2, src('/'))?.({ x: '6x' })).toBe( + '/my-route/my-parent/6x/bar', + ); + }); + + it('should resolve the most specific match', () => { + const r = new RouteResolver( + new Map([ + [legacyRef1, 'deep'], + [legacyRef2, 'root/:x'], + [legacyRef3, 'sub/:y'], + ]), + new Map([ + [legacyRef3, legacyRef2], + [legacyRef1, legacyRef3], + ]), + [ + { + routeRefs: new Set([legacyRef2]), + path: 'root/:x', + ...rest, + children: [ + MATCH_ALL_ROUTE, + { + routeRefs: new Set([legacyRef3]), + path: 'sub/:y', + ...rest, + children: [ + MATCH_ALL_ROUTE, + { + routeRefs: new Set([legacyRef1]), + path: 'deep', + ...rest, + }, + ], + }, + ], + }, + ], + new Map(), + '', + emptyResolver, + new Map(), + ); + + expect(r.resolve(legacyRef2, src('/'))?.({ x: 'x' })).toBe('/root/x'); + expect(r.resolve(legacyRef3, src('/root/x'))?.({ y: 'y' })).toBe( + '/root/x/sub/y', + ); + + expect(() => r.resolve(legacyRef1, src('/'))?.()).toThrow( + /^Cannot route.*with parent.*as it has parameters$/, + ); + expect(() => r.resolve(legacyRef1, src('/root/x'))?.()).toThrow( + /^Cannot route.*with parent.*as it has parameters$/, + ); + expect(r.resolve(legacyRef1, src('/root/x/sub/y'))?.()).toBe( + '/root/x/sub/y/deep', + ); + // Without the MATCH_ALL_ROUTE, we wouldn't properly match the route here + expect( + r.resolve(legacyRef1, src('/root/x/sub/y/any/nested/path/here'))?.(), + ).toBe('/root/x/sub/y/deep'); + }); + }); }); diff --git a/packages/frontend-app-api/src/routing/RouteResolver.ts b/packages/frontend-app-api/src/routing/RouteResolver.ts index 4c0ad0e3fa..7fd7ef934c 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.ts @@ -25,18 +25,11 @@ import { } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; import { AnyRouteRef, BackstageRouteObject } from './types'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { isRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { - isSubRouteRef, - toInternalSubRouteRef, -} from '../../../frontend-plugin-api/src/routing/SubRouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - isExternalRouteRef, - toInternalExternalRouteRef, -} from '../../../frontend-plugin-api/src/routing/ExternalRouteRef'; + OpaqueRouteRef, + OpaqueExternalRouteRef, + OpaqueSubRouteRef, +} from '@internal/frontend'; import { RouteAliasResolver } from './RouteAliasResolver'; // Joins a list of paths together, avoiding trailing and duplicate slashes @@ -65,10 +58,10 @@ function resolveTargetRef( let ref: AnyRouteRef = targetRouteRef; let path = ''; - if (isExternalRouteRef(ref)) { + if (OpaqueExternalRouteRef.isType(ref)) { let resolvedRoute = routeBindings.get(ref); if (!resolvedRoute) { - const internal = toInternalExternalRouteRef(ref); + const internal = OpaqueExternalRouteRef.toInternal(ref); const defaultTarget = internal.getDefaultTarget(); if (defaultTarget) { resolvedRoute = routeRefsById.get(defaultTarget); @@ -80,13 +73,13 @@ function resolveTargetRef( ref = resolvedRoute; } - if (isSubRouteRef(ref)) { - const internal = toInternalSubRouteRef(ref); + if (OpaqueSubRouteRef.isType(ref)) { + const internal = OpaqueSubRouteRef.toInternal(ref); path = ref.path; ref = internal.getParent(); } - if (!isRouteRef(ref)) { + if (!OpaqueRouteRef.isType(ref)) { throw new Error( `Unexpectedly resolved ${targetRouteRef} to a non-route ref ${ref}`, ); diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts index b1bab3fbbe..9575603bf4 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts @@ -39,10 +39,10 @@ describe('collectRouteIds', () => { const extRef = createExternalRouteRef(); expect(String(ref)).toMatch( - /^RouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/, + /^routeRef\{id=undefined,at='.*collectRouteIds\.test\.ts.*'\}$/, ); expect(String(extRef)).toMatch( - /^ExternalRouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/, + /^externalRouteRef\{id=undefined,at='.*collectRouteIds\.test\.ts.*'\}$/, ); const collected = collectRouteIds( @@ -62,8 +62,12 @@ describe('collectRouteIds', () => { 'test.extRef': extRef, }); - expect(String(ref)).toBe('RouteRef{test.ref}'); - expect(String(extRef)).toBe('ExternalRouteRef{test.extRef}'); + expect(String(ref)).toMatch( + /^routeRef\{id=test.ref,at='.*collectRouteIds\.test\.ts.*'\}$/, + ); + expect(String(extRef)).toMatch( + /^externalRouteRef\{id=test.extRef,at='.*collectRouteIds\.test\.ts.*'\}$/, + ); }); it('should report duplicate route IDs', () => { diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.ts b/packages/frontend-app-api/src/routing/collectRouteIds.ts index 79afb07928..c99c28ac1a 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.ts @@ -20,16 +20,12 @@ import { ExternalRouteRef, FrontendFeature, } from '@backstage/frontend-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { - isRouteRef, - toInternalRouteRef, -} from '../../../frontend-plugin-api/src/routing/RouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef'; -import { OpaqueFrontendPlugin } from '@internal/frontend'; + OpaqueRouteRef, + OpaqueSubRouteRef, + OpaqueExternalRouteRef, + OpaqueFrontendPlugin, +} from '@internal/frontend'; import { ErrorCollector } from '../wiring/createErrorCollector'; /** @internal */ @@ -62,12 +58,12 @@ export function collectRouteIds( continue; } - if (isRouteRef(ref)) { - const internalRef = toInternalRouteRef(ref); + if (OpaqueRouteRef.isType(ref)) { + const internalRef = OpaqueRouteRef.toInternal(ref); internalRef.setId(refId); routesById.set(refId, ref); } else { - const internalRef = toInternalSubRouteRef(ref); + const internalRef = OpaqueSubRouteRef.toInternal(ref); routesById.set(refId, internalRef); } } @@ -82,7 +78,7 @@ export function collectRouteIds( continue; } - const internalRef = toInternalExternalRouteRef(ref); + const internalRef = OpaqueExternalRouteRef.toInternal(ref); internalRef.setId(refId); externalRoutesById.set(refId, ref); } diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index efcc9ef18c..8375cf6d3a 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -636,7 +636,7 @@ describe('discovery', () => { }, ), ).toThrow( - /Refused to resolve alias 'other.root' for RouteRef{created at 'at .*extractRouteInfoFromAppNode\.test\.ts:\d+:\d+'} as it points to a different plugin, the expected plugin is 'test' but the alias points to 'other'/, + /Refused to resolve alias 'other.root' for routeRef{id=undefined,at='.*extractRouteInfoFromAppNode\.test\.ts:\d+:\d+'} as it points to a different plugin, the expected plugin is 'test' but the alias points to 'other'/, ); }); @@ -662,7 +662,7 @@ describe('discovery', () => { }, ), ).toThrow( - /Alias loop detected for RouteRef{created at 'at .*extractRouteInfoFromAppNode\.test\.ts:\d+:\d+'}/, + /Alias loop detected for routeRef{id=undefined,at='.*extractRouteInfoFromAppNode\.test\.ts:\d+:\d+'}/, ); }); }); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index f9836d7107..44fe4ecbf5 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -23,8 +23,7 @@ import { RouteRefsById } from './collectRouteIds'; import { ErrorCollector } from '../wiring/createErrorCollector'; import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef'; +import { OpaqueExternalRouteRef } from '@internal/frontend'; /** * Extracts a union of the keys in a map whose value extends the given type @@ -166,7 +165,7 @@ export function resolveRouteBindings( for (const externalRef of routesById.externalRoutes.values()) { if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) { const defaultRefId = - toInternalExternalRouteRef(externalRef).getDefaultTarget(); + OpaqueExternalRouteRef.toInternal(externalRef).getDefaultTarget(); if (defaultRefId) { const defaultRef = routesById.routes.get(defaultRefId); if (defaultRef) { diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 13ec6e8a59..a07fdf352d 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -787,14 +787,8 @@ describe('instantiateAppNodeTree', () => { inputs: { [name in string]: | undefined - | ResolvedExtensionInput< - ExtensionInput - > - | Array< - ResolvedExtensionInput< - ExtensionInput - > - >; + | ResolvedExtensionInput + | Array>; }; }) { return [ diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 39fc88c19a..04082f8520 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -243,20 +243,10 @@ function resolveV1Inputs( } function resolveV2Inputs( - inputMap: { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + inputMap: { [inputName in string]: ExtensionInput }, attachments: ReadonlyMap, parentCollector: ErrorCollector<{ node: AppNode }>, -): ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; -}> { +): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }> { return mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; const collector = parentCollector.child({ inputName }); @@ -297,12 +287,7 @@ function resolveV2Inputs( collector, ), ); - }) as ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }>; + }) as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }>; } /** @internal */ diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 2eacd0ae67..3ccdf5258f 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -44,6 +44,7 @@ function makeExt( version: 'v1', id, attachTo: { id: attachId, input: 'default' }, + inputs: {}, disabled: status === 'disabled', toString: expect.any(Function), } as Extension; @@ -61,6 +62,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + inputs: {}, override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index c20e92d0f0..dab69ba19e 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -66,6 +66,14 @@ export type AppErrorTypes = { API_EXTENSION_INVALID: { context: { node: AppNode }; }; + API_FACTORY_CONFLICT: { + context: { + node: AppNode; + apiRefId: string; + pluginId: string; + existingPluginId: string; + }; + }; // routing ROUTE_DUPLICATE: { context: { routeId: string }; diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index 5622af0448..6c79002ad9 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -20,7 +20,9 @@ import { coreExtensionData, createExtension, createFrontendPlugin, + createFrontendModule, ApiBlueprint, + createApiRef, createRouteRef, createExternalRouteRef, createExtensionInput, @@ -36,21 +38,22 @@ import { MemoryRouter } from 'react-router-dom'; import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { Fragment } from 'react'; +function makeAppPlugin(label: string = 'Test') { + return createFrontendPlugin({ + pluginId: 'app', + extensions: [ + createExtension({ + attachTo: { id: 'root', input: 'app' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(

    {label}
    )], + }), + ], + }); +} describe('createSpecializedApp', () => { it('should render the root app', () => { const app = createSpecializedApp({ - features: [ - createFrontendPlugin({ - pluginId: 'test', - extensions: [ - createExtension({ - attachTo: { id: 'root', input: 'app' }, - output: [coreExtensionData.reactElement], - factory: () => [coreExtensionData.reactElement(
    Test
    )], - }), - ], - }), - ], + features: [makeAppPlugin()], }); render(app.tree.root.instance!.getData(coreExtensionData.reactElement)); @@ -60,32 +63,7 @@ describe('createSpecializedApp', () => { it('should deduplicate features keeping the last received one', () => { const app = createSpecializedApp({ - features: [ - createFrontendPlugin({ - pluginId: 'test', - extensions: [ - createExtension({ - attachTo: { id: 'root', input: 'app' }, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
    Test 1
    ), - ], - }), - ], - }), - createFrontendPlugin({ - pluginId: 'test', - extensions: [ - createExtension({ - attachTo: { id: 'root', input: 'app' }, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
    Test 2
    ), - ], - }), - ], - }), - ], + features: [makeAppPlugin('Test 1'), makeAppPlugin('Test 2')], }); render(app.tree.root.instance!.getData(coreExtensionData.reactElement)); @@ -249,8 +227,9 @@ describe('createSpecializedApp', () => { const app = createSpecializedApp({ features: [ - createFrontendPlugin({ - pluginId: 'first', + makeAppPlugin(), + createFrontendModule({ + pluginId: 'app', extensions: [ ApiBlueprint.make({ params: defineParams => @@ -264,9 +243,8 @@ describe('createSpecializedApp', () => { }), ], }), - createFrontendPlugin({ - pluginId: 'test', - featureFlags: [{ name: 'a' }, { name: 'b' }], + createFrontendModule({ + pluginId: 'app', extensions: [ createExtension({ attachTo: { id: 'root', input: 'app' }, @@ -311,6 +289,107 @@ describe('createSpecializedApp', () => { expect(mockAnalyticsApi).toHaveBeenCalled(); }); + it('should select the API factory from the owning plugin on conflict', () => { + const testApiRef = createApiRef<{ value: string }>({ id: 'test.api' }); + + const app = createSpecializedApp({ + features: [ + makeAppPlugin(), + createFrontendPlugin({ + pluginId: 'other-before', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'other' }), + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'owner' }), + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'other-after', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'other' }), + }), + }), + ], + }), + ], + }); + + expect(app.errors).toEqual([ + expect.objectContaining({ + code: 'API_FACTORY_CONFLICT', + message: expect.stringContaining("API 'test.api'"), + }), + expect.objectContaining({ + code: 'API_FACTORY_CONFLICT', + message: expect.stringContaining("API 'test.api'"), + }), + ]); + + expect(app.apis.get(testApiRef)).toEqual({ value: 'owner' }); + }); + + it('should allow API overrides within the same plugin', () => { + const testApiRef = createApiRef<{ value: string }>({ id: 'test.api' }); + + const app = createSpecializedApp({ + features: [ + makeAppPlugin(), + createFrontendPlugin({ + pluginId: 'test', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'plugin' }), + }), + }), + ], + }), + createFrontendModule({ + pluginId: 'test', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'module' }), + }), + }), + ], + }), + ], + }); + + expect(app.errors).toBeUndefined(); + expect(app.apis.get(testApiRef)).toEqual({ value: 'module' }); + }); + it('should use provided apis', async () => { const app = createSpecializedApp({ advanced: { diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 9e75d19454..f902500e9f 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -388,7 +388,10 @@ function createApiFactories(options: { collector: ErrorCollector; }): AnyApiFactory[] { const emptyApiHolder = ApiRegistry.from([]); - const factories = new Array(); + const factoriesById = new Map< + string, + { pluginId: string; factory: AnyApiFactory } + >(); for (const apiNode of options.tree.root.edges.attachments.get('apis') ?? []) { if (!instantiateAppNodeTree(apiNode, emptyApiHolder, options.collector)) { @@ -396,7 +399,45 @@ function createApiFactories(options: { } const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); if (apiFactory) { - factories.push(apiFactory); + const apiRefId = apiFactory.api.id; + const ownerId = getApiOwnerId(apiRefId); + const pluginId = apiNode.spec.plugin.id ?? 'app'; + const existingFactory = factoriesById.get(apiRefId); + + // This allows modules to override factories provided by the plugin, but + // it rejects API overrides from other plugins. In the event of a + // conflict, the owning plugin is attempted to be inferred from the API + // reference ID. + if (existingFactory && existingFactory.pluginId !== pluginId) { + const shouldReplace = + ownerId === pluginId && existingFactory.pluginId !== ownerId; + const acceptedPluginId = shouldReplace + ? pluginId + : existingFactory.pluginId; + const rejectedPluginId = shouldReplace + ? existingFactory.pluginId + : pluginId; + + options.collector.report({ + code: 'API_FACTORY_CONFLICT', + message: `API '${apiRefId}' is already provided by plugin '${acceptedPluginId}', cannot also be provided by '${rejectedPluginId}'.`, + context: { + node: apiNode, + apiRefId, + pluginId: rejectedPluginId, + existingPluginId: acceptedPluginId, + }, + }); + if (shouldReplace) { + factoriesById.set(apiRefId, { + pluginId, + factory: apiFactory, + }); + } + continue; + } + + factoriesById.set(apiRefId, { pluginId, factory: apiFactory }); } else { options.collector.report({ code: 'API_EXTENSION_INVALID', @@ -408,7 +449,23 @@ function createApiFactories(options: { } } - return factories; + return Array.from(factoriesById.values(), entry => entry.factory); +} + +// TODO(Rugvip): It would be good if this was more explicit, but I think that +// might need to wait for some future update for API factories. +function getApiOwnerId(apiRefId: string): string { + const [prefix, ...rest] = apiRefId.split('.'); + if (!prefix) { + return apiRefId; + } + if (prefix === 'core') { + return 'app'; + } + if (prefix === 'plugin' && rest[0]) { + return rest[0]; + } + return prefix; } function createApiHolder(options: { diff --git a/packages/frontend-defaults/CHANGELOG.md b/packages/frontend-defaults/CHANGELOG.md index 651bdeb691..284dd0ff3b 100644 --- a/packages/frontend-defaults/CHANGELOG.md +++ b/packages/frontend-defaults/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/frontend-defaults +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-app@0.3.4 + - @backstage/frontend-app-api@0.14.0 + +## 0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-app@0.3.4-next.1 + +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/frontend-app-api@0.13.3 + +## 0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/core-components@0.18.3 + - @backstage/config@1.3.6 + - @backstage/plugin-app@0.3.2 + ## 0.3.3-next.0 ### Patch Changes diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index 3fcbb2c990..c47987083d 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-defaults", - "version": "0.3.3-next.0", + "version": "0.3.5", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 7b5e345e12..c0928e7815 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -392,6 +392,7 @@ describe('createApp', () => { + ] diff --git a/packages/frontend-dynamic-feature-loader/CHANGELOG.md b/packages/frontend-dynamic-feature-loader/CHANGELOG.md index cc2c421a4e..58cb740bd5 100644 --- a/packages/frontend-dynamic-feature-loader/CHANGELOG.md +++ b/packages/frontend-dynamic-feature-loader/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/frontend-dynamic-feature-loader +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/config@1.3.6 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/config@1.3.6 + ## 0.1.7-next.0 ### Patch Changes diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json index ce61447770..c8771ca154 100644 --- a/packages/frontend-dynamic-feature-loader/package.json +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-dynamic-feature-loader", - "version": "0.1.7-next.0", + "version": "0.1.8", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx index b2f6486766..c0bf5822cc 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -655,7 +655,7 @@ describe('dynamicFrontendFeaturesLoader', () => { const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]); expect(errorCalls).toEqual([ - `Failed fetching module federation configuration of dynamic frontend plugins: FetchError: invalid json response body at http://localhost:7007/.backstage/dynamic-features/remotes reason: Unexpected end of JSON input`, + `Failed fetching module federation configuration of dynamic frontend plugins: SyntaxError: Unexpected end of JSON input`, ]); const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]); expect(warnCalls).toEqual([]); diff --git a/packages/frontend-internal/CHANGELOG.md b/packages/frontend-internal/CHANGELOG.md index 7b31d280cb..de5b02e547 100644 --- a/packages/frontend-internal/CHANGELOG.md +++ b/packages/frontend-internal/CHANGELOG.md @@ -1,5 +1,28 @@ # @internal/frontend +## 0.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + +## 0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + ## 0.0.15-next.0 ### Patch Changes diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json index 37727cda97..705aec5835 100644 --- a/packages/frontend-internal/package.json +++ b/packages/frontend-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/frontend", - "version": "0.0.15-next.0", + "version": "0.0.16", "backstage": { "role": "web-library", "inline": true @@ -25,8 +25,7 @@ "dependencies": { "@backstage/frontend-plugin-api": "workspace:^", "@backstage/types": "workspace:^", - "@backstage/version-bridge": "workspace:^", - "zod": "^3.22.4" + "@backstage/version-bridge": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/frontend-internal/src/index.ts b/packages/frontend-internal/src/index.ts index a5728f2ff6..38bfdc53f8 100644 --- a/packages/frontend-internal/src/index.ts +++ b/packages/frontend-internal/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export * from './routing'; export * from './wiring'; diff --git a/packages/frontend-internal/src/routing/OpaqueExternalRouteRef.ts b/packages/frontend-internal/src/routing/OpaqueExternalRouteRef.ts new file mode 100644 index 0000000000..9e70dc496a --- /dev/null +++ b/packages/frontend-internal/src/routing/OpaqueExternalRouteRef.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueExternalRouteRef = OpaqueType.create<{ + public: ExternalRouteRef; + versions: { + readonly version: 'v1'; + + getParams(): string[]; + getDescription(): string; + getDefaultTarget(): string | undefined; + + setId(id: string): void; + }; +}>({ + type: '@backstage/ExternalRouteRef', + versions: ['v1'], +}); diff --git a/packages/frontend-internal/src/routing/OpaqueRouteRef.ts b/packages/frontend-internal/src/routing/OpaqueRouteRef.ts new file mode 100644 index 0000000000..83126de8e2 --- /dev/null +++ b/packages/frontend-internal/src/routing/OpaqueRouteRef.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RouteRef } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueRouteRef = OpaqueType.create<{ + public: RouteRef; + versions: { + readonly version: 'v1'; + + getParams(): string[]; + getDescription(): string; + + alias: string | undefined; + + setId(id: string): void; + }; +}>({ + type: '@backstage/RouteRef', + versions: ['v1'], +}); diff --git a/packages/frontend-internal/src/routing/OpaqueSubRouteRef.ts b/packages/frontend-internal/src/routing/OpaqueSubRouteRef.ts new file mode 100644 index 0000000000..a8559f76a2 --- /dev/null +++ b/packages/frontend-internal/src/routing/OpaqueSubRouteRef.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RouteRef, SubRouteRef } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueSubRouteRef = OpaqueType.create<{ + public: SubRouteRef; + versions: { + readonly version: 'v1'; + + getParams(): string[]; + getParent(): RouteRef; + getDescription(): string; + }; +}>({ + type: '@backstage/SubRouteRef', + versions: ['v1'], +}); diff --git a/packages/frontend-internal/src/routing/index.ts b/packages/frontend-internal/src/routing/index.ts new file mode 100644 index 0000000000..cc8c0ee5b8 --- /dev/null +++ b/packages/frontend-internal/src/routing/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { OpaqueRouteRef } from './OpaqueRouteRef'; +export { OpaqueSubRouteRef } from './OpaqueSubRouteRef'; +export { OpaqueExternalRouteRef } from './OpaqueExternalRouteRef'; diff --git a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts index 83d037902b..b7c3674f9a 100644 --- a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts +++ b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts @@ -17,10 +17,10 @@ import { ApiHolder, AppNode, - ExtensionAttachToSpec, + ExtensionDefinitionAttachTo, ExtensionDataValue, ExtensionDataRef, - ExtensionDefinition, + OverridableExtensionDefinition, ExtensionDefinitionParameters, ExtensionInput, PortableSchema, @@ -29,14 +29,14 @@ import { import { OpaqueType } from '@internal/opaque'; export const OpaqueExtensionDefinition = OpaqueType.create<{ - public: ExtensionDefinition; + public: OverridableExtensionDefinition; versions: | { readonly version: 'v1'; readonly kind?: string; readonly namespace?: string; readonly name?: string; - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionDefinitionAttachTo; readonly disabled: boolean; readonly configSchema?: PortableSchema; readonly inputs: { @@ -67,25 +67,17 @@ export const OpaqueExtensionDefinition = OpaqueType.create<{ readonly kind?: string; readonly namespace?: string; readonly name?: string; - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionDefinitionAttachTo; readonly disabled: boolean; readonly configSchema?: PortableSchema; - readonly inputs: { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }; + readonly inputs: { [inputName in string]: ExtensionInput }; readonly output: Array; factory(context: { node: AppNode; apis: ApiHolder; config: object; inputs: ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; + [inputName in string]: ExtensionInput; }>; }): Iterable>; }; diff --git a/packages/frontend-internal/src/wiring/InternalExtensionInput.ts b/packages/frontend-internal/src/wiring/InternalExtensionInput.ts new file mode 100644 index 0000000000..9090023db9 --- /dev/null +++ b/packages/frontend-internal/src/wiring/InternalExtensionInput.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +export type ExtensionInputContext = { + input: string; + kind?: string; + name?: string; +}; + +export const OpaqueExtensionInput = OpaqueType.create<{ + public: ExtensionInput; + versions: { + readonly version: undefined; + readonly context?: ExtensionInputContext; + withContext?(context: ExtensionInputContext): ExtensionInput; + }; +}>({ + type: '@backstage/ExtensionInput', + versions: [undefined], +}); diff --git a/packages/frontend-internal/src/wiring/index.ts b/packages/frontend-internal/src/wiring/index.ts index b61294cb1f..1efcc7d16d 100644 --- a/packages/frontend-internal/src/wiring/index.ts +++ b/packages/frontend-internal/src/wiring/index.ts @@ -15,6 +15,10 @@ */ export { createExtensionDataContainer } from './createExtensionDataContainer'; -export { OpaqueSwappableComponentRef } from './InternalSwappableComponentRef'; export { OpaqueExtensionDefinition } from './InternalExtensionDefinition'; +export { + OpaqueExtensionInput, + type ExtensionInputContext, +} from './InternalExtensionInput'; export { OpaqueFrontendPlugin } from './InternalFrontendPlugin'; +export { OpaqueSwappableComponentRef } from './InternalSwappableComponentRef'; diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 905b5cc9c2..4d43cb675b 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,125 @@ # @backstage/frontend-plugin-api +## 0.13.3 + +### Patch Changes + +- 3bd2a1a: Updated documentation for `createApiRef` to clarify the role of the ID in specifying the owning plugin of an API. +- 9ccf84e: The following blueprints are being restricted to only be used in app plugin overrides and modules. They are being moved to the `@backstage/plugin-app-react` package and have been deprecated: + + - `AppRootWrapperBlueprint` + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- 4554a4e: Added an alpha `PluginWrapperBlueprint` exported from `@backstage/frontend-plugin-api/alpha`, which can install components that will wrap all plugin elements. +- 872eb91: Upgrade `zod-to-json-schema` to latest version + +## 0.13.2 + +### Patch Changes + +- 75683ed: Added a new `errorPresentation` prop to `ExtensionBoundary` to control how errors are presented to the user. The default is `'error-display'`, which is the current behavior of showing the error in the `ErrorDisplay` component. The new option is `'error-api'`, posts errors to the `ErrorApi` and does not allow retries. + + The `AppRootElementBlueprint` now wraps its element in an `ErrorBoundary` using the new `'error-api'` presentation mode. + +- 0bc1ce9: Fixed a versioning conflict that could result in a `.withContext` is not a function error. +- f3f84f1: Made the return type of `.withOverrides` to be simplified. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- 9b8bde4: Removed unnecessary dependencies on `@backstage/core-components`, `@backstage/config`, `@material-ui/core`, and `lodash`. + +## 0.13.2-next.1 + +### Patch Changes + +- 75683ed: Added a new `errorPresentation` prop to `ExtensionBoundary` to control how errors are presented to the user. The default is `'error-display'`, which is the current behavior of showing the error in the `ErrorDisplay` component. The new option is `'error-api'`, posts errors to the `ErrorApi` and does not allow retries. + + The `AppRootElementBlueprint` now wraps its element in an `ErrorBoundary` using the new `'error-api'` presentation mode. + +- f3f84f1: Made the return type of `.withOverrides` to be simplified. +- Updated dependencies + - @backstage/core-components@0.18.4-next.2 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.13.2-next.0 + +### Patch Changes + +- 0bc1ce9: Fixed a versioning conflict that could result in a `.withContext` is not a function error. +- 97cd16f: Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old. +- Updated dependencies + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.13.0 + +### Minor Changes + +- 7d87b4f: Renamed `ExtensionDefinition` to `OverridableExtensionDefinition` and introduced a slimmer `ExtensionDefinition` type that does not include override methods. The overridable type is generally used as an output type, while plain `ExtensionDefinition`s are used for input. This reduces type conflicts across different of `@backstage/frontend-plugin-api`, improving long-term compatibility. + +### Patch Changes + +- 4d03f08: Internal refactor of route reference implementations with minor updates to the `toString` implementations. +- 7c6a66d: Added support for plugin-relative `attachTo` declarations for extension definitions. This allows for the creation of extension and extension blueprints that attach to other extensions of a particular `kind` in the same plugin, rather than needing to provide the exact extension ID. This is particularly useful when wanting to provide extension blueprints with a built-in hierarchy where the extensions created from one blueprint attach to extensions created from the other blueprint, for example: + + ```ts + // kind: 'tabbed-page' + const parentPage = TabbedPageBlueprint.make({ + params: {....} + }) + // attachTo: { kind: 'tabbed-page', input: 'tabs' } + const child1 = TabContentBlueprint.make({ + name: 'tab1', + params: {....} + }) + ``` + +- 878c251: Updated to `ExtensionInput` to make all type parameters optional. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + +## 0.12.2-next.2 + +### Patch Changes + +- 7c6a66d: Added support for plugin-relative `attachTo` declarations for extension definitions. This allows for the creation of extension and extension blueprints that attach to other extensions of a particular `kind` in the same plugin, rather than needing to provide the exact extension ID. This is particularly useful when wanting to provide extension blueprints with a built-in hierarchy where the extensions created from one blueprint attach to extensions created from the other blueprint, for example: + + ```ts + // kind: 'tabbed-page' + const parentPage = TabbedPageBlueprint.make({ + params: {....} + }) + // attachTo: { kind: 'tabbed-page', input: 'tabs' } + const child1 = TabContentBlueprint.make({ + name: 'tab1', + params: {....} + }) + ``` + +- Updated dependencies + - @backstage/core-components@0.18.3-next.2 + +## 0.12.2-next.1 + +### Patch Changes + +- 878c251: Updated to `ExtensionInput` to make all type parameters optional. +- Updated dependencies + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + ## 0.12.2-next.0 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 842e247e01..b48480e325 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,13 +1,11 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.12.2-next.0", + "version": "0.13.3", "backstage": { "role": "web-library" }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "repository": { "type": "git", @@ -16,8 +14,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -31,17 +44,15 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/core-components": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", - "@material-ui/core": "^4.12.4", - "lodash": "^4.17.21", "zod": "^3.22.4", - "zod-to-json-schema": "^3.21.4" + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/packages/frontend-plugin-api/report-alpha.api.md b/packages/frontend-plugin-api/report-alpha.api.md new file mode 100644 index 0000000000..77a377bb8e --- /dev/null +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -0,0 +1,68 @@ +## API Report File for "@backstage/frontend-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ApiRef } from '@backstage/frontend-plugin-api'; +import { ComponentType } from 'react'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ReactNode } from 'react'; + +// @alpha +export type PluginWrapperApi = { + getPluginWrapper(pluginId: string): + | ComponentType<{ + children: ReactNode; + }> + | undefined; +}; + +// @alpha +export const pluginWrapperApiRef: ApiRef; + +// @alpha +export const PluginWrapperBlueprint: ExtensionBlueprint<{ + kind: 'plugin-wrapper'; + params: (params: { + loader: () => Promise<{ + component: ComponentType<{ + children: ReactNode; + }>; + }>; + }) => ExtensionBlueprintParams<{ + loader: () => Promise<{ + component: ComponentType<{ + children: ReactNode; + }>; + }>; + }>; + output: ExtensionDataRef< + () => Promise<{ + component: ComponentType<{ + children: ReactNode; + }>; + }>, + 'core.plugin-wrapper.loader', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + wrapper: ConfigurableExtensionDataRef< + () => Promise<{ + component: ComponentType<{ + children: ReactNode; + }>; + }>, + 'core.plugin-wrapper.loader', + {} + >; + }; +}>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 52a50659b6..1a7f2e3c7a 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -3,103 +3,43 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AlertApi } from '@backstage/core-plugin-api'; -import { alertApiRef } from '@backstage/core-plugin-api'; -import { AlertMessage } from '@backstage/core-plugin-api'; -import { AnyApiFactory } from '@backstage/core-plugin-api'; -import { AnyApiRef } from '@backstage/core-plugin-api'; import { AnyRouteRefParams as AnyRouteRefParams_2 } from '@backstage/frontend-plugin-api'; -import { ApiFactory } from '@backstage/core-plugin-api'; -import { ApiHolder } from '@backstage/core-plugin-api'; -import { ApiRef } from '@backstage/core-plugin-api'; -import { ApiRefConfig } from '@backstage/core-plugin-api'; -import { AppTheme } from '@backstage/core-plugin-api'; -import { AppThemeApi } from '@backstage/core-plugin-api'; -import { appThemeApiRef } from '@backstage/core-plugin-api'; -import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; -import { AuthProviderInfo } from '@backstage/core-plugin-api'; -import { AuthRequestOptions } from '@backstage/core-plugin-api'; -import { BackstageIdentityApi } from '@backstage/core-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; -import { BackstageUserIdentity } from '@backstage/core-plugin-api'; -import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; -import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; +import { ApiRef as ApiRef_2 } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; -import { ConfigApi } from '@backstage/core-plugin-api'; -import { configApiRef } from '@backstage/core-plugin-api'; +import type { Config } from '@backstage/config'; import { ConfigurableExtensionDataRef as ConfigurableExtensionDataRef_2 } from '@backstage/frontend-plugin-api'; -import { createApiFactory } from '@backstage/core-plugin-api'; -import { createApiRef } from '@backstage/core-plugin-api'; -import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; -import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { createTranslationResource } from '@backstage/core-plugin-api/alpha'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; -import { discoveryApiRef } from '@backstage/core-plugin-api'; -import { ErrorApi } from '@backstage/core-plugin-api'; -import { ErrorApiError } from '@backstage/core-plugin-api'; -import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; -import { errorApiRef } from '@backstage/core-plugin-api'; import { Expand } from '@backstage/types'; +import { ExpandRecursive } from '@backstage/types'; import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api'; -import { FeatureFlag } from '@backstage/core-plugin-api'; -import { FeatureFlagsApi } from '@backstage/core-plugin-api'; -import { featureFlagsApiRef } from '@backstage/core-plugin-api'; -import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; -import { FeatureFlagState } from '@backstage/core-plugin-api'; -import { FetchApi } from '@backstage/core-plugin-api'; -import { fetchApiRef } from '@backstage/core-plugin-api'; -import { githubAuthApiRef } from '@backstage/core-plugin-api'; -import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; -import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api'; -import { IconComponent as IconComponent_3 } from '@backstage/core-plugin-api'; -import { IdentityApi } from '@backstage/core-plugin-api'; -import { identityApiRef } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { JSX as JSX_3 } from 'react'; -import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; -import { OAuthApi } from '@backstage/core-plugin-api'; -import { OAuthRequestApi } from '@backstage/core-plugin-api'; -import { oauthRequestApiRef } from '@backstage/core-plugin-api'; -import { OAuthRequester } from '@backstage/core-plugin-api'; -import { OAuthRequesterOptions } from '@backstage/core-plugin-api'; -import { OAuthScope } from '@backstage/core-plugin-api'; -import { oktaAuthApiRef } from '@backstage/core-plugin-api'; -import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; -import { OpenIdConnectApi } from '@backstage/core-plugin-api'; -import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; -import { PendingOAuthRequest } from '@backstage/core-plugin-api'; -import { ProfileInfo } from '@backstage/core-plugin-api'; -import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; +import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; -import { SessionApi } from '@backstage/core-plugin-api'; -import { SessionState } from '@backstage/core-plugin-api'; -import { SignInPageProps } from '@backstage/core-plugin-api'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { storageApiRef } from '@backstage/core-plugin-api'; -import { StorageValueSnapshot } from '@backstage/core-plugin-api'; -import { TranslationMessages } from '@backstage/core-plugin-api/alpha'; -import { TranslationMessagesOptions } from '@backstage/core-plugin-api/alpha'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -import { TranslationRefOptions } from '@backstage/core-plugin-api/alpha'; -import { TranslationResource } from '@backstage/core-plugin-api/alpha'; -import { TranslationResourceOptions } from '@backstage/core-plugin-api/alpha'; -import { TypesToApiRefs } from '@backstage/core-plugin-api'; -import { useApi } from '@backstage/core-plugin-api'; -import { useApiHolder } from '@backstage/core-plugin-api'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; -import { withApis } from '@backstage/core-plugin-api'; -import { z } from 'zod'; +import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api'; +import type { z } from 'zod'; -export { AlertApi }; +// @public +export type AlertApi = { + post(alert: AlertMessage): void; + alert$(): Observable; +}; -export { alertApiRef }; +// @public +export const alertApiRef: ApiRef; -export { AlertMessage }; +// @public +export type AlertMessage = { + message: string; + severity?: 'success' | 'info' | 'warning' | 'error'; + display?: 'permanent' | 'transient'; +}; // @public export type AnalyticsApi = { @@ -143,12 +83,12 @@ export type AnalyticsImplementation = { }; // @public -export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{ +export const AnalyticsImplementationBlueprint: ExtensionBlueprint_2<{ kind: 'analytics'; params: ( params: AnalyticsImplementationFactory, - ) => ExtensionBlueprintParams>; - output: ExtensionDataRef< + ) => ExtensionBlueprintParams_2>; + output: ExtensionDataRef_2< AnalyticsImplementationFactory<{}>, 'core.analytics.factory', {} @@ -157,7 +97,7 @@ export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - factory: ConfigurableExtensionDataRef< + factory: ConfigurableExtensionDataRef_2< AnalyticsImplementationFactory<{}>, 'core.analytics.factory', {} @@ -187,9 +127,17 @@ export type AnalyticsTracker = { ) => void; }; -export { AnyApiFactory }; +// @public +export type AnyApiFactory = ApiFactory< + unknown, + unknown, + { + [key in string]: unknown; + } +>; -export { AnyApiRef }; +// @public +export type AnyApiRef = ApiRef; // @public @deprecated (undocumented) export type AnyExtensionDataRef = ExtensionDataRef; @@ -202,7 +150,7 @@ export type AnyRouteRefParams = | undefined; // @public -export const ApiBlueprint: ExtensionBlueprint<{ +export const ApiBlueprint: ExtensionBlueprint_2<{ kind: 'api'; params: < TApi, @@ -210,13 +158,13 @@ export const ApiBlueprint: ExtensionBlueprint<{ TDeps extends { [name in string]: unknown }, >( params: ApiFactory, - ) => ExtensionBlueprintParams; - output: ExtensionDataRef; + ) => ExtensionBlueprintParams_2; + output: ExtensionDataRef_2; inputs: {}; config: {}; configInput: {}; dataRefs: { - factory: ConfigurableExtensionDataRef< + factory: ConfigurableExtensionDataRef_2< AnyApiFactory, 'core.api.factory', {} @@ -224,13 +172,51 @@ export const ApiBlueprint: ExtensionBlueprint<{ }; }>; -export { ApiFactory }; +// @public +export type ApiFactory< + Api, + Impl extends Api, + Deps extends { + [name in string]: unknown; + }, +> = { + api: ApiRef; + deps: TypesToApiRefs; + factory(deps: Deps): Impl; +}; -export { ApiHolder }; +// @public +export type ApiHolder = { + get(api: ApiRef): T | undefined; +}; -export { ApiRef }; +// @public +export type ApiRef = { + id: string; + T: T; +}; -export { ApiRefConfig }; +// @public +export type ApiRefConfig = { + id: string; +}; + +// @public (undocumented) +export type AppLanguageApi = { + getAvailableLanguages(): { + languages: string[]; + }; + setLanguage(language?: string): void; + getLanguage(): { + language: string; + }; + language$(): Observable<{ + language: string; + }>; +}; + +// @public (undocumented) +export const appLanguageApiRef: ApiRef; // @public export interface AppNode { @@ -259,7 +245,7 @@ export interface AppNodeInstance { // @public export interface AppNodeSpec { // (undocumented) - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionAttachTo; // (undocumented) readonly config?: unknown; // (undocumented) @@ -273,26 +259,26 @@ export interface AppNodeSpec { } // @public -export const AppRootElementBlueprint: ExtensionBlueprint<{ +export const AppRootElementBlueprint: ExtensionBlueprint_2<{ kind: 'app-root-element'; params: { element: JSX.Element; }; - output: ExtensionDataRef; + output: ExtensionDataRef_2; inputs: {}; config: {}; configInput: {}; dataRefs: never; }>; -// @public -export const AppRootWrapperBlueprint: ExtensionBlueprint<{ +// @public @deprecated +export const AppRootWrapperBlueprint: ExtensionBlueprint_2<{ kind: 'app-root-wrapper'; params: { Component?: [error: 'Use the `component` parameter instead']; component: (props: { children: ReactNode }) => JSX.Element | null; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< (props: { children: ReactNode }) => JSX.Element | null, 'app.root.wrapper', {} @@ -301,7 +287,7 @@ export const AppRootWrapperBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - component: ConfigurableExtensionDataRef< + component: ConfigurableExtensionDataRef_2< (props: { children: ReactNode }) => JSX.Element | null, 'app.root.wrapper', {} @@ -309,11 +295,25 @@ export const AppRootWrapperBlueprint: ExtensionBlueprint<{ }; }>; -export { AppTheme }; +// @public +export type AppTheme = { + id: string; + title: string; + variant: 'light' | 'dark'; + icon?: React.ReactElement; + Provider(props: { children: ReactNode }): JSX.Element | null; +}; -export { AppThemeApi }; +// @public +export type AppThemeApi = { + getInstalledThemes(): AppTheme[]; + activeThemeId$(): Observable; + getActiveThemeId(): string | undefined; + setActiveThemeId(themeId?: string): void; +}; -export { appThemeApiRef }; +// @public +export const appThemeApiRef: ApiRef; // @public export interface AppTree { @@ -333,27 +333,63 @@ export interface AppTreeApi { } // @public -export const appTreeApiRef: ApiRef; +export const appTreeApiRef: ApiRef_2; -export { atlassianAuthApiRef }; +// @public +export const atlassianAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; -export { AuthProviderInfo }; +// @public +export type AuthProviderInfo = { + id: string; + title: string; + icon: IconComponent; + message?: string; +}; -export { AuthRequestOptions }; +// @public +export type AuthRequestOptions = { + optional?: boolean; + instantPopup?: boolean; +}; -export { BackstageIdentityApi }; +// @public +export type BackstageIdentityApi = { + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; -export { BackstageIdentityResponse }; +// @public +export type BackstageIdentityResponse = { + token: string; + expiresAt?: Date; + identity: BackstageUserIdentity; +}; -export { BackstageUserIdentity }; +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; -export { bitbucketAuthApiRef }; +// @public +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; -export { bitbucketServerAuthApiRef }; +// @public +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; -export { ConfigApi }; +// @public +export type ConfigApi = Config; -export { configApiRef }; +// @public +export const configApiRef: ApiRef; // @public (undocumented) export interface ConfigurableExtensionDataRef< @@ -377,35 +413,43 @@ export interface ConfigurableExtensionDataRef< // @public (undocumented) export const coreExtensionData: { - title: ConfigurableExtensionDataRef; - reactElement: ConfigurableExtensionDataRef< + title: ConfigurableExtensionDataRef_2; + reactElement: ConfigurableExtensionDataRef_2< JSX_3.Element, 'core.reactElement', {} >; - routePath: ConfigurableExtensionDataRef; - routeRef: ConfigurableExtensionDataRef< + routePath: ConfigurableExtensionDataRef_2; + routeRef: ConfigurableExtensionDataRef_2< RouteRef, 'core.routing.ref', {} >; }; -export { createApiFactory }; +// @public +export function createApiFactory< + Api, + Impl extends Api, + Deps extends { + [name in string]: unknown; + }, +>(factory: ApiFactory): ApiFactory; -export { createApiRef }; +// @public +export function createApiFactory( + api: ApiRef, + instance: Impl, +): ApiFactory; + +// @public +export function createApiRef(config: ApiRefConfig): ApiRef; // @public export function createExtension< UOutput extends ExtensionDataRef, TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [inputName in string]: ExtensionInput; }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; @@ -413,6 +457,7 @@ export function createExtension< UFactoryOutput extends ExtensionDataValue, const TKind extends string | undefined = undefined, const TName extends string | undefined = undefined, + UParentInputs extends ExtensionDataRef = ExtensionDataRef, >( options: CreateExtensionOptions< TKind, @@ -420,9 +465,10 @@ export function createExtension< UOutput, TInputs, TConfigSchema, - UFactoryOutput + UFactoryOutput, + UParentInputs >, -): ExtensionDefinition<{ +): OverridableExtensionDefinition<{ config: string extends keyof TConfigSchema ? {} : { @@ -453,19 +499,14 @@ export function createExtensionBlueprint< TParams extends object | ExtensionBlueprintDefineParams, UOutput extends ExtensionDataRef, TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [inputName in string]: ExtensionInput; }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, TKind extends string, + UParentInputs extends ExtensionDataRef, TDataRefs extends { [name in string]: ExtensionDataRef; } = never, @@ -477,7 +518,8 @@ export function createExtensionBlueprint< TInputs, TConfigSchema, UFactoryOutput, - TDataRefs + TDataRefs, + UParentInputs >, ): ExtensionBlueprint<{ kind: TKind; @@ -511,13 +553,7 @@ export type CreateExtensionBlueprintOptions< TParams extends object | ExtensionBlueprintDefineParams, UOutput extends ExtensionDataRef, TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [inputName in string]: ExtensionInput; }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; @@ -526,9 +562,11 @@ export type CreateExtensionBlueprintOptions< TDataRefs extends { [name in string]: ExtensionDataRef; }, + UParentInputs extends ExtensionDataRef, > = { kind: TKind; - attachTo: ExtensionAttachToSpec; + attachTo: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo; disabled?: boolean; inputs?: TInputs; output: Array; @@ -601,22 +639,18 @@ export type CreateExtensionOptions< TName extends string | undefined, UOutput extends ExtensionDataRef, TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [inputName in string]: ExtensionInput; }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UParentInputs extends ExtensionDataRef, > = { kind?: TKind; name?: TName; - attachTo: ExtensionAttachToSpec; + attachTo: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo; disabled?: boolean; inputs?: TInputs; output: Array; @@ -641,7 +675,7 @@ export function createExternalRouteRef< } | undefined = undefined, TParamKeys extends string = string, ->(options?: { +>(config?: { readonly params?: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[]; @@ -785,11 +819,50 @@ export type CreateSwappableComponentOptions< transformProps?: (props: TExternalComponentProps) => TInnerComponentProps; }; -export { createTranslationMessages }; +// @public +export function createTranslationMessages< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TFull extends boolean, +>( + options: TranslationMessagesOptions, +): TranslationMessages; -export { createTranslationRef }; +// @public (undocumented) +export function createTranslationRef< + TId extends string, + const TNestedMessages extends AnyNestedMessages, + TTranslations extends { + [language in string]: () => Promise<{ + default: { + [key in keyof FlattenedMessages]: string | null; + }; + }>; + }, +>( + config: TranslationRefOptions, +): TranslationRef>; -export { createTranslationResource }; +// @public (undocumented) +export function createTranslationResource< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages + | { + [key in keyof TMessages]: string | null; + }; + }>; + }, +>( + options: TranslationResourceOptions, +): TranslationResource; // @public export interface DialogApi { @@ -821,24 +894,44 @@ export interface DialogApiDialog { } // @public -export const dialogApiRef: ApiRef; +export const dialogApiRef: ApiRef_2; -export { DiscoveryApi }; +// @public +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; -export { discoveryApiRef }; +// @public +export const discoveryApiRef: ApiRef; -export { ErrorApi }; +// @public +export type ErrorApi = { + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; +}; -export { ErrorApiError }; +// @public +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; -export { ErrorApiErrorContext }; +// @public +export type ErrorApiErrorContext = { + hidden?: boolean; +}; -export { errorApiRef }; +// @public +export const errorApiRef: ApiRef; // @public (undocumented) export const ErrorDisplay: { (props: ErrorDisplayProps): JSX.Element | null; - ref: SwappableComponentRef; + ref: SwappableComponentRef_2; }; // @public (undocumented) @@ -863,7 +956,7 @@ export interface Extension { } // @public (undocumented) -export type ExtensionAttachToSpec = +export type ExtensionAttachTo = | { id: string; input: string; @@ -873,6 +966,9 @@ export type ExtensionAttachToSpec = input: string; }>; +// @public @deprecated (undocumented) +export type ExtensionAttachToSpec = ExtensionAttachTo; + // @public (undocumented) export interface ExtensionBlueprint< T extends ExtensionBlueprintParameters = ExtensionBlueprintParameters, @@ -882,17 +978,19 @@ export interface ExtensionBlueprint< // (undocumented) make< TName extends string | undefined, - TParamsInput extends AnyParamsInput>, + TParamsInput extends AnyParamsInput_2>, + UParentInputs extends ExtensionDataRef, >(args: { name?: TName; - attachTo?: ExtensionAttachToSpec; + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo, UParentInputs>; disabled?: boolean; params: TParamsInput extends ExtensionBlueprintDefineParams ? TParamsInput : T['params'] extends ExtensionBlueprintDefineParams ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `.make({ params: defineParams => defineParams() })`' : T['params']; - }): ExtensionDefinition<{ + }): OverridableExtensionDefinition<{ kind: T['kind']; name: string | undefined extends TName ? undefined : TName; config: T['config']; @@ -908,18 +1006,19 @@ export interface ExtensionBlueprint< }, UFactoryOutput extends ExtensionDataValue, UNewOutput extends ExtensionDataRef, + UParentInputs extends ExtensionDataRef, TExtraInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; - }, + [inputName in string]: ExtensionInput; + } = {}, >(args: { name?: TName; - attachTo?: ExtensionAttachToSpec; + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo< + ExtensionDataRef extends UNewOutput + ? NonNullable + : UNewOutput, + UParentInputs + >; disabled?: boolean; inputs?: TExtraInputs & { [KName in keyof T['inputs']]?: `Error: Input '${KName & @@ -934,7 +1033,7 @@ export interface ExtensionBlueprint< }; factory( originalFactory: < - TParamsInput extends AnyParamsInput>, + TParamsInput extends AnyParamsInput_2>, >( params: TParamsInput extends ExtensionBlueprintDefineParams ? TParamsInput @@ -963,27 +1062,31 @@ export interface ExtensionBlueprint< : UNewOutput, UFactoryOutput >; - }): ExtensionDefinition<{ - config: (string extends keyof TExtensionConfigSchema - ? {} - : { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }) & - T['config']; - configInput: (string extends keyof TExtensionConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] + }): OverridableExtensionDefinition<{ + config: Expand< + (string extends keyof TExtensionConfigSchema + ? {} + : { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType >; - }> - >) & - T['configInput']; + }) & + T['config'] + >; + configInput: Expand< + (string extends keyof TExtensionConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + >) & + T['configInput'] + >; output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput; - inputs: T['inputs'] & TExtraInputs; + inputs: Expand; kind: T['kind']; name: string | undefined extends TName ? undefined : TName; params: T['params']; @@ -1008,13 +1111,7 @@ export type ExtensionBlueprintParameters = { }; output?: ExtensionDataRef; inputs?: { - [KName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [KName in string]: ExtensionInput; }; dataRefs?: { [name in string]: ExtensionDataRef; @@ -1049,6 +1146,8 @@ export interface ExtensionBoundaryProps { // (undocumented) children: ReactNode; // (undocumented) + errorPresentation?: 'error-api' | 'error-display'; + // (undocumented) node: AppNode; } @@ -1104,111 +1203,49 @@ export type ExtensionDataValue = { }; // @public (undocumented) -export type ExtensionDefinition< - T extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, -> = { +export interface ExtensionDefinition< + TParams extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, +> { + // (undocumented) $$type: '@backstage/ExtensionDefinition'; - readonly T: T; - override< - TExtensionConfigSchema extends { - [key in string]: (zImpl: typeof z) => z.ZodType; - }, - UFactoryOutput extends ExtensionDataValue, - UNewOutput extends ExtensionDataRef, - TExtraInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; + // (undocumented) + readonly T: TParams; +} + +// @public +export type ExtensionDefinitionAttachTo< + UParentInputs extends ExtensionDataRef = ExtensionDataRef, +> = + | { + id: string; + input: string; + relative?: never; + } + | { + relative: { + kind?: string; + name?: string; + }; + input: string; + id?: never; + } + | ExtensionInput + | Array< + | { + id: string; + input: string; + relative?: never; } - >; - }, - TParamsInput extends AnyParamsInput_2>, - >( - args: Expand< - { - attachTo?: ExtensionAttachToSpec; - disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof T['inputs']]?: `Error: Input '${KName & - string}' is already defined in parent definition`; - }; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof T['config']]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; + | { + relative: { + kind?: string; + name?: string; }; - }; - factory?( - originalFactory: < - TFactoryParamsReturn extends AnyParamsInput_2< - NonNullable - >, - >( - context?: Expand< - { - config?: T['config']; - inputs?: ResolvedInputValueOverrides>; - } & ([T['params']] extends [never] - ? {} - : { - params?: TFactoryParamsReturn extends ExtensionBlueprintDefineParams - ? TFactoryParamsReturn - : T['params'] extends ExtensionBlueprintDefineParams - ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams())`' - : Partial; - }) - >, - ) => ExtensionDataContainer>, - context: { - node: AppNode; - apis: ApiHolder; - config: T['config'] & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & ([T['params']] extends [never] - ? {} - : { - params?: TParamsInput extends ExtensionBlueprintDefineParams - ? TParamsInput - : T['params'] extends ExtensionBlueprintDefineParams - ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams())`' - : Partial; - }) - > & - VerifyExtensionFactoryOutput< - ExtensionDataRef extends UNewOutput - ? NonNullable - : UNewOutput, - UFactoryOutput - >, - ): ExtensionDefinition<{ - kind: T['kind']; - name: T['name']; - output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput; - inputs: T['inputs'] & TExtraInputs; - config: T['config'] & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - configInput: T['configInput'] & - z.input< - z.ZodObject<{ - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] - >; - }> - >; - }>; -}; + input: string; + id?: never; + } + | ExtensionInput + >; // @public (undocumented) export type ExtensionDefinitionParameters = { @@ -1222,13 +1259,7 @@ export type ExtensionDefinitionParameters = { }; output?: ExtensionDataRef; inputs?: { - [KName in string]: ExtensionInput< - ExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; + [KName in string]: ExtensionInput; }; params?: object | ExtensionBlueprintDefineParams; }; @@ -1253,20 +1284,23 @@ export interface ExtensionInput< { optional?: true; } - >, + > = ExtensionDataRef, TConfig extends { singleton: boolean; optional: boolean; + } = { + singleton: boolean; + optional: boolean; }, > { // (undocumented) - $$type: '@backstage/ExtensionInput'; + readonly $$type: '@backstage/ExtensionInput'; // (undocumented) - config: TConfig; + readonly config: TConfig; // (undocumented) - extensionData: Array; + readonly extensionData: Array; // (undocumented) - replaces?: Array<{ + readonly replaces?: Array<{ id: string; input: string; }>; @@ -1282,24 +1316,60 @@ export interface ExternalRouteRef< readonly T: TParams; } -export { FeatureFlag }; +// @public +export type FeatureFlag = { + name: string; + pluginId: string; + description?: string; +}; // @public export type FeatureFlagConfig = { name: string; }; -export { FeatureFlagsApi }; +// @public +export interface FeatureFlagsApi { + getRegisteredFlags(): FeatureFlag[]; + isActive(name: string): boolean; + registerFlag(flag: FeatureFlag): void; + save(options: FeatureFlagsSaveOptions): void; +} -export { featureFlagsApiRef }; +// @public +export const featureFlagsApiRef: ApiRef; -export { FeatureFlagsSaveOptions }; +// @public +export type FeatureFlagsSaveOptions = { + states: Record; + merge?: boolean; +}; -export { FeatureFlagState }; +// @public +export const FeatureFlagState: { + readonly None: 0; + readonly Active: 1; +}; -export { FetchApi }; +// @public (undocumented) +export type FeatureFlagState = + (typeof FeatureFlagState)[keyof typeof FeatureFlagState]; -export { fetchApiRef }; +// @public (undocumented) +export namespace FeatureFlagState { + // (undocumented) + export type Active = typeof FeatureFlagState.Active; + // (undocumented) + export type None = typeof FeatureFlagState.None; +} + +// @public +export type FetchApi = { + fetch: typeof fetch; +}; + +// @public +export const fetchApiRef: ApiRef; // @public (undocumented) export type FrontendFeature = FrontendPlugin | FrontendModule; @@ -1364,19 +1434,36 @@ export type FrontendPluginInfoOptions = { manifest?: () => Promise; }; -export { githubAuthApiRef }; +// @public +export const githubAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; -export { gitlabAuthApiRef }; +// @public +export const gitlabAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; -export { googleAuthApiRef }; +// @public +export const googleAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; -// @public (undocumented) -export const IconBundleBlueprint: ExtensionBlueprint<{ +// @public @deprecated (undocumented) +export const IconBundleBlueprint: ExtensionBlueprint_2<{ kind: 'icon-bundle'; params: { icons: { [key in string]: IconComponent }; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< { [x: string]: IconComponent; }, @@ -1387,7 +1474,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - icons: ConfigurableExtensionDataRef< + icons: ConfigurableExtensionDataRef_2< { [x: string]: IconComponent; }, @@ -1411,15 +1498,31 @@ export interface IconsApi { } // @public -export const iconsApiRef: ApiRef; - -export { IdentityApi }; - -export { identityApiRef }; - -export { microsoftAuthApiRef }; +export const iconsApiRef: ApiRef_2; // @public +export type IdentityApi = { + getProfileInfo(): Promise; + getBackstageIdentity(): Promise; + getCredentials(): Promise<{ + token?: string; + }>; + signOut(): Promise; +}; + +// @public +export const identityApiRef: ApiRef; + +// @public +export const microsoftAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; + +// @public @deprecated export const NavContentBlueprint: ExtensionBlueprint_2<{ kind: 'nav-content'; params: { @@ -1459,17 +1562,17 @@ export interface NavContentComponentProps { } // @public -export const NavItemBlueprint: ExtensionBlueprint<{ +export const NavItemBlueprint: ExtensionBlueprint_2<{ kind: 'nav-item'; params: { title: string; - icon: IconComponent_3; + icon: IconComponent; routeRef: RouteRef; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< { title: string; - icon: IconComponent_3; + icon: IconComponent; routeRef: RouteRef; }, 'core.nav-item.target', @@ -1479,10 +1582,10 @@ export const NavItemBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - target: ConfigurableExtensionDataRef< + target: ConfigurableExtensionDataRef_2< { title: string; - icon: IconComponent_3; + icon: IconComponent; routeRef: RouteRef; }, 'core.nav-item.target', @@ -1494,7 +1597,7 @@ export const NavItemBlueprint: ExtensionBlueprint<{ // @public (undocumented) export const NotFoundErrorPage: { (props: NotFoundErrorPageProps): JSX.Element | null; - ref: SwappableComponentRef; + ref: SwappableComponentRef_2; }; // @public (undocumented) @@ -1502,25 +1605,178 @@ export type NotFoundErrorPageProps = { children?: ReactNode; }; -export { OAuthApi }; +// @public +export type OAuthApi = { + getAccessToken( + scope?: OAuthScope, + options?: AuthRequestOptions, + ): Promise; +}; -export { OAuthRequestApi }; +// @public +export type OAuthRequestApi = { + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; + authRequest$(): Observable; +}; -export { oauthRequestApiRef }; +// @public +export const oauthRequestApiRef: ApiRef; -export { OAuthRequester }; +// @public +export type OAuthRequester = ( + scopes: Set, +) => Promise; -export { OAuthRequesterOptions }; +// @public +export type OAuthRequesterOptions = { + provider: AuthProviderInfo; + onAuthRequest(scopes: Set): Promise; +}; -export { OAuthScope }; +// @public +export type OAuthScope = string | string[]; -export { oktaAuthApiRef }; +// @public +export const oktaAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; -export { oneloginAuthApiRef }; +// @public +export const oneloginAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; -export { OpenIdConnectApi }; +// @public +export type OpenIdConnectApi = { + getIdToken(options?: AuthRequestOptions): Promise; +}; -export { openshiftAuthApiRef }; +// @public +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + +// @public (undocumented) +export interface OverridableExtensionDefinition< + T extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, +> extends ExtensionDefinition { + readonly inputs: { + [K in keyof T['inputs']]: ExtensionInput< + T['inputs'][K] extends ExtensionInput ? IData : never + >; + }; + // (undocumented) + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends ExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput; + }, + TParamsInput extends AnyParamsInput>, + UParentInputs extends ExtensionDataRef, + >( + args: Expand< + { + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo< + ExtensionDataRef extends UNewOutput + ? NonNullable + : UNewOutput, + UParentInputs + >; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof T['inputs']]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof T['config']]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory?( + originalFactory: < + TFactoryParamsReturn extends AnyParamsInput< + NonNullable + >, + >( + context?: Expand< + { + config?: T['config']; + inputs?: ResolvedInputValueOverrides>; + } & ([T['params']] extends [never] + ? {} + : { + params?: TFactoryParamsReturn extends ExtensionBlueprintDefineParams + ? TFactoryParamsReturn + : T['params'] extends ExtensionBlueprintDefineParams + ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams())`' + : Partial; + }) + >, + ) => ExtensionDataContainer>, + context: { + node: AppNode; + apis: ApiHolder; + config: T['config'] & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & ([T['params']] extends [never] + ? {} + : { + params?: TParamsInput extends ExtensionBlueprintDefineParams + ? TParamsInput + : T['params'] extends ExtensionBlueprintDefineParams + ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams())`' + : Partial; + }) + > & + VerifyExtensionFactoryOutput< + ExtensionDataRef extends UNewOutput + ? NonNullable + : UNewOutput, + UFactoryOutput + >, + ): OverridableExtensionDefinition<{ + kind: T['kind']; + name: T['name']; + output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput; + inputs: T['inputs'] & TExtraInputs; + config: T['config'] & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + configInput: T['configInput'] & + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + >; + }>; +} // @public export interface OverridableFrontendPlugin< @@ -1541,7 +1797,9 @@ export interface OverridableFrontendPlugin< }, > extends FrontendPlugin { // (undocumented) - getExtension(id: TId): TExtensionMap[TId]; + getExtension( + id: TId, + ): OverridableExtensionDefinition; // (undocumented) withOverrides(options: { extensions: Array; @@ -1550,7 +1808,7 @@ export interface OverridableFrontendPlugin< } // @public -export const PageBlueprint: ExtensionBlueprint<{ +export const PageBlueprint: ExtensionBlueprint_2<{ kind: 'page'; params: { defaultPath?: [Error: `Use the 'path' param instead`]; @@ -1559,10 +1817,10 @@ export const PageBlueprint: ExtensionBlueprint<{ routeRef?: RouteRef; }; output: - | ExtensionDataRef - | ExtensionDataRef - | ExtensionDataRef< - RouteRef, + | ExtensionDataRef_2 + | ExtensionDataRef_2 + | ExtensionDataRef_2< + RouteRef, 'core.routing.ref', { optional: true; @@ -1578,7 +1836,12 @@ export const PageBlueprint: ExtensionBlueprint<{ dataRefs: never; }>; -export { PendingOAuthRequest }; +// @public +export type PendingOAuthRequest = { + provider: AuthProviderInfo; + reject(): void; + trigger(): Promise; +}; // @public (undocumented) export interface PluginOptions< @@ -1611,32 +1874,39 @@ export type PortableSchema = { schema: JsonObject; }; -export { ProfileInfo }; +// @public +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; -export { ProfileInfoApi }; +// @public +export type ProfileInfoApi = { + getProfile(options?: AuthRequestOptions): Promise; +}; // @public (undocumented) export const Progress: { (props: ProgressProps): JSX.Element | null; - ref: SwappableComponentRef; + ref: SwappableComponentRef_2; }; // @public (undocumented) export type ProgressProps = {}; // @public -export type ResolvedExtensionInput< - TExtensionInput extends ExtensionInput, -> = TExtensionInput['extensionData'] extends Array - ? { - node: AppNode; - } & ExtensionDataContainer - : never; +export type ResolvedExtensionInput = + TExtensionInput['extensionData'] extends Array + ? { + node: AppNode; + } & ExtensionDataContainer + : never; // @public export type ResolvedExtensionInputs< TInputs extends { - [name in string]: ExtensionInput; + [name in string]: ExtensionInput; }, > = { [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] @@ -1653,14 +1923,14 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; -// @public (undocumented) -export const RouterBlueprint: ExtensionBlueprint<{ +// @public @deprecated (undocumented) +export const RouterBlueprint: ExtensionBlueprint_2<{ kind: 'app-router-component'; params: { Component?: [error: 'Use the `component` parameter instead']; component: (props: { children: ReactNode }) => JSX.Element | null; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< (props: { children: ReactNode }) => JSX.Element | null, 'app.router.wrapper', {} @@ -1669,7 +1939,7 @@ export const RouterBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - component: ConfigurableExtensionDataRef< + component: ConfigurableExtensionDataRef_2< (props: { children: ReactNode }) => JSX.Element | null, 'app.router.wrapper', {} @@ -1702,19 +1972,39 @@ export interface RouteResolutionApi { } // @public -export const routeResolutionApiRef: ApiRef; - -export { SessionApi }; - -export { SessionState }; +export const routeResolutionApiRef: ApiRef_2; // @public -export const SignInPageBlueprint: ExtensionBlueprint<{ +export type SessionApi = { + signIn(): Promise; + signOut(): Promise; + sessionState$(): Observable; +}; + +// @public +export const SessionState: { + readonly SignedIn: 'SignedIn'; + readonly SignedOut: 'SignedOut'; +}; + +// @public (undocumented) +export type SessionState = (typeof SessionState)[keyof typeof SessionState]; + +// @public (undocumented) +export namespace SessionState { + // (undocumented) + export type SignedIn = typeof SessionState.SignedIn; + // (undocumented) + export type SignedOut = typeof SessionState.SignedOut; +} + +// @public @deprecated +export const SignInPageBlueprint: ExtensionBlueprint_2<{ kind: 'sign-in-page'; params: { loader: () => Promise>; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< ComponentType, 'core.sign-in-page.component', {} @@ -1723,7 +2013,7 @@ export const SignInPageBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - component: ConfigurableExtensionDataRef< + component: ConfigurableExtensionDataRef_2< ComponentType, 'core.sign-in-page.component', {} @@ -1731,11 +2021,38 @@ export const SignInPageBlueprint: ExtensionBlueprint<{ }; }>; -export { StorageApi }; +// @public +export type SignInPageProps = { + onSignInSuccess(identityApi: IdentityApi): void; + children?: ReactNode; +}; -export { storageApiRef }; +// @public +export interface StorageApi { + forBucket(name: string): StorageApi; + observe$( + key: string, + ): Observable>; + remove(key: string): Promise; + set(key: string, data: T): Promise; + snapshot(key: string): StorageValueSnapshot; +} -export { StorageValueSnapshot }; +// @public +export const storageApiRef: ApiRef; + +// @public +export type StorageValueSnapshot = + | { + key: string; + presence: 'unknown' | 'absent'; + value?: undefined; + } + | { + key: string; + presence: 'present'; + value: TValue; + }; // @public export interface SubRouteRef< @@ -1749,8 +2066,8 @@ export interface SubRouteRef< readonly T: TParams; } -// @public -export const SwappableComponentBlueprint: ExtensionBlueprint<{ +// @public @deprecated +export const SwappableComponentBlueprint: ExtensionBlueprint_2<{ kind: 'component'; params: >(params: { component: Ref extends SwappableComponentRef< @@ -1766,7 +2083,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ | (() => (props: IInnerComponentProps) => JSX.Element | null) | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never; - }) => ExtensionBlueprintParams<{ + }) => ExtensionBlueprintParams_2<{ component: Ref extends SwappableComponentRef< any, infer IExternalComponentProps @@ -1781,7 +2098,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never; }>; - output: ExtensionDataRef< + output: ExtensionDataRef_2< { ref: SwappableComponentRef; loader: @@ -1795,7 +2112,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - component: ConfigurableExtensionDataRef< + component: ConfigurableExtensionDataRef_2< { ref: SwappableComponentRef; loader: @@ -1831,30 +2148,51 @@ export interface SwappableComponentsApi { } // @public -export const swappableComponentsApiRef: ApiRef; +export const swappableComponentsApiRef: ApiRef_2; -// @public -export const ThemeBlueprint: ExtensionBlueprint<{ +// @public @deprecated +export const ThemeBlueprint: ExtensionBlueprint_2<{ kind: 'theme'; params: { theme: AppTheme; }; - output: ExtensionDataRef; + output: ExtensionDataRef_2; inputs: {}; config: {}; configInput: {}; dataRefs: { - theme: ConfigurableExtensionDataRef; + theme: ConfigurableExtensionDataRef_2; }; }>; -// @public -export const TranslationBlueprint: ExtensionBlueprint<{ +// @public (undocumented) +export type TranslationApi = { + getTranslation< + TMessages extends { + [key in string]: string; + }, + >( + translationRef: TranslationRef, + ): TranslationSnapshot; + translation$< + TMessages extends { + [key in string]: string; + }, + >( + translationRef: TranslationRef, + ): Observable>; +}; + +// @public (undocumented) +export const translationApiRef: ApiRef; + +// @public @deprecated +export const TranslationBlueprint: ExtensionBlueprint_2<{ kind: 'translation'; params: { resource: TranslationResource | TranslationMessages; }; - output: ExtensionDataRef< + output: ExtensionDataRef_2< | TranslationResource | TranslationMessages< string, @@ -1870,7 +2208,7 @@ export const TranslationBlueprint: ExtensionBlueprint<{ config: {}; configInput: {}; dataRefs: { - translation: ConfigurableExtensionDataRef< + translation: ConfigurableExtensionDataRef_2< | TranslationResource | TranslationMessages< string, @@ -1885,26 +2223,169 @@ export const TranslationBlueprint: ExtensionBlueprint<{ }; }>; -export { TranslationMessages }; +// @public (undocumented) +export type TranslationFunction< + TMessages extends { + [key in string]: string; + }, +> = CollapsedMessages extends infer IMessages extends { + [key in string]: string; +} + ? { + ( + key: TKey, + ...[args]: TranslationFunctionOptions< + NestedMessageKeys, + PluralKeys, + IMessages, + string + > + ): IMessages[TKey]; + ( + key: TKey, + ...[args]: TranslationFunctionOptions< + NestedMessageKeys, + PluralKeys, + IMessages, + string | JSX_3.Element + > + ): JSX_3.Element; + } + : never; -export { TranslationMessagesOptions }; +// @public +export interface TranslationMessages< + TId extends string = string, + TMessages extends { + [key in string]: string; + } = { + [key in string]: string; + }, + TFull extends boolean = boolean, +> { + // (undocumented) + $$type: '@backstage/TranslationMessages'; + full: TFull; + id: TId; + messages: TMessages; +} -export { TranslationRef }; +// @public +export interface TranslationMessagesOptions< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TFull extends boolean, +> { + // (undocumented) + full?: TFull; + // (undocumented) + messages: false extends TFull + ? { + [key in keyof TMessages]?: string | null; + } + : { + [key in keyof TMessages]: string | null; + }; + // (undocumented) + ref: TranslationRef; +} -export { TranslationRefOptions }; +// @public (undocumented) +export interface TranslationRef< + TId extends string = string, + TMessages extends { + [key in string]: string; + } = { + [key in string]: string; + }, +> { + // (undocumented) + $$type: '@backstage/TranslationRef'; + // (undocumented) + id: TId; + // (undocumented) + T: TMessages; +} -export { TranslationResource }; +// @public (undocumented) +export interface TranslationRefOptions< + TId extends string, + TNestedMessages extends AnyNestedMessages, + TTranslations extends { + [language in string]: () => Promise<{ + default: { + [key in keyof FlattenedMessages]: string | null; + }; + }>; + }, +> { + // (undocumented) + id: TId; + // (undocumented) + messages: TNestedMessages; + // (undocumented) + translations?: TTranslations; +} -export { TranslationResourceOptions }; +// @public (undocumented) +export interface TranslationResource { + // (undocumented) + $$type: '@backstage/TranslationResource'; + // (undocumented) + id: TId; +} -export { TypesToApiRefs }; +// @public (undocumented) +export interface TranslationResourceOptions< + TId extends string, + TMessages extends { + [key in string]: string; + }, + TTranslations extends { + [language in string]: () => Promise<{ + default: + | TranslationMessages + | { + [key in keyof TMessages]: string | null; + }; + }>; + }, +> { + // (undocumented) + ref: TranslationRef; + // (undocumented) + translations: TTranslations; +} + +// @public (undocumented) +export type TranslationSnapshot< + TMessages extends { + [key in string]: string; + }, +> = + | { + ready: false; + } + | { + ready: true; + t: TranslationFunction; + }; + +// @public +export type TypesToApiRefs = { + [key in keyof T]: ApiRef; +}; // @public export function useAnalytics(): AnalyticsTracker; -export { useApi }; +// @public +export function useApi(apiRef: ApiRef): T; -export { useApiHolder }; +// @public +export function useApiHolder(): ApiHolder; // @public export function useAppNode(): AppNode | undefined; @@ -1922,9 +2403,29 @@ export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params; -export { useTranslationRef }; +// @public (undocumented) +export const useTranslationRef: ( + translationRef: TranslationRef, +) => { + t: TranslationFunction; +}; -export { vmwareCloudAuthApiRef }; +// @public +export const vmwareCloudAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>; -export { withApis }; +// @public +export function withApis( + apis: TypesToApiRefs, +): ( + WrappedComponent: ComponentType, +) => { + (props: PropsWithChildren>): JSX_2.Element; + displayName: string; +}; ``` diff --git a/packages/core-plugin-api/src/apis/definitions/alpha.ts b/packages/frontend-plugin-api/src/alpha.ts similarity index 70% rename from packages/core-plugin-api/src/apis/definitions/alpha.ts rename to packages/frontend-plugin-api/src/alpha.ts index e7f6f13fef..2251bfafb7 100644 --- a/packages/core-plugin-api/src/apis/definitions/alpha.ts +++ b/packages/frontend-plugin-api/src/alpha.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export { PluginWrapperBlueprint } from './blueprints/PluginWrapperBlueprint'; export { - translationApiRef, - type TranslationApi, - type TranslationFunction, - type TranslationSnapshot, -} from './TranslationApi'; -export { appLanguageApiRef, type AppLanguageApi } from './AppLanguageApi'; + type PluginWrapperApi, + pluginWrapperApiRef, +} from './apis/definitions/PluginWrapperApi'; diff --git a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx index 547c78a7fc..18fe64af30 100644 --- a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx +++ b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx @@ -16,7 +16,7 @@ import { renderHook } from '@testing-library/react'; import { useAnalytics } from './useAnalytics'; -import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { analyticsApiRef } from '../apis/definitions/AnalyticsApi'; import { TestApiProvider } from '@backstage/test-utils'; describe('useAnalytics', () => { diff --git a/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx b/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx index 397906d1d1..5c5f2ad60a 100644 --- a/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx +++ b/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useApi } from '@backstage/core-plugin-api'; +import { useApi } from '../apis/system'; import { useAnalyticsContext } from './AnalyticsContext'; import { analyticsApiRef, AnalyticsTracker, AnalyticsApi } from '../apis'; import { useRef } from 'react'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts index 649a930b50..afdcb6a617 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts @@ -14,8 +14,43 @@ * limitations under the License. */ -export { - type AlertApi, - type AlertMessage, - alertApiRef, -} from '@backstage/core-plugin-api'; +import { createApiRef, ApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Message handled by the {@link AlertApi}. + * + * @public + */ +export type AlertMessage = { + message: string; + // Severity will default to success since that is what material ui defaults the value to. + severity?: 'success' | 'info' | 'warning' | 'error'; + display?: 'permanent' | 'transient'; +}; + +/** + * The alert API is used to report alerts to the app, and display them to the user. + * + * @public + */ +export type AlertApi = { + /** + * Post an alert for handling by the application. + */ + post(alert: AlertMessage): void; + + /** + * Observe alerts posted by other parts of the application. + */ + alert$(): Observable; +}; + +/** + * The {@link ApiRef} of {@link AlertApi}. + * + * @public + */ +export const alertApiRef: ApiRef = createApiRef({ + id: 'core.alert', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts index 2040966464..aa1f08ffbe 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; import { AnalyticsContextValue } from '../../analytics/types'; -import type { AnalyticsImplementationBlueprint } from '../../blueprints/'; /** * Represents an event worth tracking in an analytics system that could inform diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts index 37ffcfa820..36b97f9fff 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts @@ -14,7 +14,23 @@ * limitations under the License. */ -export { - type AppLanguageApi, - appLanguageApiRef, -} from '@backstage/core-plugin-api/alpha'; +import { ApiRef, createApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** @public */ +export type AppLanguageApi = { + getAvailableLanguages(): { languages: string[] }; + + setLanguage(language?: string): void; + + getLanguage(): { language: string }; + + language$(): Observable<{ language: string }>; +}; + +/** + * @public + */ +export const appLanguageApiRef: ApiRef = createApiRef({ + id: 'core.applanguage', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts index ce62cd72c3..e771fad597 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,8 +14,74 @@ * limitations under the License. */ -export { - type AppTheme, - type AppThemeApi, - appThemeApiRef, -} from '@backstage/core-plugin-api'; +import { ReactNode } from 'react'; +import { ApiRef, createApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Describes a theme provided by the app. + * + * @public + */ +export type AppTheme = { + /** + * ID used to remember theme selections. + */ + id: string; + + /** + * Title of the theme + */ + title: string; + + /** + * Theme variant + */ + variant: 'light' | 'dark'; + + /** + * An Icon for the theme mode setting. + */ + icon?: React.ReactElement; + + Provider(props: { children: ReactNode }): JSX.Element | null; +}; + +/** + * The AppThemeApi gives access to the current app theme, and allows switching + * to other options that have been registered as a part of the App. + * + * @public + */ +export type AppThemeApi = { + /** + * Get a list of available themes. + */ + getInstalledThemes(): AppTheme[]; + + /** + * Observe the currently selected theme. A value of undefined means no specific theme has been selected. + */ + activeThemeId$(): Observable; + + /** + * Get the current theme ID. Returns undefined if no specific theme is selected. + */ + getActiveThemeId(): string | undefined; + + /** + * Set a specific theme to use in the app, overriding the default theme selection. + * + * Clear the selection by passing in undefined. + */ + setActiveThemeId(themeId?: string): void; +}; + +/** + * The {@link ApiRef} of {@link AppThemeApi}. + * + * @public + */ +export const appThemeApiRef: ApiRef = createApiRef({ + id: 'core.apptheme', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index f4fd2f3a6f..89902d2727 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core-plugin-api'; -import { - FrontendPlugin, - Extension, - ExtensionDataRef, - ExtensionAttachToSpec, -} from '../../wiring'; +import { createApiRef } from '../system'; +import { FrontendPlugin, Extension, ExtensionDataRef } from '../../wiring'; +import { ExtensionAttachTo } from '../../wiring/resolveExtensionDefinition'; /** * The specification for this {@link AppNode} in the {@link AppTree}. @@ -33,7 +29,7 @@ import { */ export interface AppNodeSpec { readonly id: string; - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionAttachTo; readonly extension: Extension; readonly disabled: boolean; readonly config?: unknown; diff --git a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts index 968a2843ba..f935dfa3af 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts @@ -13,5 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiRef, createApiRef } from '../system'; +import type { Config } from '@backstage/config'; -export { type ConfigApi, configApiRef } from '@backstage/core-plugin-api'; +/** + * The Config API is used to provide a mechanism to access the + * runtime configuration of the system. + * + * @public + */ +export type ConfigApi = Config; + +/** + * The {@link ApiRef} of {@link ConfigApi}. + * + * @public + */ +export const configApiRef: ApiRef = createApiRef({ + id: 'core.config', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts index 801ec88c69..85151b05f6 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core-plugin-api'; +import { createApiRef } from '../system'; /** * A handle for an open dialog that can be used to interact with it. diff --git a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts index 98b3f0bbf0..d23fe3db6c 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -13,5 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiRef, createApiRef } from '../system'; -export { type DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +/** + * The discovery API is used to provide a mechanism for plugins to + * discover the endpoint to use to talk to their backend counterpart. + * + * @remarks + * + * The purpose of the discovery API is to allow for many different deployment + * setups and routing methods through a central configuration, instead + * of letting each individual plugin manage that configuration. + * + * Implementations of the discovery API can be a simple as a URL pattern + * using the pluginId, but could also have overrides for individual plugins, + * or query a separate discovery service. + * + * @public + */ +export type DiscoveryApi = { + /** + * Returns the HTTP base backend URL for a given plugin, without a trailing slash. + * + * This method must always be called just before making a request, as opposed to + * fetching the URL when constructing an API client. That is to ensure that more + * flexible routing patterns can be supported. + * + * For example, asking for the URL for `auth` may return something + * like `https://backstage.example.com/api/auth` + */ + getBaseUrl(pluginId: string): Promise; +}; + +/** + * The {@link ApiRef} of {@link DiscoveryApi}. + * + * @public + */ +export const discoveryApiRef: ApiRef = createApiRef({ + id: 'core.discovery', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts index 57782a05dc..9c73d94cac 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts @@ -14,9 +14,78 @@ * limitations under the License. */ -export { - type ErrorApiError, - type ErrorApiErrorContext, - type ErrorApi, - errorApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; +import { Observable } from '@backstage/types'; + +/** + * Mirrors the JavaScript Error class, for the purpose of + * providing documentation and optional fields. + * + * @public + */ +export type ErrorApiError = { + name: string; + message: string; + stack?: string; +}; + +/** + * Provides additional information about an error that was posted to the application. + * + * @public + */ +export type ErrorApiErrorContext = { + /** + * If set to true, this error should not be displayed to the user. + * + * Hidden errors are typically not displayed in the UI, but the ErrorApi + * implementation may still report them to error tracking services + * or other utilities that care about all errors. + * + * @defaultValue false + */ + hidden?: boolean; +}; + +/** + * The error API is used to report errors to the app, and display them to the user. + * + * @remarks + * + * Plugins can use this API as a method of displaying errors to the user, but also + * to report errors for collection by error reporting services. + * + * If an error can be displayed inline, e.g. as feedback in a form, that should be + * preferred over relying on this API to display the error. The main use of this API + * for displaying errors should be for asynchronous errors, such as a failing background process. + * + * Even if an error is displayed inline, it should still be reported through this API + * if it would be useful to collect or log it for debugging purposes, but with + * the hidden flag set. For example, an error arising from form field validation + * should probably not be reported, while a failed REST call would be useful to report. + * + * @public + */ +export type ErrorApi = { + /** + * Post an error for handling by the application. + */ + post(error: ErrorApiError, context?: ErrorApiErrorContext): void; + + /** + * Observe errors posted by other parts of the application. + */ + error$(): Observable<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; +}; + +/** + * The {@link ApiRef} of {@link ErrorApi}. + * + * @public + */ +export const errorApiRef: ApiRef = createApiRef({ + id: 'core.error', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index 820efc0a25..d4429975cc 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -13,11 +13,114 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* We want to maintain the same information as an enum, so we disable the redeclaration warning */ +/* eslint-disable @typescript-eslint/no-redeclare */ -export { - type FeatureFlag, - type FeatureFlagState, - type FeatureFlagsSaveOptions, - type FeatureFlagsApi, - featureFlagsApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; + +/** + * Feature flag descriptor. + * + * @public + */ +export type FeatureFlag = { + name: string; + pluginId: string; + description?: string; +}; + +/** + * Enum representing the state of a feature flag (inactive/active). + * + * @public + */ +export const FeatureFlagState = { + /** + * Feature flag inactive (disabled). + */ + None: 0, + /** + * Feature flag active (enabled). + */ + Active: 1, +} as const; + +/** + * @public + */ +export type FeatureFlagState = + (typeof FeatureFlagState)[keyof typeof FeatureFlagState]; + +/** + * @public + */ +export namespace FeatureFlagState { + export type None = typeof FeatureFlagState.None; + export type Active = typeof FeatureFlagState.Active; +} + +/** + * Options to use when saving feature flags. + * + * @public + */ +export type FeatureFlagsSaveOptions = { + /** + * The new feature flag states to save. + */ + states: Record; + + /** + * Whether the saves states should be merged into the existing ones, or replace them. + * + * Defaults to false. + */ + merge?: boolean; +}; + +/** + * The feature flags API is used to toggle functionality to users across plugins and Backstage. + * + * @remarks + * + * Plugins can use this API to register feature flags that they have available + * for users to enable/disable, and this API will centralize the current user's + * state of which feature flags they would like to enable. + * + * This is ideal for Backstage plugins, as well as your own App, to trial incomplete + * or unstable upcoming features. Although there will be a common interface for users + * to enable and disable feature flags, this API acts as another way to enable/disable. + * + * @public + */ +export interface FeatureFlagsApi { + /** + * Registers a new feature flag. Once a feature flag has been registered it + * can be toggled by users, and read back to enable or disable features. + */ + registerFlag(flag: FeatureFlag): void; + + /** + * Get a list of all registered flags. + */ + getRegisteredFlags(): FeatureFlag[]; + + /** + * Whether the feature flag with the given name is currently activated for the user. + */ + isActive(name: string): boolean; + + /** + * Save the user's choice of feature flag states. + */ + save(options: FeatureFlagsSaveOptions): void; +} + +/** + * The {@link ApiRef} of {@link FeatureFlagsApi}. + * + * @public + */ +export const featureFlagsApiRef: ApiRef = createApiRef({ + id: 'core.featureflags', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts index bcc9bc6a91..aba0e53bb7 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,4 +14,38 @@ * limitations under the License. */ -export { type FetchApi, fetchApiRef } from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export type FetchApi = { + /** + * The `fetch` implementation. + */ + fetch: typeof fetch; +}; + +/** + * The {@link ApiRef} of {@link FetchApi}. + * + * @remarks + * + * This is a wrapper for the fetch API, that has additional behaviors such as + * the ability to automatically inject auth information where necessary. + * + * Note that the default behavior of this API (unless overridden by your org), + * is to require that the user is already signed in so that it has auth + * information to inject. Therefore, using the default implementation of this + * utility API e.g. on the `SignInPage` or similar, would cause issues. In + * special circumstances like those, you can use the regular system `fetch` + * instead. + * + * @public + */ +export const fetchApiRef: ApiRef = createApiRef({ + id: 'core.fetch', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts index aa5b7f4fac..fbc9928dc1 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core-plugin-api'; +import { createApiRef } from '../system'; import { IconComponent } from '../../icons'; /** diff --git a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts index 03503a8fba..1b127a1971 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts @@ -13,5 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiRef, createApiRef } from '../system'; +import { BackstageUserIdentity, ProfileInfo } from './auth'; -export { type IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +/** + * The Identity API used to identify and get information about the signed in user. + * + * @public + */ +export type IdentityApi = { + /** + * The profile of the signed in user. + */ + getProfileInfo(): Promise; + + /** + * User identity information within Backstage. + */ + getBackstageIdentity(): Promise; + + /** + * Provides credentials in the form of a token which proves the identity of the signed in user. + * + * The token will be undefined if the signed in user does not have a verified + * identity, such as a demo user or mocked user for e2e tests. + */ + getCredentials(): Promise<{ token?: string }>; + + /** + * Sign out the current user + */ + signOut(): Promise; +}; + +/** + * The {@link ApiRef} of {@link IdentityApi}. + * + * @public + */ +export const identityApiRef: ApiRef = createApiRef({ + id: 'core.identity', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts index ff7a0f2852..75bf3a3864 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,10 +14,118 @@ * limitations under the License. */ -export { - type OAuthRequesterOptions, - type OAuthRequester, - type PendingOAuthRequest, - type OAuthRequestApi, - oauthRequestApiRef, -} from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; +import { ApiRef, createApiRef } from '../system'; +import { AuthProviderInfo } from './auth'; + +/** + * Describes how to handle auth requests. Both how to show them to the user, and what to do when + * the user accesses the auth request. + * + * @public + */ +export type OAuthRequesterOptions = { + /** + * Information about the auth provider, which will be forwarded to auth requests. + */ + provider: AuthProviderInfo; + + /** + * Implementation of the auth flow, which will be called synchronously when + * trigger() is called on an auth requests. + */ + onAuthRequest(scopes: Set): Promise; +}; + +/** + * Function used to trigger new auth requests for a set of scopes. + * + * @remarks + * + * The returned promise will resolve to the same value returned by the onAuthRequest in the + * {@link OAuthRequesterOptions}. Or rejected, if the request is rejected. + * + * This function can be called multiple times before the promise resolves. All calls + * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the + * union of all requested scopes. + * + * @public + */ +export type OAuthRequester = ( + scopes: Set, +) => Promise; + +/** + * An pending auth request for a single auth provider. The request will remain in this pending + * state until either reject() or trigger() is called. + * + * @remarks + * + * Any new requests for the same provider are merged into the existing pending request, meaning + * there will only ever be a single pending request for a given provider. + * + * @public + */ +export type PendingOAuthRequest = { + /** + * Information about the auth provider, as given in the AuthRequesterOptions + */ + provider: AuthProviderInfo; + + /** + * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". + */ + reject(): void; + + /** + * Trigger the auth request to continue the auth flow, by for example showing a popup. + * + * Synchronously calls onAuthRequest with all scope currently in the request. + */ + trigger(): Promise; +}; + +/** + * Provides helpers for implemented OAuth login flows within Backstage. + * + * @public + */ +export type OAuthRequestApi = { + /** + * A utility for showing login popups or similar things, and merging together multiple requests for + * different scopes into one request that includes all scopes. + * + * The passed in options provide information about the login provider, and how to handle auth requests. + * + * The returned AuthRequester function is used to request login with new scopes. These requests + * are merged together and forwarded to the auth handler, as soon as a consumer of auth requests + * triggers an auth flow. + * + * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. + */ + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; + + /** + * Observers pending auth requests. The returned observable will emit all + * current active auth request, at most one for each created auth requester. + * + * Each request has its own info about the login provider, forwarded from the auth requester options. + * + * Depending on user interaction, the request should either be rejected, or used to trigger the auth handler. + * If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError". + * If a auth is triggered, and the auth handler resolves successfully, then all currently pending + * AuthRequester calls will resolve to the value returned by the onAuthRequest call. + */ + authRequest$(): Observable; +}; + +/** + * The {@link ApiRef} of {@link OAuthRequestApi}. + * + * @public + */ +export const oauthRequestApiRef: ApiRef = createApiRef({ + id: 'core.oauthrequest', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts new file mode 100644 index 0000000000..a4b66d6128 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType, ReactNode } from 'react'; +import { createApiRef } from '@backstage/frontend-plugin-api'; + +/** + * The Plugin Wrapper API is used to wrap plugin extensions with providers, + * plugins should generally use `ExtensionBoundary` instead. + * + * @remarks + * + * This API is primarily intended for internal use by the Backstage frontend + * system, but can be used for advanced use-cases. If you do override it, be + * sure to include the default implementation as well. + * + * @alpha + */ +export type PluginWrapperApi = { + /** + * Returns a wrapper component for a specific plugin, or undefined if no + * wrappers exist. Do not use this API directly, instead use + * `ExtensionBoundary` to wrap your plugin components if needed. + */ + getPluginWrapper( + pluginId: string, + ): ComponentType<{ children: ReactNode }> | undefined; +}; + +/** + * The API reference of {@link PluginWrapperApi}. + * + * @alpha + */ +export const pluginWrapperApiRef = createApiRef({ + id: 'core.plugin-wrapper.alpha', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts index 6ae25a814e..0c3ca9c4cf 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts @@ -20,7 +20,7 @@ import { SubRouteRef, ExternalRouteRef, } from '../../routing'; -import { createApiRef } from '@backstage/core-plugin-api'; +import { createApiRef } from '../system'; /** * TS magic for handling route parameters. diff --git a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts index 2309d2414c..1506e5f143 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts @@ -14,8 +14,97 @@ * limitations under the License. */ -export { - type StorageValueSnapshot, - type StorageApi, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; +import { JsonValue, Observable } from '@backstage/types'; + +/** + * A snapshot in time of the current known value of a storage key. + * + * @public + */ +export type StorageValueSnapshot = + | { + key: string; + presence: 'unknown' | 'absent'; + value?: undefined; + } + | { + key: string; + presence: 'present'; + value: TValue; + }; + +/** + * Provides a key-value persistence API. + * + * @public + */ +export interface StorageApi { + /** + * Create a bucket to store data in. + * + * @param name - Namespace for the storage to be stored under, + * will inherit previous namespaces too + */ + forBucket(name: string): StorageApi; + + /** + * Remove persistent data. + * + * @param key - Unique key associated with the data. + */ + remove(key: string): Promise; + + /** + * Save persistent data, and emit messages to anyone that is using + * {@link StorageApi.observe$} for this key. + * + * @param key - Unique key associated with the data. + * @param data - The data to be stored under the key. + */ + set(key: string, data: T): Promise; + + /** + * Observe the value over time for a particular key in the current bucket. + * + * @remarks + * + * The observable will only emit values when the value changes in the underlying + * storage, although multiple values with the same shape may be emitted in a row. + * + * If a {@link StorageApi.snapshot} of a key is retrieved and the presence is + * `'unknown'`, then you are guaranteed to receive a snapshot with a known + * presence, as long as you observe the key within the same tick. + * + * Since the emitted values are shared across all subscribers, it is important + * not to mutate the returned values. The values may be frozen as a precaution. + * + * @param key - Unique key associated with the data + */ + observe$( + key: string, + ): Observable>; + + /** + * Returns an immediate snapshot value for the given key, if possible. + * + * @remarks + * + * Combine with {@link StorageApi.observe$} to get notified of value changes. + * + * Note that this method is synchronous, and some underlying storages may be + * unable to retrieve a value using this method - the result may or may not + * consistently have a presence of 'unknown'. Use {@link StorageApi.observe$} + * to be sure to receive an actual value eventually. + */ + snapshot(key: string): StorageValueSnapshot; +} + +/** + * The {@link ApiRef} of {@link StorageApi}. + * + * @public + */ +export const storageApiRef: ApiRef = createApiRef({ + id: 'core.storage', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts index ed2b20ce80..09dff04f43 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts @@ -15,7 +15,7 @@ */ import { SwappableComponentRef } from '../../components'; -import { createApiRef } from '@backstage/core-plugin-api'; +import { createApiRef } from '../system'; /** * API for looking up components based on component refs. diff --git a/packages/core-plugin-api/src/apis/definitions/TranslationApi.test.tsx b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.test.tsx similarity index 100% rename from packages/core-plugin-api/src/apis/definitions/TranslationApi.test.tsx rename to packages/frontend-plugin-api/src/apis/definitions/TranslationApi.test.tsx diff --git a/packages/core-plugin-api/src/apis/definitions/TranslationApi.ts b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts similarity index 98% rename from packages/core-plugin-api/src/apis/definitions/TranslationApi.ts rename to packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts index 5cfb14abb2..2578eaf7c4 100644 --- a/packages/core-plugin-api/src/apis/definitions/TranslationApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; import { Expand, ExpandRecursive, Observable } from '@backstage/types'; -import { TranslationRef } from '../../translation/TranslationRef'; +import { TranslationRef } from '../../translation'; import { JSX } from 'react'; /** @@ -305,7 +305,7 @@ type TranslationFunctionOptions< > >; -/** @alpha */ +/** @public */ export type TranslationFunction = CollapsedMessages extends infer IMessages extends { [key in string]: string; @@ -340,11 +340,11 @@ export type TranslationFunction = } : never; -/** @alpha */ +/** @public */ export type TranslationSnapshot = { ready: false } | { ready: true; t: TranslationFunction }; -/** @alpha */ +/** @public */ export type TranslationApi = { getTranslation( translationRef: TranslationRef, @@ -356,7 +356,7 @@ export type TranslationApi = { }; /** - * @alpha + * @public */ export const translationApiRef: ApiRef = createApiRef({ id: 'core.translation', diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 5a31c1603d..9a0ddcd958 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -13,29 +13,496 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* We want to maintain the same information as an enum, so we disable the redeclaration warning */ +/* eslint-disable @typescript-eslint/no-redeclare */ -export { - type BackstageIdentityApi, - type BackstageIdentityResponse, - type BackstageUserIdentity, - type AuthProviderInfo, - type AuthRequestOptions, - type OAuthScope, - type OAuthApi, - type OpenIdConnectApi, - type ProfileInfoApi, - type ProfileInfo, - type SessionApi, - SessionState, - atlassianAuthApiRef, - bitbucketAuthApiRef, - bitbucketServerAuthApiRef, - githubAuthApiRef, - gitlabAuthApiRef, - googleAuthApiRef, - oktaAuthApiRef, - microsoftAuthApiRef, - oneloginAuthApiRef, - vmwareCloudAuthApiRef, - openshiftAuthApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, createApiRef } from '../system'; +import { IconComponent } from '../../icons/types'; +import { Observable } from '@backstage/types'; + +/** + * This file contains declarations for common interfaces of auth-related APIs. + * The declarations should be used to signal which type of authentication and + * authorization methods each separate auth provider supports. + * + * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, + * would be declared as follows: + * + * const googleAuthApiRef = createApiRef({ ... }) + */ + +/** + * Information about the auth provider. + * + * @remarks + * + * This information is used both to connect the correct auth provider in the backend, as + * well as displaying the provider to the user. + * + * @public + */ +export type AuthProviderInfo = { + /** + * The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`. + */ + id: string; + + /** + * Title for the auth provider, for example "GitHub" + */ + title: string; + + /** + * Icon for the auth provider. + */ + icon: IconComponent; + + /** + * Optional user friendly messaage to display for the auth provider. + */ + message?: string; +}; + +/** + * An array of scopes, or a scope string formatted according to the + * auth provider, which is typically a space separated list. + * + * @remarks + * + * See the documentation for each auth provider for the list of scopes + * supported by each provider. + * + * @public + */ +export type OAuthScope = string | string[]; + +/** + * Configuration of an authentication request. + * + * @public + */ +export type AuthRequestOptions = { + /** + * If this is set to true, the user will not be prompted to log in, + * and an empty response will be returned if there is no existing session. + * + * This can be used to perform a check whether the user is logged in, or if you don't + * want to force a user to be logged in, but provide functionality if they already are. + * + * @defaultValue false + */ + optional?: boolean; + + /** + * If this is set to true, the request will bypass the regular oauth login modal + * and open the login popup directly. + * + * The method must be called synchronously from a user action for this to work in all browsers. + * + * @defaultValue false + */ + instantPopup?: boolean; +}; + +/** + * This API provides access to OAuth 2 credentials. It lets you request access tokens, + * which can be used to act on behalf of the user when talking to APIs. + * + * @public + */ +export type OAuthApi = { + /** + * Requests an OAuth 2 Access Token, optionally with a set of scopes. The access token allows + * you to make requests on behalf of the user, and the copes may grant you broader access, depending + * on the auth provider. + * + * Each auth provider has separate handling of scope, so you need to look at the documentation + * for each one to know what scope you need to request. + * + * This method is cheap and should be called each time an access token is used. Do not for example + * store the access token in React component state, as that could cause the token to expire. Instead + * fetch a new access token for each request. + * + * Be sure to include all required scopes when requesting an access token. When testing your implementation + * it is best to log out the Backstage session and then visit your plugin page directly, as + * you might already have some required scopes in your existing session. Not requesting the correct + * scopes can lead to 403 or other authorization errors, which can be tricky to debug. + * + * If the user has not yet granted access to the provider and the set of requested scopes, the user + * will be prompted to log in. The returned promise will not resolve until the user has + * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. + */ + getAccessToken( + scope?: OAuthScope, + options?: AuthRequestOptions, + ): Promise; +}; + +/** + * This API provides access to OpenID Connect credentials. It lets you request ID tokens, + * which can be passed to backend services to prove the user's identity. + * + * @public + */ +export type OpenIdConnectApi = { + /** + * Requests an OpenID Connect ID Token. + * + * This method is cheap and should be called each time an ID token is used. Do not for example + * store the id token in React component state, as that could cause the token to expire. Instead + * fetch a new id token for each request. + * + * If the user has not yet logged in to Google inside Backstage, the user will be prompted + * to log in. The returned promise will not resolve until the user has successfully logged in. + * The returned promise can be rejected, but only if the user rejects the login request. + */ + getIdToken(options?: AuthRequestOptions): Promise; +}; + +/** + * This API provides access to profile information of the user from an auth provider. + * + * @public + */ +export type ProfileInfoApi = { + /** + * Get profile information for the user as supplied by this auth provider. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. + */ + getProfile(options?: AuthRequestOptions): Promise; +}; + +/** + * This API provides access to the user's identity within Backstage. + * + * @remarks + * + * An auth provider that implements this interface can be used to sign-in to backstage. It is + * not intended to be used directly from a plugin, but instead serves as a connection between + * this authentication method and the app's {@link IdentityApi} + * + * @public + */ +export type BackstageIdentityApi = { + /** + * Get the user's identity within Backstage. This should normally not be called directly, + * use the {@link IdentityApi} instead. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. + */ + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + +/** + * Token and Identity response, with the users claims in the Identity. + * + * @public + */ +export type BackstageIdentityResponse = { + /** + * The token used to authenticate the user within Backstage. + */ + token: string; + + /** + * The time at which the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresAt?: Date; + + /** + * Identity information derived from the token. + */ + identity: BackstageUserIdentity; +}; + +/** + * Profile information of the user. + * + * @public + */ +export type ProfileInfo = { + /** + * Email ID. + */ + email?: string; + + /** + * Display name that can be presented to the user. + */ + displayName?: string; + + /** + * URL to an avatar image of the user. + */ + picture?: string; +}; + +/** + * Session state values passed to subscribers of the SessionApi. + * + * @public + */ +export const SessionState = { + /** + * User signed in. + */ + SignedIn: 'SignedIn', + /** + * User not signed in. + */ + SignedOut: 'SignedOut', +} as const; + +/** + * @public + */ +export type SessionState = (typeof SessionState)[keyof typeof SessionState]; + +/** + * @public + */ +export namespace SessionState { + export type SignedIn = typeof SessionState.SignedIn; + export type SignedOut = typeof SessionState.SignedOut; +} + +/** + * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. + * + * @public + */ +export type SessionApi = { + /** + * Sign in with a minimum set of permissions. + */ + signIn(): Promise; + + /** + * Sign out from the current session. This will reload the page. + */ + signOut(): Promise; + + /** + * Observe the current state of the auth session. Emits the current state on subscription. + */ + sessionState$(): Observable; +}; + +/** + * Provides authentication towards Google APIs and identities. + * + * @public + * @remarks + * + * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. + * + * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, + * email and expiration information. Do not rely on any other fields, as they might not be present. + */ +export const googleAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.google', +}); + +/** + * Provides authentication towards GitHub APIs. + * + * @public + * @remarks + * + * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} + * for a full list of supported scopes. + */ +export const githubAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.github', +}); + +/** + * Provides authentication towards Okta APIs. + * + * @public + * @remarks + * + * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} + * for a full list of supported scopes. + */ +export const oktaAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.okta', +}); + +/** + * Provides authentication towards GitLab APIs. + * + * @public + * @remarks + * + * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} + * for a full list of supported scopes. + */ +export const gitlabAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.gitlab', +}); + +/** + * Provides authentication towards Microsoft APIs and identities. + * + * @public + * @remarks + * + * For more info and a full list of supported scopes, see: + * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} + * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} + */ +export const microsoftAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.microsoft', +}); + +/** + * Provides authentication towards OneLogin APIs. + * + * @public + */ +export const oneloginAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.onelogin', +}); + +/** + * Provides authentication towards Bitbucket APIs. + * + * @public + * @remarks + * + * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} + * for a full list of supported scopes. + */ +export const bitbucketAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + 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. + * + * @public + * @remarks + * + * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} + * for a full list of supported scopes. + */ +export const atlassianAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.atlassian', +}); + +/** + * Provides authentication towards VMware Cloud APIs and identities. + * + * @public + * @remarks + * + * For more info about VMware Cloud identity and access management: + * - {@link https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-53D39337-D93A-4B84-BD18-DDF43C21479A.html} + */ +export const vmwareCloudAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.vmware-cloud', +}); + +/** + * Provides authentication towards OpenShift APIs and identities. + * + * @public + * @remarks + * + * See {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients} + * on how to configure the OAuth clients and + * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} + * for available scopes. + */ +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.openshift', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index 7533481d01..5e7f713fcf 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -33,6 +33,7 @@ export { export * from './auth'; export * from './AlertApi'; +export * from './AppLanguageApi'; export * from './AppThemeApi'; export * from './SwappableComponentsApi'; export * from './ConfigApi'; @@ -47,3 +48,4 @@ export * from './OAuthRequestApi'; export * from './RouteResolutionApi'; export * from './StorageApi'; export * from './AnalyticsApi'; +export * from './TranslationApi'; diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts new file mode 100644 index 0000000000..dab872236f --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { createApiRef } from './ApiRef'; + +describe('ApiRef', () => { + it('should be created', () => { + const ref = createApiRef({ id: 'abc' }); + expect(ref.id).toBe('abc'); + expect(String(ref)).toBe('apiRef{abc}'); + expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); + }); + + it('should reject invalid ids', () => { + for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) { + expect(createApiRef({ id }).id).toBe(id); + } + + for (const id of [ + '123', + 'ab-3', + 'ab_c', + '.', + '2ac', + 'ab.3a', + '.abc', + 'abc.', + 'ab..s', + '', + '_', + ]) { + expect(() => createApiRef({ id }).id).toThrow( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, + ); + } + }); +}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts index 0d1f8312cb..0db3d89bd5 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts @@ -14,8 +14,59 @@ * limitations under the License. */ -export { - type ApiRef, - type ApiRefConfig, - createApiRef, -} from '@backstage/core-plugin-api'; +import type { ApiRef } from './types'; + +/** + * API reference configuration - holds an ID of the referenced API. + * + * @public + */ +export type ApiRefConfig = { + id: string; +}; + +class ApiRefImpl implements ApiRef { + constructor(private readonly config: ApiRefConfig) { + const valid = config.id + .split('.') + .flatMap(part => part.split('-')) + .every(part => part.match(/^[a-z][a-z0-9]*$/)); + if (!valid) { + throw new Error( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, + ); + } + } + + get id(): string { + return this.config.id; + } + + // Utility for getting type of an api, using `typeof apiRef.T` + get T(): T { + throw new Error(`tried to read ApiRef.T of ${this}`); + } + + toString() { + return `apiRef{${this.config.id}}`; + } +} + +/** + * Creates a reference to an API. The provided `id` is a stable identifier for + * the API implementation. + * + * @remarks + * + * The frontend system infers the owning plugin for an API from the `id`. The + * recommended pattern is `plugin..*` (for example, + * `plugin.catalog.entity-presentation`). This ensures that other plugins can't + * mistakenly override your API implementation. + * + * @param config - The descriptor of the API to reference. + * @returns An API reference. + * @public + */ +export function createApiRef(config: ApiRefConfig): ApiRef { + return new ApiRefImpl(config); +} diff --git a/packages/frontend-plugin-api/src/apis/system/helpers.ts b/packages/frontend-plugin-api/src/apis/system/helpers.ts index 5de265be99..eaffed3003 100644 --- a/packages/frontend-plugin-api/src/apis/system/helpers.ts +++ b/packages/frontend-plugin-api/src/apis/system/helpers.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. @@ -14,4 +14,61 @@ * limitations under the License. */ -export { createApiFactory } from '@backstage/core-plugin-api'; +import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; + +/** + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed + * to another function. + * + * @remarks + * + * This function doesn't actually do anything, it's only used to infer types. + * + * @public + */ +export function createApiFactory< + Api, + Impl extends Api, + Deps extends { [name in string]: unknown }, +>(factory: ApiFactory): ApiFactory; +/** + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed + * to another function. + * + * @param api - Ref of the API that will be produced by the factory. + * @param instance - Implementation of the API to use. + * @public + */ +export function createApiFactory( + api: ApiRef, + instance: Impl, +): ApiFactory; +/** + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed + * to another function. + * + * @remarks + * + * Creates factory from {@link ApiRef} or returns the factory itself if provided. + * + * @param factory - Existing factory or {@link ApiRef}. + * @param instance - The instance to be returned by the factory. + * @public + */ +export function createApiFactory< + Api, + Impl extends Api, + Deps extends { [name in string]: unknown }, +>( + factory: ApiFactory | ApiRef, + instance?: Impl, +): ApiFactory { + if ('id' in factory) { + return { + api: factory, + deps: {} as TypesToApiRefs, + factory: () => instance!, + }; + } + return factory; +} diff --git a/packages/frontend-plugin-api/src/apis/system/index.ts b/packages/frontend-plugin-api/src/apis/system/index.ts index b1bd3ac2b3..964750474e 100644 --- a/packages/frontend-plugin-api/src/apis/system/index.ts +++ b/packages/frontend-plugin-api/src/apis/system/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. diff --git a/packages/frontend-plugin-api/src/apis/system/types.ts b/packages/frontend-plugin-api/src/apis/system/types.ts index cc44e19e3f..96614c320c 100644 --- a/packages/frontend-plugin-api/src/apis/system/types.ts +++ b/packages/frontend-plugin-api/src/apis/system/types.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. @@ -14,11 +14,61 @@ * limitations under the License. */ -export type { - ApiRef, - AnyApiRef, - TypesToApiRefs, - ApiHolder, - ApiFactory, - AnyApiFactory, -} from '@backstage/core-plugin-api'; +/** + * API reference. + * + * @public + */ +export type ApiRef = { + id: string; + T: T; +}; + +/** + * Catch-all {@link ApiRef} type. + * + * @public + */ +export type AnyApiRef = ApiRef; + +/** + * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. + * + * @public + */ +export type TypesToApiRefs = { [key in keyof T]: ApiRef }; + +/** + * Provides lookup of APIs through their {@link ApiRef}s. + * + * @public + */ +export type ApiHolder = { + get(api: ApiRef): T | undefined; +}; + +/** + * Describes type returning API implementations. + * + * @public + */ +export type ApiFactory< + Api, + Impl extends Api, + Deps extends { [name in string]: unknown }, +> = { + api: ApiRef; + deps: TypesToApiRefs; + factory(deps: Deps): Impl; +}; + +/** + * Catch-all {@link ApiFactory} type. + * + * @public + */ +export type AnyApiFactory = ApiFactory< + unknown, + unknown, + { [key in string]: unknown } +>; diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx new file mode 100644 index 0000000000..4b9d80fb62 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { createVersionedContextForTesting } from '@backstage/version-bridge'; +import { createApiRef } from './ApiRef'; +import { useApi } from './useApi'; + +describe('useApi', () => { + const context = createVersionedContextForTesting('api-context'); + + afterEach(() => { + context.reset(); + }); + + it('should resolve routes', () => { + const get = jest.fn(() => 'my-api-impl'); + context.set({ 1: { get } }); + + const apiRef = createApiRef({ id: 'x' }); + const renderedHook = renderHook(() => useApi(apiRef)); + + const value = renderedHook.result.current; + expect(value).toBe('my-api-impl'); + expect(get).toHaveBeenCalledWith(apiRef); + }); +}); diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx index d2efa49fa1..30e39033f7 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. @@ -14,4 +14,81 @@ * limitations under the License. */ -export { useApiHolder, useApi, withApis } from '@backstage/core-plugin-api'; +import { ComponentType, PropsWithChildren } from 'react'; +import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; +import { useVersionedContext } from '@backstage/version-bridge'; +import { NotImplementedError } from '@backstage/errors'; + +/** + * React hook for retrieving {@link ApiHolder}, an API catalog. + * + * @public + */ +export function useApiHolder(): ApiHolder { + const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); + if (!versionedHolder) { + throw new NotImplementedError('API context is not available'); + } + + const apiHolder = versionedHolder.atVersion(1); + if (!apiHolder) { + throw new NotImplementedError('ApiContext v1 not available'); + } + return apiHolder; +} + +/** + * React hook for retrieving APIs. + * + * @param apiRef - Reference of the API to use. + * @public + */ +export function useApi(apiRef: ApiRef): T { + const apiHolder = useApiHolder(); + + const api = apiHolder.get(apiRef); + if (!api) { + throw new NotImplementedError(`No implementation available for ${apiRef}`); + } + return api; +} + +/** + * Wrapper for giving component an API context. + * + * @param apis - APIs for the context. + * @public + */ +export function withApis(apis: TypesToApiRefs) { + return function withApisWrapper( + WrappedComponent: ComponentType, + ) { + const Hoc = (props: PropsWithChildren>) => { + const apiHolder = useApiHolder(); + + const impls = {} as T; + + for (const key in apis) { + if (apis.hasOwnProperty(key)) { + const ref = apis[key]; + + const api = apiHolder.get(ref); + if (!api) { + throw new NotImplementedError( + `No implementation available for ${ref}`, + ); + } + impls[key] = api; + } + } + + return ; + }; + const displayName = + WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + Hoc.displayName = `withApis(${displayName})`; + + return Hoc; + }; +} diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts index 471e933b12..6d16a215e1 100644 --- a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -16,7 +16,7 @@ import { createExtensionInput } from '../wiring'; import { ApiBlueprint } from './ApiBlueprint'; -import { createApiRef } from '@backstage/core-plugin-api'; +import { createApiRef } from '../apis/system'; describe('ApiBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -203,10 +203,16 @@ describe('ApiBlueprint', () => { "optional": false, "singleton": false, }, + "context": { + "input": "test", + "kind": "api", + "name": "test", + }, "extensionData": [ [Function], ], "replaces": undefined, + "withContext": [Function], }, }, "kind": "api", diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx index 39f03cfad4..31d7323aec 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx @@ -14,7 +14,19 @@ * limitations under the License. */ +import { screen, waitFor } from '@testing-library/react'; +import { + MockErrorApi, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; +import { errorApiRef } from '../apis'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; import { AppRootElementBlueprint } from './AppRootElementBlueprint'; +import { ForwardedError } from '@backstage/errors'; describe('AppRootElementBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -46,4 +58,57 @@ describe('AppRootElementBlueprint', () => { } `); }); + + it('should post error to errorApi and not render children when error occurs', async () => { + const errorApi = new MockErrorApi({ collect: true }); + const errorMessage = 'Test error message'; + const ErrorComponent = () => { + throw new Error(errorMessage); + }; + + await withLogCollector(['error'], async () => { + const extension = AppRootElementBlueprint.make({ + params: { + element: , + }, + }); + + const tester = createExtensionTester(extension); + renderInTestApp( + + {tester.reactElement()} + , + ); + + await waitFor(() => { + const errors = errorApi.getErrors(); + expect(errors.length).toBeGreaterThan(0); + const postedError = errors[0].error; + expect(postedError).toBeInstanceOf(ForwardedError); + expect(postedError.message).toBe( + "Error in extension 'app-root-element:test'; caused by Error: Test error message", + ); + }); + + expect(screen.queryByText(errorMessage)).not.toBeInTheDocument(); + }); + }); + + it('should render children when there is no error', async () => { + const successMessage = 'Success!'; + const SuccessComponent = () =>
    {successMessage}
    ; + + const extension = AppRootElementBlueprint.make({ + params: { + element: , + }, + }); + + const tester = createExtensionTester(extension); + renderInTestApp(tester.reactElement()); + + await waitFor(() => { + expect(screen.getByText(successMessage)).toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx similarity index 78% rename from packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx index d1c71155f2..955abce136 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ExtensionBoundary } from '@backstage/frontend-plugin-api'; import { coreExtensionData, createExtensionBlueprint } from '../wiring'; /** @@ -26,7 +27,11 @@ export const AppRootElementBlueprint = createExtensionBlueprint({ kind: 'app-root-element', attachTo: { id: 'app/root', input: 'elements' }, output: [coreExtensionData.reactElement], - *factory(params: { element: JSX.Element }) { - yield coreExtensionData.reactElement(params.element); + *factory(params: { element: JSX.Element }, { node }) { + yield coreExtensionData.reactElement( + + {params.element} + , + ); }, }); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index 493fba05f6..184a8c3327 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -27,6 +27,9 @@ const componentDataRef = createExtensionDataRef< * and similar. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#AppRootWrapperBlueprint} instead. + * If you were using this blueprint to provide a context for your plugin, + * use `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api/alpha` instead. */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', diff --git a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts index 303d1a3fd2..2f52f7590a 100644 --- a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts @@ -21,7 +21,10 @@ const iconsDataRef = createExtensionDataRef<{ [key in string]: IconComponent; }>().with({ id: 'core.icons' }); -/** @public */ +/** + * @public + * @deprecated Use {@link @backstage/plugin-app-react#IconBundleBlueprint} instead. + */ export const IconBundleBlueprint = createExtensionBlueprint({ kind: 'icon-bundle', attachTo: { id: 'api:app/icons', input: 'icons' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts index a6524ea49e..f56de2032b 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts @@ -60,6 +60,7 @@ const componentDataRef = createExtensionDataRef().with({ * Creates an extension that replaces the entire nav bar with your own component. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#NavContentBlueprint} instead. */ export const NavContentBlueprint = createExtensionBlueprint({ kind: 'nav-content', diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index 197f72da73..ce59a9439b 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IconComponent } from '@backstage/core-plugin-api'; +import { IconComponent } from '../icons/types'; import { RouteRef } from '../routing'; import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; diff --git a/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx new file mode 100644 index 0000000000..f6f673bd93 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType, ReactNode } from 'react'; +import { + createExtensionBlueprint, + createExtensionBlueprintParams, + createExtensionDataRef, +} from '../wiring'; + +const wrapperDataRef = createExtensionDataRef< + () => Promise<{ component: ComponentType<{ children: ReactNode }> }> +>().with({ id: 'core.plugin-wrapper.loader' }); + +/** + * Creates extensions that wrap plugin extensions with providers. + * + * @alpha + */ +export const PluginWrapperBlueprint = createExtensionBlueprint({ + kind: 'plugin-wrapper', + attachTo: { id: 'api:app/plugin-wrapper', input: 'wrappers' }, + output: [wrapperDataRef], + dataRefs: { + wrapper: wrapperDataRef, + }, + defineParams(params: { + loader: () => Promise<{ + component: ComponentType<{ children: ReactNode }>; + }>; + }) { + return createExtensionBlueprintParams(params); + }, + *factory(params) { + yield wrapperDataRef(params.loader); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx index c4b7c16bd6..88096cf16a 100644 --- a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -21,7 +21,10 @@ const componentDataRef = createExtensionDataRef< (props: { children: ReactNode }) => JSX.Element | null >().with({ id: 'app.router.wrapper' }); -/** @public */ +/** + * @public + * @deprecated Use {@link @backstage/plugin-app-react#RouterBlueprint} instead. + */ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index 5e4112a8d1..9f51e8f666 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -14,10 +14,27 @@ * limitations under the License. */ -import { ComponentType, lazy } from 'react'; +import { ComponentType, lazy, ReactNode } from 'react'; import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; -import { SignInPageProps } from '@backstage/core-plugin-api'; import { ExtensionBoundary } from '../components'; +import { IdentityApi } from '../apis'; + +/** + * Props for the `SignInPage` component. + * + * @public + */ +export type SignInPageProps = { + /** + * Set the IdentityApi on successful sign-in. This should only be called once. + */ + onSignInSuccess(identityApi: IdentityApi): void; + + /** + * The children to render. + */ + children?: ReactNode; +}; const componentDataRef = createExtensionDataRef< ComponentType @@ -27,6 +44,7 @@ const componentDataRef = createExtensionDataRef< * Creates an extension that replaces the sign in page. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#SignInPageBlueprint} instead. */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', diff --git a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts index b72d14d87f..38e333826a 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts @@ -31,6 +31,7 @@ export const componentDataRef = createExtensionDataRef<{ * Blueprint for creating swappable components from a SwappableComponentRef and a loader * * @public + * @deprecated Use {@link @backstage/plugin-app-react#SwappableComponentBlueprint} instead. */ export const SwappableComponentBlueprint = createExtensionBlueprint({ kind: 'component', diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts index 5ec164fa8b..7a53f28fa3 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AppTheme } from '@backstage/core-plugin-api'; +import { AppTheme } from '../apis/definitions/AppThemeApi'; import { ThemeBlueprint } from './ThemeBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index 323078bc4e..f93075181e 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AppTheme } from '@backstage/core-plugin-api'; +import { AppTheme } from '../apis/definitions/AppThemeApi'; import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; const themeDataRef = createExtensionDataRef().with({ @@ -25,6 +25,7 @@ const themeDataRef = createExtensionDataRef().with({ * Creates an extension that adds/replaces an app theme. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#ThemeBlueprint} instead. */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index dd4a9ddc09..491053d763 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -25,6 +25,7 @@ const translationDataRef = createExtensionDataRef< * Creates an extension that adds translations to your app. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#TranslationBlueprint} instead. */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index c8699b4232..379c8d237b 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -30,7 +30,10 @@ export { export { NavItemBlueprint } from './NavItemBlueprint'; export { PageBlueprint } from './PageBlueprint'; export { RouterBlueprint } from './RouterBlueprint'; -export { SignInPageBlueprint } from './SignInPageBlueprint'; +export { + type SignInPageProps, + SignInPageBlueprint, +} from './SignInPageBlueprint'; export { ThemeBlueprint } from './ThemeBlueprint'; export { TranslationBlueprint } from './TranslationBlueprint'; export { SwappableComponentBlueprint } from './SwappableComponentBlueprint'; diff --git a/packages/frontend-plugin-api/src/components/AppNodeProvider.test.tsx b/packages/frontend-plugin-api/src/components/AppNodeProvider.test.tsx index fcc3b74c59..0a73f90dde 100644 --- a/packages/frontend-plugin-api/src/components/AppNodeProvider.test.tsx +++ b/packages/frontend-plugin-api/src/components/AppNodeProvider.test.tsx @@ -70,12 +70,10 @@ describe('AppNodeProvider', () => { ).toThrow('AppNodeContext v1 not available'); }); expect(error).toEqual([ - expect.objectContaining({ - detail: new Error('AppNodeContext v1 not available'), - }), - expect.objectContaining({ - detail: new Error('AppNodeContext v1 not available'), - }), + expect.stringContaining('Error: AppNodeContext v1 not available'), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('Error: AppNodeContext v1 not available'), + expect.objectContaining({ type: 'unhandled-exception' }), expect.stringContaining( 'The above error occurred in the component:', ), diff --git a/packages/frontend-plugin-api/src/components/ErrorApiBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorApiBoundary.tsx new file mode 100644 index 0000000000..e195da5d0f --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ErrorApiBoundary.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 { Component, ErrorInfo, ReactNode } from 'react'; +import { AppNode, ErrorApi } from '../apis'; +import { ForwardedError } from '@backstage/errors'; + +/** @internal */ +export class ErrorApiBoundary extends Component< + { + children: ReactNode; + node: AppNode; + errorApi?: ErrorApi; + }, + { error?: Error } +> { + static getDerivedStateFromError(error: Error) { + return { error }; + } + + state = { error: undefined }; + + componentDidCatch(error: Error, _errorInfo: ErrorInfo) { + const { node, errorApi } = this.props; + errorApi?.post( + new ForwardedError(`Error in extension '${node.spec.id}'`, error), + ); + } + + render() { + if (this.state.error) { + return null; + } + + return this.props.children; + } +} diff --git a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorDisplayBoundary.tsx similarity index 80% rename from packages/frontend-plugin-api/src/components/ErrorBoundary.tsx rename to packages/frontend-plugin-api/src/components/ErrorDisplayBoundary.tsx index 4cde84ee93..1584f18514 100644 --- a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ErrorDisplayBoundary.tsx @@ -14,25 +14,23 @@ * limitations under the License. */ -import { Component, PropsWithChildren } from 'react'; +import { Component, ReactNode } from 'react'; import { FrontendPlugin } from '../wiring'; import { ErrorDisplay } from './DefaultSwappableComponents'; -type ErrorBoundaryProps = PropsWithChildren<{ - plugin?: FrontendPlugin; -}>; -type ErrorBoundaryState = { error?: Error }; - /** @internal */ -export class ErrorBoundary extends Component< - ErrorBoundaryProps, - ErrorBoundaryState +export class ErrorDisplayBoundary extends Component< + { + children: ReactNode; + plugin: FrontendPlugin; + }, + { error?: Error } > { static getDerivedStateFromError(error: Error) { return { error }; } - state: ErrorBoundaryState = { error: undefined }; + state = { error: undefined }; handleErrorReset = () => { this.setState({ error: undefined }); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 6cce114d4d..c0068e8f7d 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -14,21 +14,22 @@ * limitations under the License. */ -import { useEffect } from 'react'; +import { useEffect, ReactNode } from 'react'; import { act, screen, waitFor } from '@testing-library/react'; -import { - mockApis, - TestApiProvider, - withLogCollector, -} from '@backstage/test-utils'; +import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; import { ExtensionBoundary } from './ExtensionBoundary'; import { coreExtensionData, createExtension } from '../wiring'; -import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; +import { analyticsApiRef } from '../apis/definitions/AnalyticsApi'; +import { useAnalytics } from '../analytics'; import { createRouteRef } from '../routing'; import { createExtensionTester, renderInTestApp, } from '@backstage/frontend-test-utils'; +import { + pluginWrapperApiRef, + PluginWrapperApi, +} from '../apis/definitions/PluginWrapperApi'; const wrapInBoundaryExtension = (element?: JSX.Element) => { const routeRef = createRouteRef(); @@ -93,7 +94,7 @@ describe('ExtensionBoundary', () => { it('should wrap children with analytics context', async () => { const action = 'render'; const subject = 'analytics'; - const analyticsApiMock = mockApis.analytics(); + const analyticsApiMock = { captureEvent: jest.fn() }; const AnalyticsComponent = () => { const analytics = useAnalytics(); @@ -124,6 +125,85 @@ describe('ExtensionBoundary', () => { }); }); + it('should wrap children with PluginWrapper when provided', async () => { + const text = 'Wrapped Content'; + const TextComponent = () => { + return

    {text}

    ; + }; + + const WrapperComponent = ({ children }: { children: ReactNode }) => { + return ( +
    + Wrapper + {children} +
    + ); + }; + + const pluginWrapperApi: PluginWrapperApi = { + getPluginWrapper: jest.fn((pluginId: string) => { + if (pluginId === 'app') { + return WrapperComponent; + } + return undefined; + }), + }; + + renderInTestApp( + + {createExtensionTester( + wrapInBoundaryExtension(), + ).reactElement()} + , + ); + + expect(await screen.findByTestId('plugin-wrapper')).toBeInTheDocument(); + expect(screen.getByText('Wrapper')).toBeInTheDocument(); + expect(screen.getByText(text)).toBeInTheDocument(); + expect(pluginWrapperApi.getPluginWrapper).toHaveBeenCalledWith('app'); + }); + + it('should handle errors thrown by PluginWrapper with ErrorDisplayBoundary', async () => { + const errorMsg = 'PluginWrapper error'; + const TextComponent = () => { + return

    Content

    ; + }; + + const ThrowingWrapper = () => { + throw new Error(errorMsg); + }; + + const pluginWrapperApi: PluginWrapperApi = { + getPluginWrapper: jest.fn((pluginId: string) => { + if (pluginId === 'app') { + return ThrowingWrapper; + } + return undefined; + }), + }; + + const { error } = await withLogCollector(['error'], async () => { + renderInTestApp( + + {createExtensionTester( + wrapInBoundaryExtension(), + ).reactElement()} + , + ); + await waitFor(() => + expect(screen.getByText(errorMsg)).toBeInTheDocument(), + ); + }); + + expect(error).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining(errorMsg), + }), + ]), + ); + }); + // TODO(Rugvip): Need a way to be able to override APIs in the app to be able to test this properly // eslint-disable-next-line jest/no-disabled-tests it.skip('should emit analytics events if routable', async () => { @@ -134,7 +214,7 @@ describe('ExtensionBoundary', () => { }); return null; }; - const analyticsApiMock = mockApis.analytics(); + const analyticsApiMock = { captureEvent: jest.fn() }; await act(async () => { renderInTestApp( diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 848ea64a2c..fefa2dc521 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -19,17 +19,38 @@ import { ReactNode, Suspense, useEffect, + useMemo, lazy as reactLazy, } from 'react'; -import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; -import { ErrorBoundary } from './ErrorBoundary'; +import { AnalyticsContext, useAnalytics } from '../analytics'; +import { ErrorDisplayBoundary } from './ErrorDisplayBoundary'; +import { ErrorApiBoundary } from './ErrorApiBoundary'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; -import { AppNode } from '../apis'; +import { AppNode, ErrorApi, errorApiRef, useApi } from '../apis'; +import { + PluginWrapperApi, + pluginWrapperApiRef, +} from '../apis/definitions/PluginWrapperApi'; import { coreExtensionData } from '../wiring'; import { AppNodeProvider } from './AppNodeProvider'; import { Progress } from './DefaultSwappableComponents'; +function useOptionalErrorApi(): ErrorApi | undefined { + try { + return useApi(errorApiRef); + } catch { + return undefined; + } +} + +function useOptionalPluginWrapperApi(): PluginWrapperApi | undefined { + try { + return useApi(pluginWrapperApiRef); + } catch { + return undefined; + } +} type RouteTrackerProps = PropsWithChildren<{ enabled?: boolean; }>; @@ -53,6 +74,7 @@ const RouteTracker = (props: RouteTrackerProps) => { /** @public */ export interface ExtensionBoundaryProps { + errorPresentation?: 'error-api' | 'error-display'; node: AppNode; children: ReactNode; } @@ -61,27 +83,52 @@ export interface ExtensionBoundaryProps { export function ExtensionBoundary(props: ExtensionBoundaryProps) { const { node, children } = props; + const errorApi = useOptionalErrorApi(); + const hasRoutePathOutput = Boolean( node.instance?.getData(coreExtensionData.routePath), ); const plugin = node.spec.plugin; + const pluginId = plugin.id ?? 'app'; + + const pluginWrapperApi = useOptionalPluginWrapperApi(); + + const PluginWrapper = useMemo(() => { + return pluginWrapperApi?.getPluginWrapper(pluginId); + }, [pluginWrapperApi, pluginId]); // Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight const attributes = { extensionId: node.spec.id, - pluginId: node.spec.plugin?.id ?? 'app', + pluginId, }; + let content = ( + + {children} + + ); + + if (PluginWrapper) { + content = {content}; + } + + if (props.errorPresentation === 'error-api') { + content = ( + + {content} + + ); + } else { + content = ( + {content} + ); + } + return ( - }> - - - {children} - - - + }>{content} ); } diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx index 91aaeb2a25..ee1136fca3 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx @@ -31,7 +31,7 @@ describe('ExtensionSuspense', () => { ), ); - expect(screen.getByTestId('progress')).toBeInTheDocument(); + expect(screen.getByTestId('core-progress')).toBeInTheDocument(); }); it('should render the lazy loaded children component', async () => { diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx index 0ae8b413e4..9439cafed8 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx @@ -15,7 +15,7 @@ */ import { ReactNode, Suspense } from 'react'; -import { useApp } from '@backstage/core-plugin-api'; +import { Progress } from './DefaultSwappableComponents'; /** @public */ export interface ExtensionSuspenseProps { @@ -26,8 +26,5 @@ export interface ExtensionSuspenseProps { export function ExtensionSuspense(props: ExtensionSuspenseProps) { const { children } = props; - const app = useApp(); - const { Progress } = app.getComponents(); - return }>{children}; } diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts index 97d3dd2264..15366c8a7a 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -14,24 +14,23 @@ * limitations under the License. */ -import { - ExternalRouteRef, - createExternalRouteRef, - toInternalExternalRouteRef, -} from './ExternalRouteRef'; +import { ExternalRouteRef, createExternalRouteRef } from './ExternalRouteRef'; +import { OpaqueExternalRouteRef } from '@internal/frontend'; import { AnyRouteRefParams } from './types'; describe('ExternalRouteRef', () => { it('should be created', () => { const routeRef: ExternalRouteRef = createExternalRouteRef(); - const internal = toInternalExternalRouteRef(routeRef); + const internal = OpaqueExternalRouteRef.toInternal(routeRef); expect(internal.getParams()).toEqual([]); expect(String(internal)).toMatch( - /^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/, + /^externalRouteRef\{id=undefined,at='.*ExternalRouteRef\.test\.ts.*'\}$/, ); internal.setId('some-id'); - expect(String(internal)).toBe('ExternalRouteRef{some-id}'); + expect(String(internal)).toMatch( + /^externalRouteRef\{id=some-id,at='.*ExternalRouteRef\.test\.ts.*'\}$/, + ); }); it('should be created with params', () => { @@ -39,7 +38,7 @@ describe('ExternalRouteRef', () => { x: string; y: string; }> = createExternalRouteRef({ params: ['x', 'y'] }); - const internal = toInternalExternalRouteRef(routeRef); + const internal = OpaqueExternalRouteRef.toInternal(routeRef); expect(internal.getParams()).toEqual(['x', 'y']); }); diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts index 3de276f85c..cd2f98f504 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RouteRefImpl } from './RouteRef'; +import { OpaqueExternalRouteRef } from '@internal/frontend'; import { describeParentCallSite } from './describeParentCallSite'; import { AnyRouteRefParams } from './types'; @@ -58,37 +58,6 @@ export function toInternalExternalRouteRef< return r; } -/** @internal */ -export function isExternalRouteRef(opaque: { - $$type: string; -}): opaque is ExternalRouteRef { - return opaque.$$type === '@backstage/ExternalRouteRef'; -} - -/** @internal */ -class ExternalRouteRefImpl - extends RouteRefImpl - implements InternalExternalRouteRef -{ - readonly $$type = '@backstage/ExternalRouteRef' as any; - readonly params: string[]; - readonly defaultTarget: string | undefined; - - constructor( - params: string[] = [], - defaultTarget: string | undefined, - creationSite: string, - ) { - super(params, creationSite); - this.params = params; - this.defaultTarget = defaultTarget; - } - - getDefaultTarget() { - return this.defaultTarget; - } -} - /** * Creates a route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references. * @@ -102,7 +71,7 @@ class ExternalRouteRefImpl export function createExternalRouteRef< TParams extends { [param in TParamKeys]: string } | undefined = undefined, TParamKeys extends string = string, ->(options?: { +>(config?: { /** * The parameters that will be provided to the external route reference. */ @@ -124,9 +93,38 @@ export function createExternalRouteRef< ? TParams : { [param in TParamKeys]: string } > { - return new ExternalRouteRefImpl( - options?.params as string[] | undefined, - options?.defaultTarget, - describeParentCallSite(), - ); + const params = (config?.params ?? []) as string[]; + const creationSite = describeParentCallSite(); + + let id: string | undefined = undefined; + + return OpaqueExternalRouteRef.createInstance('v1', { + T: undefined as unknown as TParams, + getParams() { + return params; + }, + getDescription() { + if (id) { + return id; + } + return `created at '${creationSite}'`; + }, + getDefaultTarget() { + return config?.defaultTarget; + }, + setId(newId: string) { + if (!newId) { + throw new Error(`ExternalRouteRef id must be a non-empty string`); + } + if (id && id !== newId) { + throw new Error( + `ExternalRouteRef was referenced twice as both '${id}' and '${newId}'`, + ); + } + id = newId; + }, + toString(): string { + return `externalRouteRef{id=${id},at='${creationSite}'}`; + }, + }); } diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts index 570723df9a..1bd546f821 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.test.ts @@ -15,18 +15,19 @@ */ import { AnyRouteRefParams } from './types'; -import { RouteRef, createRouteRef, toInternalRouteRef } from './RouteRef'; +import { RouteRef, createRouteRef } from './RouteRef'; +import { OpaqueRouteRef } from '@internal/frontend'; describe('RouteRef', () => { it('should be created and have a mutable ID', () => { const routeRef: RouteRef = createRouteRef(); - const internal = toInternalRouteRef(routeRef); + const internal = OpaqueRouteRef.toInternal(routeRef); expect(internal.T).toBe(undefined); expect(internal.getParams()).toEqual([]); expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/); expect(String(internal)).toMatch( - /^RouteRef\{created at .*RouteRef\.test\.ts.*\}$/, + /^routeRef\{id=undefined,at='.*RouteRef\.test\.ts.*'\}$/, ); expect(() => internal.setId('')).toThrow( @@ -34,7 +35,9 @@ describe('RouteRef', () => { ); internal.setId('some-id'); - expect(String(internal)).toBe('RouteRef{some-id}'); + expect(String(internal)).toMatch( + /^routeRef\{id=some-id,at='.*RouteRef\.test\.ts.*'\}$/, + ); internal.setId('some-id'); // Should allow same ID expect(() => internal.setId('some-other-id')).toThrow( @@ -49,7 +52,7 @@ describe('RouteRef', () => { }> = createRouteRef({ params: ['x', 'y'], }); - const internal = toInternalRouteRef(routeRef); + const internal = OpaqueRouteRef.toInternal(routeRef); expect(internal.getParams()).toEqual(['x', 'y']); expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/); }); diff --git a/packages/frontend-plugin-api/src/routing/RouteRef.ts b/packages/frontend-plugin-api/src/routing/RouteRef.ts index 46076e6258..62f29adb86 100644 --- a/packages/frontend-plugin-api/src/routing/RouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/RouteRef.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { OpaqueRouteRef } from '@internal/frontend'; import { describeParentCallSite } from './describeParentCallSite'; import { AnyRouteRefParams } from './types'; @@ -33,93 +34,6 @@ export interface RouteRef< readonly T: TParams; } -/** @internal */ -export interface InternalRouteRef< - TParams extends AnyRouteRefParams = AnyRouteRefParams, -> extends RouteRef { - readonly version: 'v1'; - getParams(): string[]; - getDescription(): string; - - alias: string | undefined; - - setId(id: string): void; -} - -/** @internal */ -export function toInternalRouteRef< - TParams extends AnyRouteRefParams = AnyRouteRefParams, ->(resource: RouteRef): InternalRouteRef { - const r = resource as InternalRouteRef; - if (r.$$type !== '@backstage/RouteRef') { - throw new Error(`Invalid RouteRef, bad type '${r.$$type}'`); - } - - return r; -} - -/** @internal */ -export function isRouteRef(opaque: { $$type: string }): opaque is RouteRef { - return opaque.$$type === '@backstage/RouteRef'; -} - -/** @internal */ -export class RouteRefImpl implements InternalRouteRef { - readonly $$type = '@backstage/RouteRef'; - readonly version = 'v1'; - declare readonly T: never; - - #id?: string; - readonly #params: string[]; - readonly #creationSite: string; - readonly #alias?: string; - - constructor( - readonly params: string[] = [], - creationSite: string, - alias?: string, - ) { - this.#params = params; - this.#creationSite = creationSite; - this.#alias = alias; - } - - getParams(): string[] { - return this.#params; - } - - get alias(): string | undefined { - return this.#alias; - } - - getDescription(): string { - if (this.#id) { - return this.#id; - } - return `created at '${this.#creationSite}'`; - } - - get #name() { - return this.$$type.slice('@backstage/'.length); - } - - setId(id: string): void { - if (!id) { - throw new Error(`${this.#name} id must be a non-empty string`); - } - if (this.#id && this.#id !== id) { - throw new Error( - `${this.#name} was referenced twice as both '${this.#id}' and '${id}'`, - ); - } - this.#id = id; - } - - toString(): string { - return `${this.#name}{${this.getDescription()}}`; - } -} - /** * Create a {@link RouteRef} from a route descriptor. * @@ -145,9 +59,36 @@ export function createRouteRef< ? TParams : { [param in TParamKeys]: string } > { - return new RouteRefImpl( - config?.params as string[] | undefined, - describeParentCallSite(), - config?.aliasFor, - ) as RouteRef; + const params = (config?.params ?? []) as string[]; + const creationSite = describeParentCallSite(); + + let id: string | undefined = undefined; + + return OpaqueRouteRef.createInstance('v1', { + T: undefined as unknown as TParams, + getParams() { + return params; + }, + getDescription() { + if (id) { + return id; + } + return `created at '${creationSite}'`; + }, + alias: config?.aliasFor, + setId(newId: string) { + if (!newId) { + throw new Error(`RouteRef id must be a non-empty string`); + } + if (id && id !== newId) { + throw new Error( + `RouteRef was referenced twice as both '${id}' and '${newId}'`, + ); + } + id = newId; + }, + toString(): string { + return `routeRef{id=${id},at='${creationSite}'}`; + }, + }); } diff --git a/packages/frontend-plugin-api/src/routing/SubRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/SubRouteRef.test.ts index b04728c49b..4746600f40 100644 --- a/packages/frontend-plugin-api/src/routing/SubRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/SubRouteRef.test.ts @@ -15,33 +15,32 @@ */ import { AnyRouteRefParams } from './types'; -import { - SubRouteRef, - createSubRouteRef, - toInternalSubRouteRef, -} from './SubRouteRef'; -import { createRouteRef, toInternalRouteRef } from './RouteRef'; +import { SubRouteRef, createSubRouteRef } from './SubRouteRef'; +import { createRouteRef } from './RouteRef'; +import { OpaqueRouteRef, OpaqueSubRouteRef } from '@internal/frontend'; const parent = createRouteRef(); const parentX = createRouteRef({ params: ['x'] }); describe('SubRouteRef', () => { it('should be created', () => { - const internalParent = toInternalRouteRef(createRouteRef()); + const internalParent = OpaqueRouteRef.toInternal(createRouteRef()); const routeRef: SubRouteRef = createSubRouteRef({ parent: internalParent, path: '/foo', }); - const internal = toInternalSubRouteRef(routeRef); + const internal = OpaqueSubRouteRef.toInternal(routeRef); expect(internal.path).toBe('/foo'); expect(internal.T).toBe(undefined); expect(internal.getParent()).toBe(internalParent); expect(internal.getParams()).toEqual([]); expect(String(internal)).toMatch( - /SubRouteRef\{at \/foo with parent created at '.*SubRouteRef\.test\.ts.*'\}/, + /^subRouteRef\{path='\/foo',parent=routeRef\{id=undefined,at='.*SubRouteRef\.test\.ts.*'\}\}$/, ); internalParent.setId('some-id'); - expect(String(internal)).toBe('SubRouteRef{at /foo with parent some-id}'); + expect(String(internal)).toMatch( + /^subRouteRef\{path='\/foo',parent=routeRef\{id=some-id,at='.*SubRouteRef\.test\.ts.*'\}\}$/, + ); }); it('should be created with params', () => { @@ -49,7 +48,7 @@ describe('SubRouteRef', () => { parent, path: '/foo/:bar', }); - const internal = toInternalSubRouteRef(routeRef); + const internal = OpaqueSubRouteRef.toInternal(routeRef); expect(internal.path).toBe('/foo/:bar'); expect(internal.getParent()).toBe(parent); expect(internal.getParams()).toEqual(['bar']); @@ -64,7 +63,7 @@ describe('SubRouteRef', () => { parent: parentX, path: '/foo/:y/:z', }); - const internal = toInternalSubRouteRef(routeRef); + const internal = OpaqueSubRouteRef.toInternal(routeRef); expect(internal.path).toBe('/foo/:y/:z'); expect(internal.getParent()).toBe(parentX); expect(internal.getParams()).toEqual(['x', 'y', 'z']); @@ -75,7 +74,7 @@ describe('SubRouteRef', () => { parent: parentX, path: '/foo/bar', }); - const internal = toInternalSubRouteRef(routeRef); + const internal = OpaqueSubRouteRef.toInternal(routeRef); expect(internal.path).toBe('/foo/bar'); expect(internal.getParent()).toBe(parentX); expect(internal.getParams()).toEqual(['x']); diff --git a/packages/frontend-plugin-api/src/routing/SubRouteRef.ts b/packages/frontend-plugin-api/src/routing/SubRouteRef.ts index 74996ab4e7..4fe11ff6fb 100644 --- a/packages/frontend-plugin-api/src/routing/SubRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/SubRouteRef.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { RouteRef, toInternalRouteRef } from './RouteRef'; +import { OpaqueRouteRef, OpaqueSubRouteRef } from '@internal/frontend'; +import { RouteRef } from './RouteRef'; import { AnyRouteRefParams } from './types'; // Should match the pattern in react-router @@ -50,59 +51,6 @@ export interface InternalSubRouteRef< getDescription(): string; } -/** @internal */ -export function toInternalSubRouteRef< - TParams extends AnyRouteRefParams = AnyRouteRefParams, ->(resource: SubRouteRef): InternalSubRouteRef { - const r = resource as InternalSubRouteRef; - if (r.$$type !== '@backstage/SubRouteRef') { - throw new Error(`Invalid SubRouteRef, bad type '${r.$$type}'`); - } - - return r; -} - -/** @internal */ -export function isSubRouteRef(opaque: { - $$type: string; -}): opaque is SubRouteRef { - return opaque.$$type === '@backstage/SubRouteRef'; -} - -/** @internal */ -export class SubRouteRefImpl - implements SubRouteRef -{ - readonly $$type = '@backstage/SubRouteRef'; - readonly version = 'v1'; - declare readonly T: never; - - #params: string[]; - #parent: RouteRef; - - constructor(readonly path: string, params: string[], parent: RouteRef) { - this.#params = params; - this.#parent = parent; - } - - getParams(): string[] { - return this.#params; - } - - getParent(): RouteRef { - return this.#parent; - } - - getDescription(): string { - const parent = toInternalRouteRef(this.#parent); - return `at ${this.path} with parent ${parent.getDescription()}`; - } - - toString(): string { - return `SubRouteRef{${this.getDescription()}}`; - } -} - /** * Used in {@link PathParams} type declaration. * @ignore @@ -168,8 +116,9 @@ export function createSubRouteRef< const { path, parent } = config; type Params = PathParams; - const internalParent = toInternalRouteRef(parent); + const internalParent = OpaqueRouteRef.toInternal(parent); const parentParams = internalParent.getParams(); + const parentDescription = internalParent.getDescription(); // Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz' const pathParams = path @@ -195,14 +144,22 @@ export function createSubRouteRef< } } - // We ensure that the type of the return type is sane here - const subRouteRef = new SubRouteRefImpl( + return OpaqueSubRouteRef.createInstance('v1', { + T: undefined as unknown as TrimEmptyParams< + MergeParams + >, path, - params as string[], - parent, - ) as SubRouteRef>>; - - // But skip type checking of the return value itself, because the conditional - // type checking of the parent parameter overlap is tricky to express. - return subRouteRef as any; + getParams() { + return params; + }, + getParent() { + return parent; + }, + getDescription() { + return `at ${path} with parent ${parentDescription}`; + }, + toString() { + return `subRouteRef{path='${path}',parent=${parent}}`; + }, + }); } diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index 85f9d5c44a..b15ff980ec 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { z, ZodSchema, ZodTypeDef } from 'zod'; +import { z, type ZodSchema, type ZodTypeDef } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; import { PortableSchema } from './types'; diff --git a/packages/core-plugin-api/src/translation/TranslationMessages.test.ts b/packages/frontend-plugin-api/src/translation/TranslationMessages.test.ts similarity index 100% rename from packages/core-plugin-api/src/translation/TranslationMessages.test.ts rename to packages/frontend-plugin-api/src/translation/TranslationMessages.test.ts diff --git a/packages/core-plugin-api/src/translation/TranslationMessages.ts b/packages/frontend-plugin-api/src/translation/TranslationMessages.ts similarity index 95% rename from packages/core-plugin-api/src/translation/TranslationMessages.ts rename to packages/frontend-plugin-api/src/translation/TranslationMessages.ts index 0108fb9f18..83dd2ef557 100644 --- a/packages/core-plugin-api/src/translation/TranslationMessages.ts +++ b/packages/frontend-plugin-api/src/translation/TranslationMessages.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef } from './TranslationRef'; /** * Represents a collection of messages to be provided for a given translation ref. * - * @alpha + * @public * @remarks * * This collection of messages can either be used directly as an override for the @@ -43,7 +43,7 @@ export interface TranslationMessages< /** * Options for {@link createTranslationMessages}. * - * @alpha + * @public */ export interface TranslationMessagesOptions< TId extends string, @@ -62,7 +62,7 @@ export interface TranslationMessagesOptions< /** * Creates a collection of messages for a given translation ref. * - * @alpha + * @public */ export function createTranslationMessages< TId extends string, diff --git a/packages/core-plugin-api/src/translation/TranslationRef.test.ts b/packages/frontend-plugin-api/src/translation/TranslationRef.test.ts similarity index 100% rename from packages/core-plugin-api/src/translation/TranslationRef.test.ts rename to packages/frontend-plugin-api/src/translation/TranslationRef.test.ts diff --git a/packages/core-plugin-api/src/translation/TranslationRef.ts b/packages/frontend-plugin-api/src/translation/TranslationRef.ts similarity index 99% rename from packages/core-plugin-api/src/translation/TranslationRef.ts rename to packages/frontend-plugin-api/src/translation/TranslationRef.ts index c2ac7a5ce3..7686e68343 100644 --- a/packages/core-plugin-api/src/translation/TranslationRef.ts +++ b/packages/frontend-plugin-api/src/translation/TranslationRef.ts @@ -19,7 +19,7 @@ import { TranslationResource, } from './TranslationResource'; -/** @alpha */ +/** @public */ export interface TranslationRef< TId extends string = string, TMessages extends { [key in string]: string } = { [key in string]: string }, @@ -83,7 +83,7 @@ export interface InternalTranslationRef< getDefaultResource(): TranslationResource | undefined; } -/** @alpha */ +/** @public */ export interface TranslationRefOptions< TId extends string, TNestedMessages extends AnyNestedMessages, @@ -164,7 +164,7 @@ class TranslationRefImpl< } } -/** @alpha */ +/** @public */ export function createTranslationRef< TId extends string, const TNestedMessages extends AnyNestedMessages, diff --git a/packages/core-plugin-api/src/translation/TranslationResource.test.ts b/packages/frontend-plugin-api/src/translation/TranslationResource.test.ts similarity index 100% rename from packages/core-plugin-api/src/translation/TranslationResource.test.ts rename to packages/frontend-plugin-api/src/translation/TranslationResource.test.ts diff --git a/packages/core-plugin-api/src/translation/TranslationResource.ts b/packages/frontend-plugin-api/src/translation/TranslationResource.ts similarity index 94% rename from packages/core-plugin-api/src/translation/TranslationResource.ts rename to packages/frontend-plugin-api/src/translation/TranslationResource.ts index 0184c55b98..c317bda381 100644 --- a/packages/core-plugin-api/src/translation/TranslationResource.ts +++ b/packages/frontend-plugin-api/src/translation/TranslationResource.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -import { - TranslationMessages, - TranslationRef, -} from '@backstage/core-plugin-api/alpha'; +import { TranslationMessages } from './TranslationMessages'; +import { TranslationRef } from './TranslationRef'; -/** @alpha */ +/** @public */ export interface TranslationResource { $$type: '@backstage/TranslationResource'; id: TId; @@ -55,7 +53,7 @@ export function toInternalTranslationResource( return r; } -/** @alpha */ +/** @public */ export interface TranslationResourceOptions< TId extends string, TMessages extends { [key in string]: string }, @@ -72,7 +70,7 @@ export interface TranslationResourceOptions< translations: TTranslations; } -/** @alpha */ +/** @public */ export function createTranslationResource< TId extends string, TMessages extends { [key in string]: string }, diff --git a/packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts b/packages/frontend-plugin-api/src/translation/__fixtures__/counting-de.ts similarity index 100% rename from packages/core-plugin-api/src/translation/__fixtures__/counting-de.ts rename to packages/frontend-plugin-api/src/translation/__fixtures__/counting-de.ts diff --git a/packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json b/packages/frontend-plugin-api/src/translation/__fixtures__/counting-sv.json similarity index 100% rename from packages/core-plugin-api/src/translation/__fixtures__/counting-sv.json rename to packages/frontend-plugin-api/src/translation/__fixtures__/counting-sv.json diff --git a/packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json b/packages/frontend-plugin-api/src/translation/__fixtures__/fruits-de.json similarity index 100% rename from packages/core-plugin-api/src/translation/__fixtures__/fruits-de.json rename to packages/frontend-plugin-api/src/translation/__fixtures__/fruits-de.json diff --git a/packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts b/packages/frontend-plugin-api/src/translation/__fixtures__/fruits-sv.ts similarity index 100% rename from packages/core-plugin-api/src/translation/__fixtures__/fruits-sv.ts rename to packages/frontend-plugin-api/src/translation/__fixtures__/fruits-sv.ts diff --git a/packages/core-plugin-api/src/translation/__fixtures__/refs.ts b/packages/frontend-plugin-api/src/translation/__fixtures__/refs.ts similarity index 100% rename from packages/core-plugin-api/src/translation/__fixtures__/refs.ts rename to packages/frontend-plugin-api/src/translation/__fixtures__/refs.ts diff --git a/packages/frontend-plugin-api/src/translation/index.ts b/packages/frontend-plugin-api/src/translation/index.ts index ef3ccccbe3..121ec62c72 100644 --- a/packages/frontend-plugin-api/src/translation/index.ts +++ b/packages/frontend-plugin-api/src/translation/index.ts @@ -17,12 +17,16 @@ export { type TranslationMessages, type TranslationMessagesOptions, + createTranslationMessages, +} from './TranslationMessages'; +export { type TranslationResource, type TranslationResourceOptions, + createTranslationResource, +} from './TranslationResource'; +export { type TranslationRef, type TranslationRefOptions, - createTranslationMessages, - createTranslationResource, createTranslationRef, - useTranslationRef, -} from '@backstage/core-plugin-api/alpha'; +} from './TranslationRef'; +export { useTranslationRef } from './useTranslationRef'; diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx b/packages/frontend-plugin-api/src/translation/useTranslationRef.test.tsx similarity index 98% rename from packages/core-plugin-api/src/translation/useTranslationRef.test.tsx rename to packages/frontend-plugin-api/src/translation/useTranslationRef.test.tsx index b1fbd8a904..a7f5bd41ed 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx +++ b/packages/frontend-plugin-api/src/translation/useTranslationRef.test.tsx @@ -27,11 +27,11 @@ import { useTranslationRef } from './useTranslationRef'; import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppLanguageSelector } from '../../..//core-app-api/src/apis/implementations/AppLanguageApi'; +import { createTranslationResource } from './TranslationResource'; import { - createTranslationResource, TranslationApi, translationApiRef, -} from '../alpha'; +} from '../apis/definitions/TranslationApi'; import { ErrorApi, errorApiRef } from '../apis'; const plainRef = createTranslationRef({ diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/frontend-plugin-api/src/translation/useTranslationRef.ts similarity index 98% rename from packages/core-plugin-api/src/translation/useTranslationRef.ts rename to packages/frontend-plugin-api/src/translation/useTranslationRef.ts index b874ac93d8..2e029f82fa 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/frontend-plugin-api/src/translation/useTranslationRef.ts @@ -20,13 +20,13 @@ import { translationApiRef, TranslationFunction, TranslationSnapshot, -} from '../apis/alpha'; +} from '../apis/definitions/TranslationApi'; import { TranslationRef } from './TranslationRef'; // Make sure we don't fill the logs with loading errors for the same ref const loggedRefs = new WeakSet>(); -/** @alpha */ +/** @public */ export const useTranslationRef = < TMessages extends { [key in string]: string }, >( diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 59a81f1b59..989dcf4468 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -216,6 +216,167 @@ describe('createExtension', () => { ); }); + it('should create an extension with relative attachment points', () => { + const extension = createExtension({ + attachTo: [ + { relative: {}, input: 'tabs' }, + { relative: { kind: 'page' }, input: 'tabs' }, + { relative: { name: 'index' }, input: 'tabs' }, + { relative: { kind: 'page', name: 'index' }, input: 'tabs' }, + ], + output: [stringDataRef], + factory: () => [stringDataRef('bar')], + }); + expect(String(extension)).toBe( + 'ExtensionDefinition{attachTo=@tabs+page:@tabs+/index@tabs+page:/index@tabs}', + ); + }); + + it('should create an extension with relative attachment points by reference', () => { + const baseOpts = { + attachTo: { id: 'root', input: 'children' }, + inputs: { + tabs: createExtensionInput([stringDataRef]), + }, + output: [], + factory: () => [], + }; + const parent1 = createExtension({ + ...baseOpts, + }); + const parent2 = createExtension({ + ...baseOpts, + kind: 'page', + }); + const parent3 = createExtension({ + ...baseOpts, + name: 'index', + }); + const parent4 = createExtension({ + ...baseOpts, + inputs: {}, + kind: 'page', + name: 'index', + }).override({ + inputs: { + otherTabs: createExtensionInput([stringDataRef]), + }, + factory: () => [], + }); + const extension = createExtension({ + attachTo: [ + parent1.inputs.tabs, + parent2.inputs.tabs, + parent3.inputs.tabs, + parent4.inputs.otherTabs, + ], + output: [stringDataRef], + factory: () => [stringDataRef('bar')], + }); + expect(String(extension)).toBe( + 'ExtensionDefinition{attachTo=@tabs+page:@tabs+/index@tabs+page:/index@otherTabs}', + ); + const overrdeExtension = extension.override({ + attachTo: [ + parent2.inputs.tabs, + parent3.inputs.tabs, + parent4.inputs.otherTabs, + ], + }); + expect(String(overrdeExtension)).toBe( + 'ExtensionDefinition{attachTo=page:@tabs+/index@tabs+page:/index@otherTabs}', + ); + }); + + it('should provide type safe attachments by reference', () => { + const parent = createExtension({ + attachTo: { id: 'root', input: 'children' }, + inputs: { + string: createExtensionInput([stringDataRef]), + stringOpt: createExtensionInput([stringDataRef.optional()]), + number: createExtensionInput([numberDataRef]), + numberOpt: createExtensionInput([numberDataRef.optional()]), + both: createExtensionInput([stringDataRef, numberDataRef]), + bothOptString: createExtensionInput([ + stringDataRef.optional(), + numberDataRef, + ]), + bothOptNumber: createExtensionInput([ + stringDataRef, + numberDataRef.optional(), + ]), + bothOpt: createExtensionInput([ + stringDataRef.optional(), + numberDataRef.optional(), + ]), + }, + output: [], + factory: () => [], + }); + const strOutExt = createExtension({ + attachTo: parent.inputs.string, + output: [stringDataRef], + factory: () => [stringDataRef('str')], + }); + strOutExt.override({ + attachTo: parent.inputs.string, + }); + strOutExt.override({ + attachTo: parent.inputs.stringOpt, + }); + strOutExt.override({ + // @ts-expect-error + attachTo: parent.inputs.number, + }); + strOutExt.override({ + attachTo: parent.inputs.numberOpt, + }); + strOutExt.override({ + // @ts-expect-error + attachTo: parent.inputs.both, + }); + strOutExt.override({ + attachTo: parent.inputs.bothOptNumber, + }); + strOutExt.override({ + // @ts-expect-error + attachTo: parent.inputs.bothOptString, + }); + strOutExt.override({ + attachTo: parent.inputs.bothOpt, + }); + const numberOutExt = createExtension({ + // @ts-expect-error + attachTo: parent.inputs.string, + output: [numberDataRef], + factory: () => [numberDataRef(1)], + }); + numberOutExt.override({ + // @ts-expect-error + attachTo: parent.inputs.string, + }); + numberOutExt.override({ + attachTo: parent.inputs.number, + }); + const bothOutExt = createExtension({ + attachTo: parent.inputs.both, + output: [numberDataRef, stringDataRef], + factory: () => [numberDataRef(1), stringDataRef('str')], + }); + // TODO(Rugvip): Potentially encapsulate the parent input type in the extension, until then we can't verify this + bothOutExt.override({ + output: [numberDataRef.optional(), stringDataRef], + factory: () => [stringDataRef('str')], + }); + bothOutExt.override({ + // @ts-expect-error + attachTo: parent.inputs.both, + output: [numberDataRef.optional(), stringDataRef], + factory: () => [stringDataRef('str')], + }); + expect('types').not.toBe('broken'); + }); + it('should create an extension with input', () => { const extension = createExtension({ attachTo: { id: 'root', input: 'default' }, @@ -1157,4 +1318,26 @@ describe('createExtension', () => { ); }); }); + + it('should old inputs without context support', () => { + const legacyInput = createExtensionInput([numberDataRef]); + + // old API without context + delete (legacyInput as any).context; + delete (legacyInput as any).withContext; + + const extension = createExtension({ + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + inputs: { + foo: legacyInput, + }, + factory({ inputs }) { + unused(inputs.foo); + return [stringDataRef('output')]; + }, + }); + + expect(extension.inputs.foo).toBe(legacyInput); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 0c0b75a572..6e4aba2910 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -20,10 +20,13 @@ import { ResolvedInputValueOverrides, resolveInputOverrides, } from './resolveInputOverrides'; -import { createExtensionDataContainer } from '@internal/frontend'; +import { + createExtensionDataContainer, + OpaqueExtensionInput, +} from '@internal/frontend'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { ExtensionInput } from './createExtensionInput'; -import { z } from 'zod'; +import type { z } from 'zod'; import { createSchemaFromZod } from '../schema/createSchemaFromZod'; import { OpaqueExtensionDefinition } from '@internal/frontend'; import { ExtensionDataContainer } from './types'; @@ -44,13 +47,12 @@ export const ctxParamsSymbol = Symbol('params'); * Convert a single extension input into a matching resolved input. * @public */ -export type ResolvedExtensionInput< - TExtensionInput extends ExtensionInput, -> = TExtensionInput['extensionData'] extends Array - ? { - node: AppNode; - } & ExtensionDataContainer - : never; +export type ResolvedExtensionInput = + TExtensionInput['extensionData'] extends Array + ? { + node: AppNode; + } & ExtensionDataContainer + : never; /** * Converts an extension input map into a matching collection of resolved inputs. @@ -58,7 +60,7 @@ export type ResolvedExtensionInput< */ export type ResolvedExtensionInputs< TInputs extends { - [name in string]: ExtensionInput; + [name in string]: ExtensionInput; }, > = { [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] @@ -91,50 +93,104 @@ type JoinStringUnion< : JoinStringUnion : TResult; +/** @ignore */ +export type RequiredExtensionIds = + UExtensionData extends any + ? UExtensionData['config']['optional'] extends true + ? never + : UExtensionData['id'] + : never; + /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends ExtensionDataRef, UFactoryOutput extends ExtensionDataValue, -> = ( - UDeclaredOutput extends any - ? UDeclaredOutput['config']['optional'] extends true - ? never - : UDeclaredOutput['id'] - : never -) extends infer IRequiredOutputIds - ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] - ? {} - : `Error: The extension factory has undeclared output(s): ${JoinStringUnion< - Exclude - >}` - : `Error: The extension factory is missing the following output(s): ${JoinStringUnion< - Exclude +> = [RequiredExtensionIds] extends [UFactoryOutput['id']] + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} + : `Error: The extension factory has undeclared output(s): ${JoinStringUnion< + Exclude >}` - : never; + : `Error: The extension factory is missing the following output(s): ${JoinStringUnion< + Exclude, UFactoryOutput['id']> + >}`; -/** @public */ -export type ExtensionAttachToSpec = - | { id: string; input: string } - | Array<{ id: string; input: string }>; +/** @ignore */ +export type VerifyExtensionAttachTo< + UOutput extends ExtensionDataRef, + UParentInput extends ExtensionDataRef, +> = ExtensionDataRef extends UParentInput + ? {} + : [RequiredExtensionIds] extends [RequiredExtensionIds] + ? {} + : `Error: This parent extension input requires the following extension data, but it is not declared as guaranteed output of this extension: ${JoinStringUnion< + Exclude, RequiredExtensionIds> + >}`; + +/** + * Specifies where an extension should attach in the extension tree. + * + * @remarks + * + * A standard attachment point declaration will specify the ID of the parent extension, as well as the name of the input to attach to. + * + * There are three more advanced forms that are available for more complex use-cases: + * + * 1. Relative attachment points: using the `relative` property instead of `id`, the attachment point is resolved relative to the current plugin. + * 2. Extension input references: using a reference in code to another extension's input in the same plugin. These references are always relative. + * 3. Array of attachment points: an array of attachment points can be used to clone and attach to multiple extensions at once. + * + * @example + * ```ts + * // Attach to a specific extension by full ID + * { id: 'app/routes', input: 'routes' } + * + * // Attach to an extension in the same plugin by kind + * { relative: { kind: 'page' }, input: 'actions' } + * + * // Attach to a specific input of another extension + * const page = ParentBlueprint.make({ ... }); + * const child = ChildBlueprint.make({ attachTo: page.inputs.children }); + * + * // Attach to multiple parents at once + * [ + * { id: 'page/home', input: 'widgets' }, + * { relative: { kind: 'page' }, input: 'widgets' }, + * ] + * ``` + * + * @public + */ +export type ExtensionDefinitionAttachTo< + UParentInputs extends ExtensionDataRef = ExtensionDataRef, +> = + | { id: string; input: string; relative?: never } + | { relative: { kind?: string; name?: string }; input: string; id?: never } + | ExtensionInput + | Array< + | { id: string; input: string; relative?: never } + | { + relative: { kind?: string; name?: string }; + input: string; + id?: never; + } + | ExtensionInput + >; /** @public */ export type CreateExtensionOptions< TKind extends string | undefined, TName extends string | undefined, UOutput extends ExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + TInputs extends { [inputName in string]: ExtensionInput }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + UParentInputs extends ExtensionDataRef, > = { kind?: TKind; name?: TName; - attachTo: ExtensionAttachToSpec; + attachTo: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo; disabled?: boolean; inputs?: TInputs; output: Array; @@ -158,12 +214,7 @@ export type ExtensionDefinitionParameters = { configInput?: { [K in string]: any }; config?: { [K in string]: any }; output?: ExtensionDataRef; - inputs?: { - [KName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }; + inputs?: { [KName in string]: ExtensionInput }; params?: object | ExtensionBlueprintDefineParams; }; @@ -182,11 +233,25 @@ type AnyParamsInput = ) => ReturnType>); /** @public */ -export type ExtensionDefinition< - T extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, -> = { +export interface ExtensionDefinition< + TParams extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, +> { $$type: '@backstage/ExtensionDefinition'; - readonly T: T; + readonly T: TParams; +} + +/** @public */ +export interface OverridableExtensionDefinition< + T extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, +> extends ExtensionDefinition { + /** + * References to the inputs of this extension, which can be used to attach child extensions. + */ + readonly inputs: { + [K in keyof T['inputs']]: ExtensionInput< + T['inputs'][K] extends ExtensionInput ? IData : never + >; + }; override< TExtensionConfigSchema extends { @@ -194,17 +259,19 @@ export type ExtensionDefinition< }, UFactoryOutput extends ExtensionDataValue, UNewOutput extends ExtensionDataRef, - TExtraInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + TExtraInputs extends { [inputName in string]: ExtensionInput }, TParamsInput extends AnyParamsInput>, + UParentInputs extends ExtensionDataRef, >( args: Expand< { - attachTo?: ExtensionAttachToSpec; + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo< + ExtensionDataRef extends UNewOutput + ? NonNullable + : UNewOutput, + UParentInputs + >; disabled?: boolean; inputs?: TExtraInputs & { [KName in keyof T['inputs']]?: `Error: Input '${KName & @@ -265,7 +332,7 @@ export type ExtensionDefinition< : UNewOutput, UFactoryOutput >, - ): ExtensionDefinition<{ + ): OverridableExtensionDefinition<{ kind: T['kind']; name: T['name']; output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput; @@ -284,7 +351,31 @@ export type ExtensionDefinition< }> >; }>; -}; +} + +/** + * @internal + */ +function bindInputs( + inputs: { [inputName in string]: ExtensionInput } | undefined, + kind?: string, + name?: string, +) { + if (!inputs) { + return {}; + } + + return Object.fromEntries( + Object.entries(inputs).map(([inputName, input]) => [ + inputName, + OpaqueExtensionInput.toInternal(input).withContext?.({ + kind, + name, + input: inputName, + }) ?? input, + ]), + ); +} /** * Creates a new extension definition for installation in a Backstage app. @@ -323,16 +414,12 @@ export type ExtensionDefinition< */ export function createExtension< UOutput extends ExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + TInputs extends { [inputName in string]: ExtensionInput }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, const TKind extends string | undefined = undefined, const TName extends string | undefined = undefined, + UParentInputs extends ExtensionDataRef = ExtensionDataRef, >( options: CreateExtensionOptions< TKind, @@ -340,9 +427,10 @@ export function createExtension< UOutput, TInputs, TConfigSchema, - UFactoryOutput + UFactoryOutput, + UParentInputs >, -): ExtensionDefinition<{ +): OverridableExtensionDefinition<{ config: string extends keyof TConfigSchema ? {} : { @@ -404,7 +492,7 @@ export function createExtension< name: options.name, attachTo: options.attachTo, disabled: options.disabled ?? false, - inputs: options.inputs ?? {}, + inputs: bindInputs(options.inputs, options.kind, options.name), output: options.output, configSchema, factory: options.factory, @@ -416,12 +504,41 @@ export function createExtension< if (options.name) { parts.push(`name=${options.name}`); } - parts.push( - `attachTo=${[options.attachTo] - .flat() - .map(a => `${a.id}@${a.input}`) - .join('+')}`, - ); + const attachTo = [options.attachTo] + .flat() + .map(aAny => { + const a = aAny as ExtensionDefinitionAttachTo; + if (OpaqueExtensionInput.isType(a)) { + const { context } = OpaqueExtensionInput.toInternal(a); + if (!context) { + return ''; + } + let id = ''; + if (context?.kind) { + id = `${context?.kind}:${id}`; + } + if (context?.name) { + id = `${id}/${context?.name}`; + } + return `${id}@${context.input}`; + } + if ('relative' in a && a.relative) { + let id = ''; + if (a.relative.kind) { + id = `${a.relative.kind}:${id}`; + } + if (a.relative.name) { + id = `${id}/${a.relative.name}`; + } + return `${id}@${a.input}`; + } + if ('id' in a) { + return `${a.id}@${a.input}`; + } + throw new Error('Invalid attachment point specification'); + }) + .join('+'); + parts.push(`attachTo=${attachTo}`); return `ExtensionDefinition{${parts.join(',')}}`; }, override(overrideOptions) { @@ -449,9 +566,17 @@ export function createExtension< return createExtension({ kind: options.kind, name: options.name, - attachTo: overrideOptions.attachTo ?? options.attachTo, + attachTo: (overrideOptions.attachTo ?? + options.attachTo) as ExtensionDefinitionAttachTo, disabled: overrideOptions.disabled ?? options.disabled, - inputs: { ...overrideOptions.inputs, ...options.inputs }, + inputs: bindInputs( + { + ...(options.inputs ?? {}), + ...(overrideOptions.inputs ?? {}), + }, + options.kind, + options.name, + ), output: (overrideOptions.output ?? options.output) as ExtensionDataRef[], config: @@ -518,7 +643,7 @@ export function createExtension< return deduplicatedResult.values(); }, - }) as ExtensionDefinition; + }) as OverridableExtensionDefinition; }, }); } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index e4cc876ff0..4759441187 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -30,7 +30,7 @@ import { } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; -import { ExtensionDefinition } from './createExtension'; +import { createExtension, ExtensionDefinition } from './createExtension'; import { createExtensionDataContainer, OpaqueExtensionDefinition, @@ -1345,6 +1345,235 @@ describe('createExtensionBlueprint', () => { ).toThrow('Refused to override params and factory at the same time'); }); + describe('with relative attachment points', () => { + const dataRef = createExtensionDataRef().with({ id: 'test.data' }); + + it('should create an extension with relative attachment points', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test', + attachTo: [ + { relative: {}, input: 'tabs' }, + { relative: { kind: 'page' }, input: 'tabs' }, + { relative: { name: 'index' }, input: 'tabs' }, + { relative: { kind: 'page', name: 'index' }, input: 'tabs' }, + ], + output: [dataRef], + factory: () => [dataRef('bar')], + }); + + expect(String(blueprint.make({ params: {} }))).toBe( + 'ExtensionDefinition{kind=test,attachTo=@tabs+page:@tabs+/index@tabs+page:/index@tabs}', + ); + expect( + String( + blueprint.make({ + attachTo: [ + { relative: { kind: 'page' }, input: 'tabs' }, + { relative: { name: 'index' }, input: 'tabs' }, + { relative: { kind: 'page', name: 'index' }, input: 'tabs' }, + ], + params: {}, + }), + ), + ).toBe( + 'ExtensionDefinition{kind=test,attachTo=page:@tabs+/index@tabs+page:/index@tabs}', + ); + expect( + String( + blueprint.makeWithOverrides({ + attachTo: { + relative: { kind: 'page', name: 'index' }, + input: 'tabs', + }, + factory: orig => orig({}), + }), + ), + ).toBe( + 'ExtensionDefinition{kind=test,attachTo=page:/index@tabs}', + ); + }); + + it('should create an extension with relative attachment points by reference', () => { + const baseOpts = { + attachTo: { id: 'root', input: 'children' }, + inputs: { + tabs: createExtensionInput([dataRef]), + }, + output: [], + factory: () => [], + }; + const parent1 = createExtension({ + ...baseOpts, + }); + const parent2 = createExtension({ + ...baseOpts, + kind: 'page', + }); + const parent3 = createExtension({ + ...baseOpts, + name: 'index', + }); + const parent4 = createExtension({ + ...baseOpts, + inputs: {}, + kind: 'page', + name: 'index', + }).override({ + inputs: { + otherTabs: createExtensionInput([dataRef]), + }, + factory: () => [], + }); + const blueprint = createExtensionBlueprint({ + kind: 'test', + attachTo: [ + parent1.inputs.tabs, + parent2.inputs.tabs, + parent3.inputs.tabs, + parent4.inputs.otherTabs, + ], + output: [dataRef], + factory: () => [dataRef('bar')], + }); + expect(String(blueprint.make({ params: {} }))).toBe( + 'ExtensionDefinition{kind=test,attachTo=@tabs+page:@tabs+/index@tabs+page:/index@otherTabs}', + ); + expect( + String( + blueprint.make({ + attachTo: [ + parent2.inputs.tabs, + parent3.inputs.tabs, + parent4.inputs.otherTabs, + ], + params: {}, + }), + ), + ).toBe( + 'ExtensionDefinition{kind=test,attachTo=page:@tabs+/index@tabs+page:/index@otherTabs}', + ); + expect( + String( + blueprint.makeWithOverrides({ + attachTo: parent4.inputs.otherTabs, + factory: orig => orig({}), + }), + ), + ).toBe( + 'ExtensionDefinition{kind=test,attachTo=page:/index@otherTabs}', + ); + }); + + it('should provide type safe attachments by reference', () => { + const stringDataRef = createExtensionDataRef().with({ + id: 'test.string', + }); + const numberDataRef = createExtensionDataRef().with({ + id: 'test.number', + }); + + const parent = createExtensionBlueprint({ + kind: 'test-parent', + attachTo: { id: 'root', input: 'children' }, + inputs: { + string: createExtensionInput([stringDataRef]), + stringOpt: createExtensionInput([stringDataRef.optional()]), + number: createExtensionInput([numberDataRef]), + numberOpt: createExtensionInput([numberDataRef.optional()]), + both: createExtensionInput([stringDataRef, numberDataRef]), + bothOptString: createExtensionInput([ + stringDataRef.optional(), + numberDataRef, + ]), + bothOptNumber: createExtensionInput([ + stringDataRef, + numberDataRef.optional(), + ]), + bothOpt: createExtensionInput([ + stringDataRef.optional(), + numberDataRef.optional(), + ]), + }, + output: [], + factory: () => [], + }).make({ params: {} }); + const strOutExt = createExtensionBlueprint({ + kind: 'test', + attachTo: parent.inputs.string, + output: [stringDataRef], + factory: () => [stringDataRef('str')], + }); + strOutExt.make({ + attachTo: parent.inputs.string, + params: {}, + }); + strOutExt.makeWithOverrides({ + attachTo: parent.inputs.stringOpt, + factory: orig => orig({}), + }); + strOutExt.make({ + // @ts-expect-error + attachTo: parent.inputs.number, + params: {}, + }); + strOutExt.makeWithOverrides({ + attachTo: parent.inputs.numberOpt, + factory: orig => orig({}), + }); + strOutExt.make({ + // @ts-expect-error + attachTo: parent.inputs.both, + params: {}, + }); + strOutExt.make({ + attachTo: parent.inputs.bothOptNumber, + params: {}, + }); + strOutExt.make({ + // @ts-expect-error + attachTo: parent.inputs.bothOptString, + params: {}, + }); + strOutExt.make({ + attachTo: parent.inputs.bothOpt, + params: {}, + }); + const numberOutExt = createExtensionBlueprint({ + kind: 'test', + // @ts-expect-error + attachTo: parent.inputs.string, + output: [numberDataRef], + factory: () => [numberDataRef(1)], + }); + numberOutExt.make({ + // @ts-expect-error + attachTo: parent.inputs.string, + params: {}, + }); + numberOutExt.makeWithOverrides({ + attachTo: parent.inputs.number, + factory: orig => orig({}), + }); + const bothOutExt = createExtensionBlueprint({ + kind: 'test', + attachTo: parent.inputs.both, + output: [numberDataRef, stringDataRef], + factory: () => [numberDataRef(1), stringDataRef('str')], + }); + bothOutExt.makeWithOverrides({ + output: [numberDataRef.optional(), stringDataRef], + factory: orig => orig({}), + }); + bothOutExt.makeWithOverrides({ + // @ts-expect-error + attachTo: parent.inputs.both, + output: [numberDataRef.optional(), stringDataRef], + factory: orig => orig({}), + }); + expect('types').not.toBe('broken'); + }); + }); + describe('with advanced parameter types', () => { const testDataRef = createExtensionDataRef().with({ id: 'test' }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 1a24eed712..a7537d27e9 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -18,14 +18,15 @@ import { ApiHolder, AppNode } from '../apis'; import { Expand } from '@backstage/types'; import { OpaqueType } from '@internal/opaque'; import { - ExtensionAttachToSpec, - ExtensionDefinition, + ExtensionDefinitionAttachTo, + OverridableExtensionDefinition, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, createExtension, ctxParamsSymbol, + VerifyExtensionAttachTo, } from './createExtension'; -import { z } from 'zod'; +import type { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { createExtensionDataContainer } from '@internal/frontend'; @@ -103,18 +104,15 @@ export type CreateExtensionBlueprintOptions< TKind extends string, TParams extends object | ExtensionBlueprintDefineParams, UOutput extends ExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + TInputs extends { [inputName in string]: ExtensionInput }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: ExtensionDataRef }, + UParentInputs extends ExtensionDataRef, > = { kind: TKind; - attachTo: ExtensionAttachToSpec; + attachTo: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo; disabled?: boolean; inputs?: TInputs; output: Array; @@ -186,12 +184,7 @@ export type ExtensionBlueprintParameters = { configInput?: { [K in string]: any }; config?: { [K in string]: any }; output?: ExtensionDataRef; - inputs?: { - [KName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }; + inputs?: { [KName in string]: ExtensionInput }; dataRefs?: { [name in string]: ExtensionDataRef }; }; @@ -222,16 +215,18 @@ export interface ExtensionBlueprint< make< TName extends string | undefined, TParamsInput extends AnyParamsInput>, + UParentInputs extends ExtensionDataRef, >(args: { name?: TName; - attachTo?: ExtensionAttachToSpec; + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo, UParentInputs>; disabled?: boolean; params: TParamsInput extends ExtensionBlueprintDefineParams ? TParamsInput : T['params'] extends ExtensionBlueprintDefineParams ? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `.make({ params: defineParams => defineParams() })`' : T['params']; - }): ExtensionDefinition<{ + }): OverridableExtensionDefinition<{ kind: T['kind']; name: string | undefined extends TName ? undefined : TName; config: T['config']; @@ -254,15 +249,17 @@ export interface ExtensionBlueprint< }, UFactoryOutput extends ExtensionDataValue, UNewOutput extends ExtensionDataRef, - TExtraInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + UParentInputs extends ExtensionDataRef, + TExtraInputs extends { [inputName in string]: ExtensionInput } = {}, >(args: { name?: TName; - attachTo?: ExtensionAttachToSpec; + attachTo?: ExtensionDefinitionAttachTo & + VerifyExtensionAttachTo< + ExtensionDataRef extends UNewOutput + ? NonNullable + : UNewOutput, + UParentInputs + >; disabled?: boolean; inputs?: TExtraInputs & { [KName in keyof T['inputs']]?: `Error: Input '${KName & @@ -306,27 +303,31 @@ export interface ExtensionBlueprint< : UNewOutput, UFactoryOutput >; - }): ExtensionDefinition<{ - config: (string extends keyof TExtensionConfigSchema - ? {} - : { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }) & - T['config']; - configInput: (string extends keyof TExtensionConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] + }): OverridableExtensionDefinition<{ + config: Expand< + (string extends keyof TExtensionConfigSchema + ? {} + : { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType >; - }> - >) & - T['configInput']; + }) & + T['config'] + >; + configInput: Expand< + (string extends keyof TExtensionConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + >) & + T['configInput'] + >; output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput; - inputs: T['inputs'] & TExtraInputs; + inputs: Expand; kind: T['kind']; name: string | undefined extends TName ? undefined : TName; params: T['params']; @@ -455,15 +456,11 @@ function unwrapParams( export function createExtensionBlueprint< TParams extends object | ExtensionBlueprintDefineParams, UOutput extends ExtensionDataRef, - TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + TInputs extends { [inputName in string]: ExtensionInput }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, TKind extends string, + UParentInputs extends ExtensionDataRef, TDataRefs extends { [name in string]: ExtensionDataRef } = never, >( options: CreateExtensionBlueprintOptions< @@ -473,7 +470,8 @@ export function createExtensionBlueprint< TInputs, TConfigSchema, UFactoryOutput, - TDataRefs + TDataRefs, + UParentInputs >, ): ExtensionBlueprint<{ kind: TKind; @@ -509,7 +507,8 @@ export function createExtensionBlueprint< return createExtension({ kind: options.kind, name: args.name, - attachTo: args.attachTo ?? options.attachTo, + attachTo: (args.attachTo ?? + options.attachTo) as ExtensionDefinitionAttachTo, disabled: args.disabled ?? options.disabled, inputs: options.inputs, output: options.output as ExtensionDataRef[], @@ -519,13 +518,14 @@ export function createExtensionBlueprint< unwrapParams(args.params, ctx, defineParams, options.kind), ctx, ) as Iterable>, - }) as ExtensionDefinition; + }) as OverridableExtensionDefinition; }, makeWithOverrides(args) { return createExtension({ kind: options.kind, name: args.name, - attachTo: args.attachTo ?? options.attachTo, + attachTo: (args.attachTo ?? + options.attachTo) as ExtensionDefinitionAttachTo, disabled: args.disabled ?? options.disabled, inputs: { ...args.inputs, ...options.inputs }, output: (args.output ?? options.output) as ExtensionDataRef[], @@ -576,7 +576,7 @@ export function createExtensionBlueprint< }, ) as Iterable>; }, - }) as ExtensionDefinition; + }) as OverridableExtensionDefinition; }, } as ExtensionBlueprint<{ kind: TKind; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts index 084758b793..279878542a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { OpaqueExtensionInput } from '@internal/frontend'; import { createExtensionDataRef } from './createExtensionDataRef'; import { ExtensionInput, createExtensionInput } from './createExtensionInput'; @@ -29,6 +30,7 @@ describe('createExtensionInput', () => { $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], config: { singleton: false, optional: false }, + withContext: expect.any(Function), }); const x1: ExtensionInput< @@ -54,6 +56,20 @@ describe('createExtensionInput', () => { unused(x1, x2, x3, x4); }); + it('should attach a context to the input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef]); + const context = { input: 'test1', kind: 'test2', name: 'test3' }; + const inputWithContext = + OpaqueExtensionInput.toInternal(input).withContext?.(context); + expect(inputWithContext).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: false, optional: false }, + withContext: expect.any(Function), + context, + }); + }); + it('should create a singleton input', () => { const input = createExtensionInput([stringDataRef, numberDataRef], { singleton: true, @@ -62,6 +78,7 @@ describe('createExtensionInput', () => { $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], config: { singleton: true, optional: false }, + withContext: expect.any(Function), }); const x1: ExtensionInput< @@ -96,6 +113,7 @@ describe('createExtensionInput', () => { $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], config: { singleton: true, optional: true }, + withContext: expect.any(Function), }); const x1: ExtensionInput< diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts index 7209e6f647..57040c8adf 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts @@ -14,17 +14,28 @@ * limitations under the License. */ +import { + ExtensionInputContext, + OpaqueExtensionInput, +} from '@internal/frontend'; import { ExtensionDataRef } from './createExtensionDataRef'; /** @public */ export interface ExtensionInput< - UExtensionData extends ExtensionDataRef, - TConfig extends { singleton: boolean; optional: boolean }, + UExtensionData extends ExtensionDataRef< + unknown, + string, + { optional?: true } + > = ExtensionDataRef, + TConfig extends { singleton: boolean; optional: boolean } = { + singleton: boolean; + optional: boolean; + }, > { - $$type: '@backstage/ExtensionInput'; - extensionData: Array; - config: TConfig; - replaces?: Array<{ id: string; input: string }>; + readonly $$type: '@backstage/ExtensionInput'; + readonly extensionData: Array; + readonly config: TConfig; + readonly replaces?: Array<{ id: string; input: string }>; } /** @public */ @@ -61,8 +72,7 @@ export function createExtensionInput< } } } - return { - $$type: '@backstage/ExtensionInput', + const baseOptions = { extensionData, config: { singleton: Boolean(config?.singleton) as TConfig['singleton'] extends true @@ -73,11 +83,21 @@ export function createExtensionInput< : false, }, replaces: config?.replaces, - } as ExtensionInput< + }; + + function createInstance(parent?: ExtensionInputContext): ExtensionInput< UExtensionData, { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; } - >; + > { + return OpaqueExtensionInput.createInstance(undefined, { + ...baseOptions, + context: parent, + withContext: createInstance, + }); + } + + return createInstance(); } diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts index 5555a0b47c..68b5826baa 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts @@ -18,7 +18,10 @@ import { OpaqueExtensionDefinition, OpaqueFrontendPlugin, } from '@internal/frontend'; -import { ExtensionDefinition } from './createExtension'; +import { + ExtensionDefinition, + OverridableExtensionDefinition, +} from './createExtension'; import { Extension, resolveExtensionDefinition, @@ -104,7 +107,9 @@ export interface OverridableFrontendPlugin< [id in string]: ExtensionDefinition; }, > extends FrontendPlugin { - getExtension(id: TId): TExtensionMap[TId]; + getExtension( + id: TId, + ): OverridableExtensionDefinition; withOverrides(options: { extensions: Array; diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 45c80d4a90..6ce1bb4998 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -15,12 +15,13 @@ */ export { coreExtensionData } from './coreExtensionData'; +export { createExtension } from './createExtension'; export { - createExtension, type ExtensionDefinition, + type ExtensionDefinitionAttachTo, type ExtensionDefinitionParameters, - type ExtensionAttachToSpec, type CreateExtensionOptions, + type OverridableExtensionDefinition, type ResolvedExtensionInput, type ResolvedExtensionInputs, } from './createExtension'; @@ -54,7 +55,11 @@ export { type FrontendFeatureLoader, type CreateFrontendFeatureLoaderOptions, } from './createFrontendFeatureLoader'; -export { type Extension } from './resolveExtensionDefinition'; +export { + type Extension, + type ExtensionAttachTo, + type ExtensionAttachToSpec, +} from './resolveExtensionDefinition'; export { type ExtensionDataContainer, type FeatureFlagConfig, diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index a74d908024..79749af77c 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -14,11 +14,21 @@ * limitations under the License. */ +import { + createExtensionDataRef, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from './createExtension'; import { ResolveExtensionId, resolveExtensionDefinition, } from './resolveExtensionDefinition'; +import { + OpaqueExtensionDefinition, + OpaqueExtensionInput, +} from '@internal/frontend'; + +const testDataRef = createExtensionDataRef().with({ id: 'test' }); describe('resolveExtensionDefinition', () => { const baseDef = { @@ -26,6 +36,7 @@ describe('resolveExtensionDefinition', () => { T: undefined as any, version: 'v2', attachTo: { id: '', input: '' }, + inputs: {}, disabled: false, override: () => ({} as ExtensionDefinition), }; @@ -60,6 +71,111 @@ describe('resolveExtensionDefinition', () => { 'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=undefined namespace=undefined name=undefined', ); }); + + it('should resolve extension input references', () => { + const baseInpuf = OpaqueExtensionInput.toInternal( + createExtensionInput([testDataRef]), + ); + expect( + resolveExtensionDefinition( + OpaqueExtensionDefinition.toInternal({ + ...baseDef, + attachTo: baseInpuf.withContext?.({ + kind: 'parent', + name: 'example', + input: 'children', + }), + }), + { namespace: 'test' }, + ).attachTo, + ).toEqual({ + id: 'parent:test/example', + input: 'children', + }); + + expect( + resolveExtensionDefinition( + OpaqueExtensionDefinition.toInternal({ + ...baseDef, + attachTo: baseInpuf.withContext?.({ + name: 'example', + input: 'children', + }), + }), + { namespace: 'test' }, + ).attachTo, + ).toEqual({ + id: 'test/example', + input: 'children', + }); + + expect( + resolveExtensionDefinition( + OpaqueExtensionDefinition.toInternal({ + ...baseDef, + attachTo: baseInpuf.withContext?.({ + kind: 'parent', + input: 'children', + }), + }), + { namespace: 'test' }, + ).attachTo, + ).toEqual({ + id: 'parent:test', + input: 'children', + }); + + expect( + resolveExtensionDefinition( + OpaqueExtensionDefinition.toInternal({ + ...baseDef, + attachTo: baseInpuf.withContext?.({ + input: 'children', + }), + }), + { namespace: 'test' }, + ).attachTo, + ).toEqual({ + id: 'test', + input: 'children', + }); + + expect( + resolveExtensionDefinition( + OpaqueExtensionDefinition.toInternal({ + ...baseDef, + attachTo: [ + baseInpuf.withContext?.({ + kind: 'k1', + input: 'children', + }), + baseInpuf.withContext?.({ + kind: 'k2', + input: 'children', + }), + baseInpuf.withContext?.({ + kind: 'k3', + input: 'children', + }), + ], + }), + { namespace: 'test' }, + ).attachTo, + ).toEqual([ + { + id: 'k1:test', + input: 'children', + }, + { + id: 'k2:test', + input: 'children', + }, + { + id: 'k3:test', + input: 'children', + }, + ]); + }); }); describe('old resolveExtensionDefinition', () => { @@ -68,6 +184,7 @@ describe('old resolveExtensionDefinition', () => { T: undefined as any, version: 'v1', attachTo: { id: '', input: '' }, + inputs: {}, disabled: false, override: () => ({} as ExtensionDefinition), }; diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index a5b6859e1a..f99d586e87 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -16,7 +16,7 @@ import { ApiHolder, AppNode } from '../apis'; import { - ExtensionAttachToSpec, + ExtensionDefinitionAttachTo, ExtensionDefinition, ExtensionDefinitionParameters, ResolvedExtensionInputs, @@ -24,7 +24,21 @@ import { import { PortableSchema } from '../schema'; import { ExtensionInput } from './createExtensionInput'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; -import { OpaqueExtensionDefinition } from '@internal/frontend'; +import { + OpaqueExtensionDefinition, + OpaqueExtensionInput, +} from '@internal/frontend'; + +/** @public */ +export type ExtensionAttachTo = + | { id: string; input: string } + | Array<{ id: string; input: string }>; + +/** + * @deprecated Use {@link ExtensionAttachTo} instead. + * @public + */ +export type ExtensionAttachToSpec = ExtensionAttachTo; /** @public */ export interface Extension { @@ -68,22 +82,14 @@ export type InternalExtension = Extension< } | { readonly version: 'v2'; - readonly inputs: { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }; + readonly inputs: { [inputName in string]: ExtensionInput }; readonly output: Array; factory(options: { apis: ApiHolder; node: AppNode; config: TConfig; inputs: ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; + [inputName in string]: ExtensionInput; }>; }): Iterable>; } @@ -126,24 +132,11 @@ export type ResolveExtensionId< : never : never; -/** @internal */ -export function resolveExtensionDefinition< - T extends ExtensionDefinitionParameters, ->( - definition: ExtensionDefinition, - context?: { namespace?: string }, -): Extension { - const internalDefinition = OpaqueExtensionDefinition.toInternal(definition); - const { - name, - kind, - namespace: _skip1, - override: _skip2, - ...rest - } = internalDefinition; - - const namespace = internalDefinition.namespace ?? context?.namespace; - +function resolveExtensionId( + kind?: string, + namespace?: string, + name?: string, +): string { const namePart = name && namespace ? `${namespace}/${name}` : namespace || name; if (!namePart) { @@ -152,10 +145,75 @@ export function resolveExtensionDefinition< ); } - const id = kind ? `${kind}:${namePart}` : namePart; + return kind ? `${kind}:${namePart}` : namePart; +} + +function resolveAttachTo( + attachTo: ExtensionDefinitionAttachTo, + namespace?: string, +): ExtensionAttachToSpec { + const resolveSpec = ( + spec: Exclude>, + ): { id: string; input: string } => { + if (OpaqueExtensionInput.isType(spec)) { + const { context } = OpaqueExtensionInput.toInternal(spec); + if (!context) { + throw new Error( + 'Invalid input object without a parent extension used as attachment point', + ); + } + return { + id: resolveExtensionId(context.kind, namespace, context.name), + input: context.input, + }; + } + if ('relative' in spec && spec.relative) { + return { + id: resolveExtensionId( + spec.relative.kind, + namespace, + spec.relative.name, + ), + input: spec.input, + }; + } + if ('id' in spec) { + return { id: spec.id, input: spec.input }; + } + throw new Error('Invalid attachment point specification'); + }; + + if (Array.isArray(attachTo)) { + return attachTo.map(resolveSpec); + } + + return resolveSpec(attachTo); +} + +/** @internal */ +export function resolveExtensionDefinition< + T extends ExtensionDefinitionParameters, +>( + definition: ExtensionDefinition, + context?: { namespace?: string }, +): Extension { + const internalDefinition = OpaqueExtensionDefinition.toInternal(definition); + + const { + name, + kind, + namespace: internalNamespace, + override: _skip2, + attachTo, + ...rest + } = internalDefinition; + + const namespace = internalNamespace ?? context?.namespace; + const id = resolveExtensionId(kind, namespace, name); return { ...rest, + attachTo: resolveAttachTo(attachTo, namespace), $$type: '@backstage/Extension', version: internalDefinition.version, id, diff --git a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts index 8ffec893ee..b46bb426fa 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts @@ -19,7 +19,6 @@ import { Expand } from '@backstage/types'; import { ResolvedExtensionInput } from './createExtension'; import { createExtensionDataContainer } from '@internal/frontend'; import { - ExtensionDataRef, ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; @@ -28,16 +27,8 @@ import { ExtensionDataContainer } from './types'; /** @ignore */ export type ResolvedInputValueOverrides< - TInputs extends { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - } = { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; + TInputs extends { [inputName in string]: ExtensionInput } = { + [inputName in string]: ExtensionInput; }, > = Expand< { @@ -90,12 +81,7 @@ function expectItem(value: T | T[]): T { /** @internal */ export function resolveInputOverrides( - declaredInputs?: { - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }, + declaredInputs?: { [inputName in string]: ExtensionInput }, inputs?: { [KName in string]?: | ({ node: AppNode } & ExtensionDataContainer) diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 1fa18a1696..6fbc409d5c 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/frontend-test-utils +## 0.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-app@0.3.4 + - @backstage/frontend-app-api@0.14.0 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.4-next.0 + - @backstage/frontend-app-api@0.13.4-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-app@0.3.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/test-utils@1.7.14 + - @backstage/frontend-app-api@0.13.3 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/plugin-app@0.3.3-next.0 + - @backstage/frontend-app-api@0.13.3-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.11 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/frontend-app-api@0.13.2 + - @backstage/config@1.3.6 + - @backstage/plugin-app@0.3.2 + - @backstage/test-utils@1.7.13 + ## 0.4.1-next.0 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 18ab54d197..22a8565cf2 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.4.1-next.0", + "version": "0.4.3", "backstage": { "role": "web-library" }, @@ -35,6 +35,7 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-app": "workspace:^", + "@backstage/plugin-app-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 747229e8d6..f7b1d4b58b 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -27,11 +27,12 @@ import { RouteRef, useRouteRef, IconComponent, - RouterBlueprint, NavItemBlueprint, createFrontendPlugin, FrontendFeature, + createFrontendModule, } from '@backstage/frontend-plugin-api'; +import { RouterBlueprint } from '@backstage/plugin-app-react'; import appPlugin from '@backstage/plugin-app'; const DEFAULT_MOCK_CONFIG = { @@ -154,15 +155,6 @@ export function renderInTestApp( return [coreExtensionData.reactElement(element)]; }, }), - RouterBlueprint.make({ - params: { - component: ({ children }) => ( - - {children} - - ), - }, - }), ]; if (options?.mountedRoutes) { @@ -189,6 +181,20 @@ export function renderInTestApp( } const features: FrontendFeature[] = [ + createFrontendModule({ + pluginId: 'app', + extensions: [ + RouterBlueprint.make({ + params: { + component: ({ children }) => ( + + {children} + + ), + }, + }), + ], + }), createFrontendPlugin({ pluginId: 'test', extensions, diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index e8d90cadd6..879e3fe1ae 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -17,16 +17,17 @@ import { createSpecializedApp } from '@backstage/frontend-app-api'; import { coreExtensionData, + createFrontendModule, createFrontendPlugin, ExtensionDefinition, FrontendFeature, - RouterBlueprint, } from '@backstage/frontend-plugin-api'; import { render } from '@testing-library/react'; import appPlugin from '@backstage/plugin-app'; import { JsonObject } from '@backstage/types'; import { ConfigReader } from '@backstage/config'; import { MemoryRouter } from 'react-router-dom'; +import { RouterBlueprint } from '@backstage/plugin-app-react'; const DEFAULT_MOCK_CONFIG = { app: { baseUrl: 'http://localhost:3000' }, @@ -74,20 +75,23 @@ const appPluginOverride = appPlugin.withOverrides({ * @public */ export function renderTestApp(options: RenderTestAppOptions) { - const extensions = [ - RouterBlueprint.make({ - params: { - component: ({ children }) => ( - - {children} - - ), - }, - }), - ...(options.extensions ?? []), - ]; + const extensions = [...(options.extensions ?? [])]; const features: FrontendFeature[] = [ + createFrontendModule({ + pluginId: 'app', + extensions: [ + RouterBlueprint.make({ + params: { + component: ({ children }) => ( + + {children} + + ), + }, + }), + ], + }), createFrontendPlugin({ pluginId: 'test', extensions, diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md index d49f70955d..60d231e612 100644 --- a/packages/integration-aws-node/CHANGELOG.md +++ b/packages/integration-aws-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-aws-node +## 0.1.19 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + ## 0.1.19-next.0 ### Patch Changes diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index e3cd09cde0..0484a54e0a 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-aws-node", - "version": "0.1.19-next.0", + "version": "0.1.19", "description": "Helpers for fetching AWS account credentials", "backstage": { "role": "node-library" diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 6956f04b08..28eca54868 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/integration-react +## 1.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2 + +## 1.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## 1.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/core-plugin-api@1.12.1 + +## 1.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/config@1.3.6 + +## 1.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/core-plugin-api@1.12.0 + - @backstage/config@1.3.6 + ## 1.2.12-next.0 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index ca9dce00d9..d99a9f7aa2 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.2.12-next.0", + "version": "1.2.14", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration-react/report.api.md b/packages/integration-react/report.api.md index 78da2eb5fa..0974c4821a 100644 --- a/packages/integration-react/report.api.md +++ b/packages/integration-react/report.api.md @@ -3,17 +3,17 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ApiFactory } from '@backstage/core-plugin-api'; +import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; -import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstageIdentityApi } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { OAuthApi } from '@backstage/core-plugin-api'; -import { OpenIdConnectApi } from '@backstage/core-plugin-api'; -import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { OpenIdConnectApi } from '@backstage/frontend-plugin-api'; +import { ProfileInfoApi } from '@backstage/frontend-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { SessionApi } from '@backstage/core-plugin-api'; +import { SessionApi } from '@backstage/frontend-plugin-api'; // @public export class ScmAuth implements ScmAuthApi { diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts index 0a1f1b1b8f..55acd29eec 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts @@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => { it('should be instantiated', () => { const i = ScmIntegrationsApi.fromConfig(new ConfigReader({})); - expect(i.list().length).toBe(7); // The default ones + expect(i.list().length).toBe(8); // The default ones }); }); diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index bcc2a938ae..453917b04c 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,87 @@ # @backstage/integration +## 1.19.2 + +### Patch Changes + +- 3afeab4: Implementing `ScmIntegration` for `GoogleGcs` +- 9083273: Rollback the lowercase replacing in GitHub integration config + +## 1.19.2-next.0 + +### Patch Changes + +- 3afeab4: Implementing `ScmIntegration` for `GoogleGcs` +- 9083273: Rollback the lowercase replacing in GitHub integration config + +## 1.19.0 + +### Minor Changes + +- 37fba1d: Added support for Bitbucket Cloud OAuth. This introduces an alternative authentication method using a workspace OAuth consumer, alongside App Passwords (deprecated) and API tokens. OAuth does not require a bot or service account and avoids token expiry issues. + + **BREAKING CHANGES** + + - **@backstage/integration** (`src/bitbucketCloud/core.ts`) + + - `getBitbucketCloudRequestOptions` now returns a `Promise` and **must** be awaited. + + - **@backstage/plugin-scaffolder-backend-module-bitbucket-cloud** (`src/actions/helpers.ts`) + - `getBitbucketClient` now returns a `Promise` and **must** be awaited. + - `getAuthorizationHeader` now returns a `Promise` and **must** be awaited. + + **OAuth usage example** + + ```yaml + integrations: + bitbucketCloud: + - clientId: client-id + clientSecret: client-secret + ``` + +### Patch Changes + +- a26a322: Added support for using a GitHub App installation to generate tokens for public repository access when the `publicAccess` option is enabled. When all other authentication methods fail (e.g., the app is not installed in that organization), the provider will now use an available installation to generate a token that can be used to access public repositories as read only. +- fb029b6: Updated luxon types +- e15fdae: Made the github urls case insensitive. + +## 1.18.3-next.1 + +### Patch Changes + +- fb029b6: Updated luxon types +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.18.3-next.0 + +### Patch Changes + +- e15fdae: Made the github urls case insensitive. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.18.2 + +### Patch Changes + +- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. + + API token usage example: + + ```yaml + integrations: + bitbucketCloud: + - username: user@domain.com + token: my-token + ``` + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/config@1.3.6 + ## 1.18.2-next.0 ### Patch Changes diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 298d765697..4278a8fcfe 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -174,12 +174,28 @@ export interface Config { * The username to use for authenticated requests. * @visibility secret */ - username: string; + username?: string; + /** + * Token used to authenticate requests. + * @visibility secret + */ + token?: string; /** * Bitbucket Cloud app password used to authenticate requests. * @visibility secret + * @deprecated Use `token` instead. */ - appPassword: string; + appPassword?: string; + /** + * OAuth client ID for Bitbucket Cloud. + * @visibility secret + */ + clientId?: string; + /** + * OAuth client secret for Bitbucket Cloud. + * @visibility secret + */ + clientSecret?: string; /** * PGP signing key for signing commits. * @visibility secret @@ -324,6 +340,10 @@ export interface Config { * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples */ allowedInstallationOwners?: string[]; + /** + * If true, then an installation token will be issued for access when no other token is available. + */ + publicAccess?: boolean; }>; }>; diff --git a/packages/integration/package.json b/packages/integration/package.json index 45928cd59b..9da5390a04 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.18.2-next.0", + "version": "1.19.2", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" @@ -51,7 +51,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/config-loader": "workspace:^", - "@types/luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/packages/integration/report.api.md b/packages/integration/report.api.md index f642058ccd..abcdb8898b 100644 --- a/packages/integration/report.api.md +++ b/packages/integration/report.api.md @@ -250,6 +250,8 @@ export type BitbucketCloudIntegrationConfig = { username?: string; appPassword?: string; token?: string; + clientId?: string; + clientSecret?: string; commitSigningKey?: string; }; @@ -450,12 +452,18 @@ export function getBitbucketCloudFileFetchUrl( config: BitbucketCloudIntegrationConfig, ): string; +// @public +export function getBitbucketCloudOAuthToken( + clientId: string, + clientSecret: string, +): Promise; + // @public export function getBitbucketCloudRequestOptions( config: BitbucketCloudIntegrationConfig, -): { +): Promise<{ headers: Record; -}; +}>; // @public @deprecated export function getBitbucketDefaultBranch( @@ -665,6 +673,7 @@ export type GithubAppConfig = { clientId: string; clientSecret: string; allowedInstallationOwners?: string[]; + publicAccess?: boolean; }; // @public @@ -772,8 +781,30 @@ export type GitLabIntegrationConfig = { commitSigningKey?: string; }; +// @public +export class GoogleGcsIntegration implements ScmIntegration { + constructor(integrationConfig: GoogleGcsIntegrationConfig); + // (undocumented) + get config(): GoogleGcsIntegrationConfig; + // (undocumented) + static factory: ScmIntegrationsFactory; + // (undocumented) + resolveEditUrl(url: string): string; + // (undocumented) + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number | undefined; + }): string; + // (undocumented) + get title(): string; + // (undocumented) + get type(): string; +} + // @public export type GoogleGcsIntegrationConfig = { + host: string; clientEmail?: string; privateKey?: string; }; @@ -831,6 +862,8 @@ export interface IntegrationsByType { // (undocumented) gitlab: ScmIntegrationsGroup; // (undocumented) + googleGcs: ScmIntegrationsGroup; + // (undocumented) harness: ScmIntegrationsGroup; } @@ -1101,6 +1134,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry { // (undocumented) get gitlab(): ScmIntegrationsGroup; // (undocumented) + get googleGcs(): ScmIntegrationsGroup; + // (undocumented) get harness(): ScmIntegrationsGroup; // (undocumented) list(): ScmIntegration[]; diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index 0e0c181861..7f75ab44fc 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -43,6 +43,7 @@ import { AzureBlobStorageIntegrationConfig, AzureBlobStorageIntergation, } from './azureBlobStorage'; +import { GoogleGcsIntegration, GoogleGcsIntegrationConfig } from './googleGcs'; describe('ScmIntegrations', () => { const awsS3 = new AwsS3Integration({ @@ -93,6 +94,10 @@ describe('ScmIntegrations', () => { host: 'harness.local', } as HarnessIntegrationConfig); + const googleGcs = new GoogleGcsIntegration({ + host: 'storage.cloud.google.com', + } as GoogleGcsIntegrationConfig); + const i = new ScmIntegrations({ awsS3: basicIntegrations([awsS3], item => item.config.host), awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host), @@ -108,6 +113,7 @@ describe('ScmIntegrations', () => { github: basicIntegrations([github], item => item.config.host), gitlab: basicIntegrations([gitlab], item => item.config.host), gitea: basicIntegrations([gitea], item => item.config.host), + googleGcs: basicIntegrations([googleGcs], item => item.config.host), harness: basicIntegrations([harness], item => item.config.host), }); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index f11cb3c17e..573975da0e 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -30,6 +30,7 @@ import { ScmIntegrationRegistry } from './registry'; import { GiteaIntegration } from './gitea'; import { HarnessIntegration } from './harness/HarnessIntegration'; import { AzureBlobStorageIntergation } from './azureBlobStorage'; +import { GoogleGcsIntegration } from './googleGcs/GoogleGcsIntegration'; /** * The set of supported integrations. @@ -51,6 +52,7 @@ export interface IntegrationsByType { github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; gitea: ScmIntegrationsGroup; + googleGcs: ScmIntegrationsGroup; harness: ScmIntegrationsGroup; } @@ -75,6 +77,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry { github: GithubIntegration.factory({ config }), gitlab: GitLabIntegration.factory({ config }), gitea: GiteaIntegration.factory({ config }), + googleGcs: GoogleGcsIntegration.factory({ config }), harness: HarnessIntegration.factory({ config }), }); } @@ -130,6 +133,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry { return this.byType.gitea; } + get googleGcs(): ScmIntegrationsGroup { + return this.byType.googleGcs; + } + get harness(): ScmIntegrationsGroup { return this.byType.harness; } diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index 601057e499..fbd6747f56 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -73,7 +73,7 @@ describe('azure core', () => { }, { url: 'com/a/b/blob/master/path/to/c.yaml', - error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml', + error: 'Invalid URL', }, ])('should handle error path %#', ({ url, error }) => { expect(() => getAzureFileFetchUrl(url)).toThrow(error); diff --git a/packages/integration/src/bitbucketCloud/config.test.ts b/packages/integration/src/bitbucketCloud/config.test.ts index 2c68059196..57e8803a14 100644 --- a/packages/integration/src/bitbucketCloud/config.test.ts +++ b/packages/integration/src/bitbucketCloud/config.test.ts @@ -22,70 +22,105 @@ import { readBitbucketCloudIntegrationConfigs, } from './config'; +// Mock constants +const BITBUCKET_CLOUD_HOST = 'bitbucket.org'; +const BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0'; + +async function buildFrontendConfig( + data: Partial, +): Promise { + const fullSchema = await loadConfigSchema({ + dependencies: ['@backstage/integration'], + }); + const serializedSchema = fullSchema.serialize() as { + schemas: { value: { properties?: { integrations?: object } } }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // only include schemas that apply to integrations + schemas: serializedSchema.schemas.filter( + s => s.value?.properties?.integrations, + ), + }, + }); + const processed = schema.process( + [{ data: { integrations: { bitbucketCloud: [data] } }, context: 'app' }], + { visibility: ['frontend'] }, + ); + return new ConfigReader(processed[0].data as any); +} + describe('readBitbucketCloudIntegrationConfig', () => { function buildConfig(data: Partial): Config { return new ConfigReader(data); } - async function buildFrontendConfig( - data: Partial, - ): Promise { - const fullSchema = await loadConfigSchema({ - dependencies: ['@backstage/integration'], - }); - const serializedSchema = fullSchema.serialize() as { - schemas: { value: { properties?: { integrations?: object } } }[]; - }; - const schema = await loadConfigSchema({ - serialized: { - ...serializedSchema, // only include schemas that apply to integrations - schemas: serializedSchema.schemas.filter( - s => s.value?.properties?.integrations, - ), - }, - }); - const processed = schema.process( - [{ data: { integrations: { bitbucketCloud: [data] } }, context: 'app' }], - { visibility: ['frontend'] }, - ); - return new ConfigReader(processed[0].data as any); - } - it('reads all values', () => { const output = readBitbucketCloudIntegrationConfig( buildConfig({ username: 'u', - appPassword: '\n\n\np', + token: 't', }), ); expect(output).toEqual({ - apiBaseUrl: 'https://api.bitbucket.org/2.0', - appPassword: 'p', - host: 'bitbucket.org', + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, username: 'u', + token: 't', }); }); it('rejects funky configs', () => { const valid: any = { username: 'u', - appPassword: 'p', + token: 't', }; expect(() => readBitbucketCloudIntegrationConfig( buildConfig({ ...valid, username: 7 }), ), ).toThrow(/username/); + expect(() => + readBitbucketCloudIntegrationConfig(buildConfig({ ...valid, token: 7 })), + ).toThrow(/token/); + }); + + it('reads OAuth configuration', () => { + const output = readBitbucketCloudIntegrationConfig( + buildConfig({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }), + ); + expect(output).toEqual({ + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }); + }); + + it('rejects incomplete OAuth configuration', () => { expect(() => readBitbucketCloudIntegrationConfig( - buildConfig({ ...valid, appPassword: 7 }), + buildConfig({ + clientId: 'my-client-id', + }), ), - ).toThrow(/appPassword/); + ).toThrow(/incomplete OAuth configuration/); + + expect(() => + readBitbucketCloudIntegrationConfig( + buildConfig({ + clientSecret: 'my-client-secret', + }), + ), + ).toThrow(/incomplete OAuth configuration/); }); it('credentials hidden on the frontend', async () => { const frontendConfig = await buildFrontendConfig({ - appPassword: 'p', + token: 't', username: 'u', }); expect( @@ -95,11 +130,94 @@ describe('readBitbucketCloudIntegrationConfig', () => { ), ).toEqual([ { - apiBaseUrl: 'https://api.bitbucket.org/2.0', - host: 'bitbucket.org', + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, }, ]); }); + + it('OAuth credentials hidden on the frontend', async () => { + const frontendConfig = await buildFrontendConfig({ + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }); + expect( + readBitbucketCloudIntegrationConfigs( + frontendConfig.getOptionalConfigArray('integrations.bitbucketCloud') ?? + [], + ), + ).toEqual([ + { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + }, + ]); + }); + + // TODO: appPassword can be removed once fully + // deprecated by BitBucket on 9th June 2026. + describe('handles deprecated appPassword', () => { + it('reads all values', () => { + const output = readBitbucketCloudIntegrationConfig( + buildConfig({ + appPassword: '\n\np', + username: 'u', + }), + ); + expect(output).toEqual({ + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + appPassword: 'p', + username: 'u', + }); + }); + + it('rejects funky configs', () => { + const valid: any = { + appPassword: 'p', + username: 'u', + }; + expect(() => + readBitbucketCloudIntegrationConfig( + buildConfig({ ...valid, appPassword: 7 }), + ), + ).toThrow(/appPassword/); + }); + + it('rejects if misconfigured', () => { + const valid: any = { + appPassword: 'p', + token: 't', + username: 'u', + }; + expect(() => + readBitbucketCloudIntegrationConfig( + buildConfig({ ...valid, appPassword: undefined, token: undefined }), + ), + ).toThrow( + /must be configured with as username and either a token or an appPassword/, + ); + }); + + it('credentials hidden on the frontend', async () => { + const frontendConfig = await buildFrontendConfig({ + appPassword: 'p', + username: 'u', + }); + expect( + readBitbucketCloudIntegrationConfigs( + frontendConfig.getOptionalConfigArray( + 'integrations.bitbucketCloud', + ) ?? [], + ), + ).toEqual([ + { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + }, + ]); + }); + }); }); describe('readBitbucketCloudIntegrationConfigs', () => { @@ -114,15 +232,15 @@ describe('readBitbucketCloudIntegrationConfigs', () => { buildConfig([ { username: 'u', - appPassword: 'p', + token: 't', }, ]), ); expect(output).toContainEqual({ apiBaseUrl: 'https://api.bitbucket.org/2.0', - appPassword: 'p', host: 'bitbucket.org', username: 'u', + token: 't', }); }); diff --git a/packages/integration/src/bitbucketCloud/config.ts b/packages/integration/src/bitbucketCloud/config.ts index eb4b5c4904..e47b215f98 100644 --- a/packages/integration/src/bitbucketCloud/config.ts +++ b/packages/integration/src/bitbucketCloud/config.ts @@ -49,9 +49,25 @@ export type BitbucketCloudIntegrationConfig = { /** * The access token to use for requests to Bitbucket Cloud (bitbucket.org). + * + * See https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/ */ token?: string; + /** + * The OAuth client ID for Bitbucket Cloud. + * + * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/ + */ + clientId?: string; + + /** + * The OAuth client secret for Bitbucket Cloud. + * + * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/ + */ + clientSecret?: string; + /** PGP private key for signing commits. */ commitSigningKey?: string; }; @@ -69,14 +85,36 @@ export function readBitbucketCloudIntegrationConfig( const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL; // If config is provided, we assume authenticated access is desired // (as the anonymous one is provided by default). - const username = config.getString('username'); - const appPassword = config.getString('appPassword')?.trim(); + const username = config.getOptionalString('username'); + // TODO: appPassword can be removed once fully + // deprecated by BitBucket on 9th June 2026. + const appPassword = config.getOptionalString('appPassword')?.trim(); + const token = config.getOptionalString('token'); + const clientId = config.getOptionalString('clientId')?.trim(); + const clientSecret = config.getOptionalString('clientSecret')?.trim(); + + // Validate: if username is provided, token or appPassword is required + if (username && !token && !appPassword) { + throw new Error( + `Bitbucket Cloud integration must be configured with as username and either a token or an appPassword.`, + ); + } + + // Validate: OAuth requires both clientId and clientSecret + if ((clientId && !clientSecret) || (clientSecret && !clientId)) { + throw new Error( + `Bitbucket Cloud integration has incomplete OAuth configuration. Both clientId and clientSecret are required.`, + ); + } return { host, apiBaseUrl, username, appPassword, + token, + clientId, + clientSecret, commitSigningKey: config.getOptionalString('commitSigningKey'), }; } diff --git a/packages/integration/src/bitbucketCloud/core.test.ts b/packages/integration/src/bitbucketCloud/core.test.ts index 56a53b924d..4f46b5ad42 100644 --- a/packages/integration/src/bitbucketCloud/core.test.ts +++ b/packages/integration/src/bitbucketCloud/core.test.ts @@ -25,35 +25,103 @@ import { getBitbucketCloudRequestOptions, } from './core'; +// Mock constants +const BITBUCKET_CLOUD_HOST = 'bitbucket.org'; +const BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0'; +const BITBUCKET_CLOUD_OAUTH_TOKEN_URL = + 'https://bitbucket.org/site/oauth2/access_token'; + describe('bitbucketCloud core', () => { const worker = setupServer(); registerMswTestHooks(worker); describe('getBitbucketCloudRequestOptions', () => { - it('insert basic auth when needed', () => { + it('insert basic auth when needed', async () => { + const withUsernameAndToken: BitbucketCloudIntegrationConfig = { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + username: 'some-user@domain.com', + token: 'my-token', + }; + // TODO: appPassword can be removed once fully + // deprecated by BitBucket on 9th June 2026. const withUsernameAndPassword: BitbucketCloudIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, username: 'some-user', appPassword: 'my-secret', }; - const withoutUsernameAndPassword: BitbucketCloudIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', + const withoutUsername: BitbucketCloudIntegrationConfig = { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, }; expect( - ( - getBitbucketCloudRequestOptions(withUsernameAndPassword) - .headers as any - ).Authorization, + (await getBitbucketCloudRequestOptions(withUsernameAndToken)).headers + .Authorization, + ).toEqual('Basic c29tZS11c2VyQGRvbWFpbi5jb206bXktdG9rZW4='); + expect( + (await getBitbucketCloudRequestOptions(withUsernameAndPassword)).headers + .Authorization, ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); expect( - ( - getBitbucketCloudRequestOptions(withoutUsernameAndPassword) - .headers as any - ).Authorization, + (await getBitbucketCloudRequestOptions(withoutUsername)).headers + .Authorization, ).toBeUndefined(); }); + + it('handles OAuth token fetch errors', async () => { + // Test error handling + worker.use( + rest.post(BITBUCKET_CLOUD_OAUTH_TOKEN_URL, (_, res, ctx) => + res(ctx.status(401), ctx.json({ error: 'invalid_client' })), + ), + ); + + const withOAuth: BitbucketCloudIntegrationConfig = { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + }; + + await expect(getBitbucketCloudRequestOptions(withOAuth)).rejects.toThrow( + /Failed to fetch OAuth token/, + ); + }); + + it('uses OAuth Bearer token and caches it', async () => { + // Test OAuth + caching + let callCount = 0; + worker.use( + rest.post(BITBUCKET_CLOUD_OAUTH_TOKEN_URL, (_, res, ctx) => { + callCount++; + return res( + ctx.status(200), + ctx.json({ + access_token: 'test-oauth-token', + expires_in: 3600, + }), + ); + }), + ); + + const withOAuth: BitbucketCloudIntegrationConfig = { + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + }; + + // First call should fetch token + const result1 = await getBitbucketCloudRequestOptions(withOAuth); + expect(result1.headers.Authorization).toEqual('Bearer test-oauth-token'); + expect(callCount).toBe(1); + + // Second call should use cached token (proves caching works) + const result2 = await getBitbucketCloudRequestOptions(withOAuth); + expect(result2.headers.Authorization).toEqual('Bearer test-oauth-token'); + expect(callCount).toBe(1); // Still 1, proving cache was used + }); }); describe('getBitbucketCloudFileFetchUrl', () => { diff --git a/packages/integration/src/bitbucketCloud/core.ts b/packages/integration/src/bitbucketCloud/core.ts index 37feb5dad6..9beb3bd748 100644 --- a/packages/integration/src/bitbucketCloud/core.ts +++ b/packages/integration/src/bitbucketCloud/core.ts @@ -17,6 +17,100 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { BitbucketCloudIntegrationConfig } from './config'; +import { DateTime } from 'luxon'; + +type OAuthTokenData = { + token: string; + expiresAt: DateTime; +}; + +// In-memory token cache (single entry since there's only one Bitbucket Cloud integration) +let cachedToken: OAuthTokenData | undefined; + +// Track in-flight token refresh request to prevent concurrent fetches +let refreshPromise: Promise | undefined; + +/** + * Fetches an OAuth access token from Bitbucket Cloud using client credentials flow. + * Tokens are cached with a 10-minute grace period to account for clock skew. + * Implements concurrent refresh protection to prevent multiple simultaneous token requests. + * + * @param clientId - OAuth client ID + * @param clientSecret - OAuth client secret + * @public + */ +export async function getBitbucketCloudOAuthToken( + clientId: string, + clientSecret: string, +): Promise { + // Check cache + if (cachedToken && DateTime.now() < cachedToken.expiresAt) { + return cachedToken.token; + } + + // Check if there's already a refresh in progress + if (refreshPromise) { + return refreshPromise; + } + + // Start a new token fetch and track it + refreshPromise = (async () => { + try { + // Fetch new token + const credentials = Buffer.from( + `${clientId}:${clientSecret}`, + 'utf8', + ).toString('base64'); + + const response = await fetch( + 'https://bitbucket.org/site/oauth2/access_token', + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${credentials}`, + }, + body: 'grant_type=client_credentials', + }, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch OAuth token from Bitbucket Cloud: ${response.status} ${response.statusText}`, + ); + } + + const data = await response.json(); + + if (!data.access_token) { + throw new Error('OAuth token response missing access_token field'); + } + + // Calculate expiration with 10-minute grace period + const expiresIn = data.expires_in || 3600; // Default to 1 hour + const expiresAt = DateTime.now() + .plus({ seconds: expiresIn }) + .minus({ minutes: 10 }); + + // Cache the token + cachedToken = { + token: data.access_token, + expiresAt, + }; + + return data.access_token; + } catch (error) { + throw new Error( + `Failed to fetch OAuth token for Bitbucket Cloud: ${error}`, + ); + } finally { + // Clean up the in-flight promise tracking + refreshPromise = undefined; + } + })(); + + return refreshPromise; +} /** * Given a URL pointing to a path on a provider, returns the default branch. @@ -34,7 +128,7 @@ export async function getBitbucketCloudDefaultBranch( const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`; const response = await fetch( branchUrl, - getBitbucketCloudRequestOptions(config), + await getBitbucketCloudRequestOptions(config), ); if (!response.ok) { @@ -117,24 +211,39 @@ export function getBitbucketCloudFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. + * Returns headers for authenticating with Bitbucket Cloud. + * Supports OAuth (clientId/clientSecret), username/token, and username/appPassword auth. * * @param config - The relevant provider config * @public */ -export function getBitbucketCloudRequestOptions( +export async function getBitbucketCloudRequestOptions( config: BitbucketCloudIntegrationConfig, -): { headers: Record } { +): Promise<{ + headers: Record; +}> { const headers: Record = {}; - if (config.username && config.appPassword) { + // OAuth authentication (clientId/clientSecret) + if (config.clientId && config.clientSecret) { + const token = await getBitbucketCloudOAuthToken( + config.clientId, + config.clientSecret, + ); + headers.Authorization = `Bearer ${token}`; + return { headers }; + } + + // Basic authentication (username + token/appPassword) + // TODO: appPassword can be removed once fully + // deprecated by BitBucket on 9th June 2026. + if (config.username && (config.token ?? config.appPassword)) { const buffer = Buffer.from( - `${config.username}:${config.appPassword}`, + `${config.username}:${config.token ?? config.appPassword}`, 'utf8', ); headers.Authorization = `Basic ${buffer.toString('base64')}`; } - return { - headers, - }; + return { headers }; } diff --git a/packages/integration/src/bitbucketCloud/index.ts b/packages/integration/src/bitbucketCloud/index.ts index 7119d3ea90..d0c11136b3 100644 --- a/packages/integration/src/bitbucketCloud/index.ts +++ b/packages/integration/src/bitbucketCloud/index.ts @@ -24,5 +24,6 @@ export { getBitbucketCloudDefaultBranch, getBitbucketCloudDownloadUrl, getBitbucketCloudFileFetchUrl, + getBitbucketCloudOAuthToken, getBitbucketCloudRequestOptions, } from './core'; diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts index 0468e96e14..fefccf22f6 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts @@ -563,4 +563,330 @@ describe('SingleInstanceGithubCredentialsProvider tests', () => { 2, ); }); + + describe('public access', () => { + it('should use an installation token when public access is enabled and owner is not in allowed list', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + allowedInstallationOwners: ['other-org'], + publicAccess: true, + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'all', + account: { + login: 'other-org', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'public_access_token', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + const { token, headers, type } = await githubProvider.getCredentials({ + url: 'https://github.com/some-public-org/some-repo', + }); + + expect(type).toEqual('app'); + expect(token).toEqual('public_access_token'); + expect(headers).toEqual({ Authorization: 'Bearer public_access_token' }); + }); + + it('should use an installation token when public access is enabled and no installation exists for owner', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + publicAccess: true, + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 42, + repository_selection: 'all', + account: { + login: 'installed-org', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'public_installation_token', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + const { token, headers, type } = await githubProvider.getCredentials({ + url: 'https://github.com/non-installed-org/some-repo', + }); + + expect(type).toEqual('app'); + expect(token).toEqual('public_installation_token'); + expect(headers).toEqual({ + Authorization: 'Bearer public_installation_token', + }); + }); + + it('should not use public access when normal installation credentials are available', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + publicAccess: true, + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'all', + account: { + login: 'backstage', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'normal_token', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + const { token, type } = await githubProvider.getCredentials({ + url: 'https://github.com/backstage/repo', + }); + + expect(type).toEqual('app'); + expect(token).toEqual('normal_token'); + // createInstallationAccessToken should only be called once for the normal flow + expect(octokit.apps.createInstallationAccessToken).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should fall back to configured token when public access fails', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + allowedInstallationOwners: ['other-org'], + publicAccess: true, + }, + ], + token: 'fallback_token', + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [], + } as unknown as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + const { token, type } = await githubProvider.getCredentials({ + url: 'https://github.com/some-org/repo', + }); + + expect(type).toEqual('token'); + expect(token).toEqual('fallback_token'); + }); + + it('should return undefined when public access is disabled and no installation exists', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + allowedInstallationOwners: ['other-org'], + // publicAccess is not set (defaults to false) + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'all', + account: { + login: 'other-org', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + const { token, headers } = await githubProvider.getCredentials({ + url: 'https://github.com/some-org/repo', + }); + + expect(token).toBeUndefined(); + expect(headers).toBeUndefined(); + }); + + it('should cache public access tokens separately from regular tokens', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + allowedInstallationOwners: ['installed-org'], + publicAccess: true, + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'all', + account: { + login: 'installed-org', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValue({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'public_token', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + // First call for a public org + await githubProvider.getCredentials({ + url: 'https://github.com/public-org/repo', + }); + + // Second call for the same public org should use cached token + await githubProvider.getCredentials({ + url: 'https://github.com/public-org/repo', + }); + + // createInstallationAccessToken should only be called once due to caching + expect(octokit.apps.createInstallationAccessToken).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should use public access with multiple apps when only one has publicAccess enabled', async () => { + const githubProvider = SingleInstanceGithubCredentialsProvider.create({ + host: 'github.com', + apps: [ + { + appId: 1, + privateKey: 'privateKey', + webhookSecret: '123', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + allowedInstallationOwners: ['org-1'], + // publicAccess not set, defaults to false + }, + { + appId: 2, + privateKey: 'privateKey2', + webhookSecret: '456', + clientId: 'CLIENT_ID_2', + clientSecret: 'CLIENT_SECRET_2', + allowedInstallationOwners: ['org-2'], + publicAccess: true, + }, + ], + }); + + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'all', + account: { + login: 'org-2', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'public_access_from_app_2', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + const { token, type } = await githubProvider.getCredentials({ + url: 'https://github.com/unknown-org/repo', + }); + + expect(type).toEqual('app'); + expect(token).toEqual('public_access_from_app_2'); + }); + }); }); diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index d2c1e37a65..40d166b223 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -100,6 +100,7 @@ class GithubAppManager { private readonly baseAuthConfig: { appId: number; privateKey: string }; private readonly cache = new Cache(); private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations + public readonly publicAccess: boolean; constructor(config: GithubAppConfig, baseUrl?: string) { this.allowedInstallationOwners = config.allowedInstallationOwners?.map( @@ -116,6 +117,7 @@ class GithubAppManager { authStrategy: createAppAuth, auth: this.baseAuthConfig, }); + this.publicAccess = config.publicAccess ?? false; } async getInstallationCredentials( @@ -170,6 +172,30 @@ class GithubAppManager { }); } + async getPublicInstallationToken(): Promise<{ accessToken: string }> { + const [installation] = await this.getInstallations(); + + if (!installation) { + throw new Error(`No installation found for public app`); + } + + return this.cache.getOrCreateToken( + `public:${installation.id}`, + undefined, + async () => { + const result = await this.appClient.apps.createInstallationAccessToken({ + installation_id: installation.id, + headers: HEADERS, + }); + + return { + token: result.data.token, + expiresAt: DateTime.fromISO(result.data.expires_at), + }; + }, + ); + } + getInstallations(): Promise< RestEndpointMethodTypes['apps']['listInstallations']['response']['data'] > { @@ -185,12 +211,14 @@ class GithubAppManager { inst.account.login?.toLocaleLowerCase('en-US') === owner.toLocaleLowerCase('en-US'), ); + if (installation) { return { installationId: installation.id, suspended: Boolean(installation.suspended_by), }; } + const notFoundError = new Error( `No app installation found for ${owner} in ${this.baseAuthConfig.appId}`, ); @@ -245,10 +273,26 @@ export class GithubAppCredentialsMux { const result = results.find( resultItem => resultItem.credentials?.accessToken, ); + if (result) { return result.credentials!.accessToken; } + // If there was no token returned, then let's find a public access app and use an installation to get a token. + const publicAccessApp = this.apps.find(app => app.publicAccess); + if (publicAccessApp) { + const publicResult = await publicAccessApp + .getPublicInstallationToken() + .then( + credentials => ({ credentials, error: undefined }), + error => ({ credentials: undefined, error }), + ); + + if (publicResult.credentials?.accessToken) { + return publicResult.credentials.accessToken; + } + } + const errors = results.map(r => r.error); const notNotFoundError = errors.find(err => err?.name !== 'NotFoundError'); if (notNotFoundError) { diff --git a/packages/integration/src/github/config.ts b/packages/integration/src/github/config.ts index 74a0267d62..f755b84c2a 100644 --- a/packages/integration/src/github/config.ts +++ b/packages/integration/src/github/config.ts @@ -109,6 +109,10 @@ export type GithubAppConfig = { * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples */ allowedInstallationOwners?: string[]; + /** + * If true, then an installation token will be issued for access when no other token is available. + */ + publicAccess?: boolean; }; /** @@ -133,6 +137,7 @@ export function readGithubIntegrationConfig( allowedInstallationOwners: c.getOptionalStringArray( 'allowedInstallationOwners', ), + publicAccess: c.getOptionalBoolean('publicAccess'), })); if (!isValidHost(host)) { diff --git a/packages/integration/src/googleGcs/GoogleGcsIntegration.test.ts b/packages/integration/src/googleGcs/GoogleGcsIntegration.test.ts new file mode 100644 index 0000000000..5666a0aa5e --- /dev/null +++ b/packages/integration/src/googleGcs/GoogleGcsIntegration.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { GoogleGcsIntegration } from './GoogleGcsIntegration'; + +describe('GoogleGcsIntegration', () => { + it('has a working factory', () => { + const integrations = GoogleGcsIntegration.factory({ + config: new ConfigReader({ + integrations: { + googleGcs: { + clientEmail: 'someone@example.com', + privateKey: 'fake-key', + }, + }, + }), + }); + expect(integrations.list().length).toBe(1); + expect(integrations.list()[0].config.host).toBe('storage.cloud.google.com'); + }); + + it('returns default integration when no config', () => { + const integrations = GoogleGcsIntegration.factory({ + config: new ConfigReader({ + integrations: {}, + }), + }); + expect(integrations.list().length).toBe(1); + expect(integrations.list()[0].config.host).toBe('storage.cloud.google.com'); + }); + + it('returns the basics', () => { + const integration = new GoogleGcsIntegration({ + host: 'storage.cloud.google.com', + }); + expect(integration.type).toBe('googleGcs'); + expect(integration.title).toBe('storage.cloud.google.com'); + }); + + describe('resolveUrl', () => { + it('works for valid urls', () => { + const integration = new GoogleGcsIntegration({ + host: 'storage.cloud.google.com', + }); + + expect( + integration.resolveUrl({ + url: 'https://storage.cloud.google.com/bucket/catalog-info.yaml', + base: 'https://storage.cloud.google.com/bucket/catalog-info.yaml', + }), + ).toBe('https://storage.cloud.google.com/bucket/catalog-info.yaml'); + }); + }); + + it('resolve edit URL', () => { + const integration = new GoogleGcsIntegration({ + host: 'storage.cloud.google.com', + }); + + expect( + integration.resolveEditUrl( + 'https://storage.cloud.google.com/bucket/catalog-info.yaml', + ), + ).toBe('https://storage.cloud.google.com/bucket/catalog-info.yaml'); + }); +}); diff --git a/packages/integration/src/googleGcs/GoogleGcsIntegration.ts b/packages/integration/src/googleGcs/GoogleGcsIntegration.ts new file mode 100644 index 0000000000..11642a231c --- /dev/null +++ b/packages/integration/src/googleGcs/GoogleGcsIntegration.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; +import { + GoogleGcsIntegrationConfig, + readGoogleGcsIntegrationConfig, + GOOGLE_GCS_HOST, +} from './config'; + +/** + * A Google Cloud Storage based integration. + * + * @public + */ +export class GoogleGcsIntegration implements ScmIntegration { + static factory: ScmIntegrationsFactory = ({ + config, + }) => { + const gcsConfig = config.has('integrations.googleGcs') + ? readGoogleGcsIntegrationConfig( + config.getConfig('integrations.googleGcs'), + ) + : { host: GOOGLE_GCS_HOST }; + + return basicIntegrations( + [new GoogleGcsIntegration(gcsConfig)], + i => i.config.host, + ); + }; + + get type(): string { + return 'googleGcs'; + } + + get title(): string { + return this.integrationConfig.host; + } + + get config(): GoogleGcsIntegrationConfig { + return this.integrationConfig; + } + + constructor(private readonly integrationConfig: GoogleGcsIntegrationConfig) {} + + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number | undefined; + }): string { + return defaultScmResolveUrl(options); + } + + resolveEditUrl(url: string): string { + return url; + } +} diff --git a/packages/integration/src/googleGcs/config.test.ts b/packages/integration/src/googleGcs/config.test.ts index 892440c64a..d074cb7129 100644 --- a/packages/integration/src/googleGcs/config.test.ts +++ b/packages/integration/src/googleGcs/config.test.ts @@ -33,13 +33,16 @@ describe('readGoogleGcsIntegrationConfig', () => { }), ); expect(output).toEqual({ + host: 'storage.cloud.google.com', privateKey: 'fake-key', clientEmail: 'someone@example.com', }); }); - it('does not fail when config is not set', () => { + it('returns default config when config is not set', () => { const output = readGoogleGcsIntegrationConfig(buildConfig({})); - expect(output).toEqual({}); + expect(output).toEqual({ + host: 'storage.cloud.google.com', + }); }); }); diff --git a/packages/integration/src/googleGcs/config.ts b/packages/integration/src/googleGcs/config.ts index 3c03204e4c..9421b9dfba 100644 --- a/packages/integration/src/googleGcs/config.ts +++ b/packages/integration/src/googleGcs/config.ts @@ -16,12 +16,22 @@ import { Config } from '@backstage/config'; +/** + * The default Google Cloud Storage host. + * + */ +export const GOOGLE_GCS_HOST = 'storage.cloud.google.com'; + /** * The configuration parameters for a single Google Cloud Storage provider. * * @public */ export type GoogleGcsIntegrationConfig = { + /** + * The host of the target that this matches on. + */ + host: string; /** * Service account email used to authenticate requests. */ @@ -42,15 +52,19 @@ export function readGoogleGcsIntegrationConfig( config: Config, ): GoogleGcsIntegrationConfig { if (!config) { - return {}; + return { host: GOOGLE_GCS_HOST }; } if (!config.has('clientEmail') && !config.has('privateKey')) { - return {}; + return { host: GOOGLE_GCS_HOST }; } const privateKey = config.getString('privateKey').split('\\n').join('\n'); - const clientEmail = config.getString('clientEmail'); - return { clientEmail: clientEmail, privateKey: privateKey }; + + return { + host: GOOGLE_GCS_HOST, + clientEmail, + privateKey, + }; } diff --git a/packages/integration/src/googleGcs/index.ts b/packages/integration/src/googleGcs/index.ts index 3d7beae59d..c30138fc3e 100644 --- a/packages/integration/src/googleGcs/index.ts +++ b/packages/integration/src/googleGcs/index.ts @@ -16,3 +16,4 @@ export { readGoogleGcsIntegrationConfig } from './config'; export type { GoogleGcsIntegrationConfig } from './config'; +export { GoogleGcsIntegration } from './GoogleGcsIntegration'; diff --git a/packages/integration/src/setupTests.ts b/packages/integration/src/setupTests.ts index d3232290a7..40bb41eb2a 100644 --- a/packages/integration/src/setupTests.ts +++ b/packages/integration/src/setupTests.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export {}; +global.setImmediate = require('timers').setImmediate; +global.clearImmediate = require('timers').clearImmediate; diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index c85546dd9d..3951c582d3 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,113 @@ # @backstage/repo-tools +## 0.16.2 + +### Patch Changes + +- 498f9dd: Fixed help text for `backstage-repo-tools package schema openapi generate` command. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/cli-common@0.1.17 + +## 0.16.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- 85895f9: Updates OpenAPI generator templates to preserve original property names (like 'group-name', 'user-id') from OpenAPI specs when propertyNaming=original is specified. Previously, these were always converted to camelCase regardless of the propertyNaming setting. + + - Updates modelGeneric.mustache templates in both client and server generators + - Updates modelTaggedUnion.mustache templates in both client and server generators + - Uses {{baseName}} when available, falls back to {{name}} for backward compatibility + - Maintains backward compatibility - no changes when propertyNaming=original is not used + +- 2bae83a: Bump `@microsoft/api-documenter` and `@microsoft/api-extractor` to latest versions. +- d1e38a7: Properly create workspace in OS temporary directory for `generate-patch` command +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/cli-node@0.2.16 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/cli-common@0.1.16 + - @backstage/config-loader@1.10.7 + +## 0.16.1-next.2 + +### Patch Changes + +- 2bae83a: Bump `@microsoft/api-documenter` and `@microsoft/api-extractor` to latest versions. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config-loader@1.10.7-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-node@0.2.16-next.1 + - @backstage/errors@1.2.7 + +## 0.16.1-next.1 + +### Patch Changes + +- 688f070: Updated to use new utilities from `@backstage/cli-common`. +- d1e38a7: Properly create workspace in OS temporary directory for `generate-patch` command +- Updated dependencies + - @backstage/cli-node@0.2.16-next.1 + - @backstage/cli-common@0.1.16-next.1 + +## 0.16.1-next.0 + +### Patch Changes + +- 85895f9: Updates OpenAPI generator templates to preserve original property names (like 'group-name', 'user-id') from OpenAPI specs when propertyNaming=original is specified. Previously, these were always converted to camelCase regardless of the propertyNaming setting. + + - Updates modelGeneric.mustache templates in both client and server generators + - Updates modelTaggedUnion.mustache templates in both client and server generators + - Uses {{baseName}} when available, falls back to {{name}} for backward compatibility + - Maintains backward compatibility - no changes when propertyNaming=original is not used + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/cli-node@0.2.16-next.0 + - @backstage/config-loader@1.10.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## 0.16.0 + +### Minor Changes + +- 11c61f2: The `package-docs` command will now automatically use a `typedoc.json` file if one exists at the root of your project. + +### Patch Changes + +- 8f56eae: Updated knip-reports to detect dependencies in dev/alpha pattern +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/config-loader@1.10.6 + - @backstage/cli-node@0.2.15 + - @backstage/catalog-model@1.7.6 + +## 0.16.0-next.2 + +### Minor Changes + +- 11c61f2: The `package-docs` command will now automatically use a `typedoc.json` file if one exists at the root of your project. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.2 + +## 0.15.4-next.1 + +### Patch Changes + +- 8f56eae: Updated knip-reports to detect dependencies in dev/alpha pattern +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 0.15.4-next.0 ### Patch Changes diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index acde92d742..6b6f2ae710 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -160,7 +160,11 @@ Options: Usage: backstage-repo-tools package schema openapi generate [options] Options: + --client-additional-properties [properties] --client-package [package] + --server + --server-additional-properties [properties] + --watch -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index f3789e14c1..60cb036b50 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.15.4-next.0", + "version": "0.16.2", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" @@ -51,8 +51,8 @@ "@backstage/errors": "workspace:^", "@electric-sql/pglite": "^0.3.0", "@manypkg/get-packages": "^1.1.3", - "@microsoft/api-documenter": "^7.25.7", - "@microsoft/api-extractor": "^7.47.2", + "@microsoft/api-documenter": "^7.28.1", + "@microsoft/api-extractor": "^7.55.1", "@openapitools/openapi-generator-cli": "^2.7.0", "@stoplight/spectral-core": "^1.18.0", "@stoplight/spectral-formatters": "^1.1.0", @@ -90,7 +90,7 @@ "@backstage/cli": "workspace:^", "@backstage/types": "workspace:^", "@types/is-glob": "^4.0.2", - "@types/node": "^20.16.0", + "@types/node": "^22.13.14", "@types/prettier": "^2.0.0", "typedoc": "^0.28.0" }, diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts b/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts index d7304553c0..a51c9ed17b 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { spawnSync } from 'child_process'; +import { run, ExitCodeError } from '@backstage/cli-common'; import { paths as cliPaths } from '../../../lib/paths'; /** @@ -30,21 +30,26 @@ import { paths as cliPaths } from '../../../lib/paths'; */ export async function generateTypeDeclarations(tsconfigFilePath: string) { await fs.remove(cliPaths.resolveTargetRoot('dist-types')); - const { status } = spawnSync( - 'yarn', - [ - 'tsc', - ['--project', tsconfigFilePath], - ['--skipLibCheck', 'false'], - ['--incremental', 'false'], - ].flat(), - { - stdio: 'inherit', - shell: true, - cwd: cliPaths.targetRoot, - }, - ); - if (status !== 0) { - process.exit(status || undefined); + try { + await run( + [ + 'yarn', + 'tsc', + '--project', + tsconfigFilePath, + '--skipLibCheck', + 'false', + '--incremental', + 'false', + ], + { + cwd: cliPaths.targetRoot, + }, + ).waitForExit(); + } catch (error) { + if (error instanceof ExitCodeError) { + process.exit(error.code); + } + throw error; } } diff --git a/packages/repo-tools/src/commands/generate-patch/generate-patch.ts b/packages/repo-tools/src/commands/generate-patch/generate-patch.ts index 10b31e0584..90cbe7f322 100644 --- a/packages/repo-tools/src/commands/generate-patch/generate-patch.ts +++ b/packages/repo-tools/src/commands/generate-patch/generate-patch.ts @@ -98,7 +98,9 @@ export default async ( throw new Error(`Could not find package ${packageArg} in source repo`); } - const tmpDir = await fs.mkdtemp(os.tmpdir()); + const tmpDir = await fs.mkdtemp( + joinPath(os.tmpdir(), 'backstage-repo-tools-generate-patch-'), + ); const ctx: PatchContext = { sourceRepo, targetRepo, diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 01288056c7..dab7d6df0d 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -44,24 +44,26 @@ function registerPackageCommand(program: Command) { openApiCommand .command('generate') + .description( + 'Command to generate a client and/or a server stub from an OpenAPI spec.', + ) .option( '--client-package [package]', 'Top-level path to where the client should be generated, ie packages/catalog-client.', ) - .option('--server') - .description( - 'Command to generate a client and/or a server stub from an OpenAPI spec.', - ) - .option('--client-additional-properties [properties]') - .description( + .option('--server', 'Generate server stub') + .option( + '--client-additional-properties [properties]', 'Additional properties that can be passed to @openapitools/openapi-generator-cli', ) - .option('--server-additional-properties [properties]') - .description( + .option( + '--server-additional-properties [properties]', 'Additional properties that can be passed to @openapitools/openapi-generator-cli', ) - .option('--watch') - .description('Watch the OpenAPI spec for changes and regenerate on save.') + .option( + '--watch', + 'Watch the OpenAPI spec for changes and regenerate on save.', + ) .action(lazy(() => import('./package/schema/openapi/generate'), 'command')); openApiCommand diff --git a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts index 68bb0a9835..af3e32d2f4 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts @@ -58,7 +58,7 @@ async function generateKnipConfig({ knipConfigPath }: KnipConfigOptions) { workspaces: { '.': {}, '{packages,plugins}/*': { - entry: ['dev/index.{ts,tsx}', 'src/index.{ts,tsx}'], + entry: ['dev/**/*.{ts,tsx}', 'src/index.{ts,tsx}'], ignore: [ '.eslintrc.js', 'config.d.ts', diff --git a/packages/repo-tools/src/commands/package-docs/command.ts b/packages/repo-tools/src/commands/package-docs/command.ts index 174a1d949d..28255193f0 100644 --- a/packages/repo-tools/src/commands/package-docs/command.ts +++ b/packages/repo-tools/src/commands/package-docs/command.ts @@ -23,6 +23,7 @@ import { mkdirp } from 'fs-extra'; import { PackageDocsCache } from './Cache'; import { Lockfile } from '@backstage/cli-node'; import { glob } from 'glob'; +import { existsSync } from 'fs'; const limit = pLimit(8); @@ -220,6 +221,9 @@ export default async function packageDocs(paths: string[] = [], opts: any) { ...HIGHLIGHT_LANGUAGES.flatMap(e => ['--highlightLanguages', e]), '--out', cliPaths.resolveTargetRoot('type-docs'), + ...(existsSync(cliPaths.resolveTargetRoot('typedoc.json')) + ? ['--options', cliPaths.resolveTargetRoot('typedoc.json')] + : []), ].join(' '), { cwd: cliPaths.targetRoot, diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 8a8817c721..7402342607 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -14,21 +14,21 @@ * limitations under the License. */ +import { resolvePackagePath } from '@backstage/backend-plugin-api'; import chalk from 'chalk'; +import fs from 'fs-extra'; import { resolve } from 'path'; +import { exec } from '../../../../../lib/exec'; import { OPENAPI_IGNORE_FILES, OUTPUT_PATH, } from '../../../../../lib/openapi/constants'; -import { paths as cliPaths } from '../../../../../lib/paths'; -import fs from 'fs-extra'; -import { exec } from '../../../../../lib/exec'; -import { resolvePackagePath } from '@backstage/backend-plugin-api'; +import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; import { getPathToCurrentOpenApiSpec, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; -import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; +import { paths as cliPaths } from '../../../../../lib/paths'; async function generate( outputDirectory: string, diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts index 515d1d68ed..34c8589d54 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts @@ -68,6 +68,8 @@ async function lint(directoryPath: string, config?: { strict: boolean }) { 'operation-tags': 'off', 'hosts-https-only-oas3': 'off', 'no-unknown-error-format': 'off', + // Disabled as it currently gives false positives when 'example' is used as a property name. + 'oas3-valid-media-example': 'off', }, }, ], diff --git a/packages/repo-tools/templates/typescript-backstage-client/modelGeneric.mustache b/packages/repo-tools/templates/typescript-backstage-client/modelGeneric.mustache index d01ea29260..d246837155 100644 --- a/packages/repo-tools/templates/typescript-backstage-client/modelGeneric.mustache +++ b/packages/repo-tools/templates/typescript-backstage-client/modelGeneric.mustache @@ -18,7 +18,7 @@ export {{#isNullable}}type{{/isNullable}}{{^isNullable}}interface{{/isNullable}} * {{{.}}} */ {{/description}} - '{{name}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}}; + '{{#baseName}}{{baseName}}{{/baseName}}{{^baseName}}{{name}}{{/baseName}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}}; {{/vars}} }{{#isNullable}} | null{{/isNullable}} diff --git a/packages/repo-tools/templates/typescript-backstage-client/modelTaggedUnion.mustache b/packages/repo-tools/templates/typescript-backstage-client/modelTaggedUnion.mustache index a3f5e00904..3f5a3baf5e 100644 --- a/packages/repo-tools/templates/typescript-backstage-client/modelTaggedUnion.mustache +++ b/packages/repo-tools/templates/typescript-backstage-client/modelTaggedUnion.mustache @@ -12,7 +12,7 @@ export interface {{classname}} { {{>modelGenericAdditionalProperties}} * {{{.}}} */ {{/description}} - {{name}}{{^required}}?{{/required}}: {{#discriminatorValue}}'{{.}}'{{/discriminatorValue}}{{^discriminatorValue}}{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/discriminatorValue}}{{#isNullable}} | null{{/isNullable}}; + '{{#baseName}}{{baseName}}{{/baseName}}{{^baseName}}{{name}}{{/baseName}}'{{^required}}?{{/required}}: {{#discriminatorValue}}'{{.}}'{{/discriminatorValue}}{{^discriminatorValue}}{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/discriminatorValue}}{{#isNullable}} | null{{/isNullable}}; {{/allVars}} } {{>modelGenericEnums}} diff --git a/packages/repo-tools/templates/typescript-backstage-server/modelGeneric.mustache b/packages/repo-tools/templates/typescript-backstage-server/modelGeneric.mustache index d01ea29260..d246837155 100644 --- a/packages/repo-tools/templates/typescript-backstage-server/modelGeneric.mustache +++ b/packages/repo-tools/templates/typescript-backstage-server/modelGeneric.mustache @@ -18,7 +18,7 @@ export {{#isNullable}}type{{/isNullable}}{{^isNullable}}interface{{/isNullable}} * {{{.}}} */ {{/description}} - '{{name}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}}; + '{{#baseName}}{{baseName}}{{/baseName}}{{^baseName}}{{name}}{{/baseName}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}}; {{/vars}} }{{#isNullable}} | null{{/isNullable}} diff --git a/packages/repo-tools/templates/typescript-backstage-server/modelTaggedUnion.mustache b/packages/repo-tools/templates/typescript-backstage-server/modelTaggedUnion.mustache index a3f5e00904..3f5a3baf5e 100644 --- a/packages/repo-tools/templates/typescript-backstage-server/modelTaggedUnion.mustache +++ b/packages/repo-tools/templates/typescript-backstage-server/modelTaggedUnion.mustache @@ -12,7 +12,7 @@ export interface {{classname}} { {{>modelGenericAdditionalProperties}} * {{{.}}} */ {{/description}} - {{name}}{{^required}}?{{/required}}: {{#discriminatorValue}}'{{.}}'{{/discriminatorValue}}{{^discriminatorValue}}{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/discriminatorValue}}{{#isNullable}} | null{{/isNullable}}; + '{{#baseName}}{{baseName}}{{/baseName}}{{^baseName}}{{name}}{{/baseName}}'{{^required}}?{{/required}}: {{#discriminatorValue}}'{{.}}'{{/discriminatorValue}}{{^discriminatorValue}}{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/discriminatorValue}}{{#isNullable}} | null{{/isNullable}}; {{/allVars}} } {{>modelGenericEnums}} diff --git a/packages/scaffolder-internal/CHANGELOG.md b/packages/scaffolder-internal/CHANGELOG.md index 3b3a6f7c8c..2c3f721b86 100644 --- a/packages/scaffolder-internal/CHANGELOG.md +++ b/packages/scaffolder-internal/CHANGELOG.md @@ -1,5 +1,45 @@ # @internal/scaffolder +## 0.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/plugin-scaffolder-react@1.19.5 + +## 0.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-scaffolder-react@1.19.4 + +## 0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.4-next.0 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.3 + ## 0.0.15-next.0 ### Patch Changes diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 256ec5b5e3..111e76c36a 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/scaffolder", - "version": "0.0.15-next.0", + "version": "0.0.17", "backstage": { "role": "web-library", "inline": true diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index adc7acaab5..530126d6c0 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,169 @@ # techdocs-cli-embedded-app +## 0.2.116 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/plugin-catalog@1.32.2 + - @backstage/cli@0.35.2 + - @backstage/core-components@0.18.5 + - @backstage/plugin-techdocs@1.16.2 + - @backstage/app-defaults@1.7.4 + - @backstage/integration-react@1.2.14 + - @backstage/plugin-techdocs-react@1.3.7 + +## 0.2.116-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/app-defaults@1.7.4-next.0 + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-catalog@1.32.2-next.1 + - @backstage/plugin-techdocs@1.16.2-next.1 + - @backstage/plugin-techdocs-react@1.3.7-next.0 + +## 0.2.116-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.35.2-next.1 + - @backstage/integration-react@1.2.14-next.0 + - @backstage/plugin-techdocs@1.16.2-next.0 + - @backstage/plugin-catalog@1.32.2-next.0 + +## 0.2.116-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/cli@0.35.2-next.0 + - @backstage/app-defaults@1.7.3 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.3 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/integration-react@1.2.13 + - @backstage/test-utils@1.7.14 + - @backstage/theme@0.7.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-techdocs-react@1.3.6 + +## 0.2.115 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/cli@0.35.0 + - @backstage/ui@0.10.0 + - @backstage/plugin-techdocs@1.16.1 + - @backstage/plugin-catalog@1.32.1 + - @backstage/core-components@0.18.4 + - @backstage/test-utils@1.7.14 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/app-defaults@1.7.3 + - @backstage/integration-react@1.2.13 + - @backstage/plugin-techdocs-react@1.3.6 + +## 0.2.115-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/cli@0.35.0-next.2 + - @backstage/plugin-techdocs@1.16.1-next.2 + - @backstage/plugin-catalog@1.32.1-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/ui@0.10.0-next.1 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + +## 0.2.115-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/cli@0.34.6-next.1 + - @backstage/plugin-techdocs@1.16.1-next.1 + +## 0.2.115-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/test-utils@1.7.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-techdocs@1.16.1-next.0 + - @backstage/plugin-catalog@1.32.1-next.0 + - @backstage/cli@0.34.6-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-techdocs-react@1.3.6-next.0 + - @backstage/integration-react@1.2.13-next.0 + - @backstage/app-defaults@1.7.3-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## 0.2.114 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/cli@0.34.5 + - @backstage/plugin-techdocs@1.16.0 + - @backstage/plugin-catalog@1.32.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-techdocs-react@1.3.5 + - @backstage/app-defaults@1.7.2 + - @backstage/integration-react@1.2.12 + - @backstage/test-utils@1.7.13 + +## 0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.2 + - @backstage/plugin-techdocs@1.16.0-next.1 + - @backstage/plugin-catalog@1.32.0-next.1 + - @backstage/cli@0.34.5-next.1 + - @backstage/core-components@0.18.3-next.1 + - @backstage/core-plugin-api@1.11.2-next.1 + - @backstage/core-app-api@1.19.2-next.1 + +## 0.2.114-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + ## 0.2.114-next.0 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index fa2cdf9dfb..93a2a2793b 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.114-next.0", + "version": "0.2.116", "backstage": { "role": "frontend" }, @@ -62,7 +62,7 @@ "@testing-library/user-event": "^14.0.0", "@types/react": "*", "@types/react-dom": "*", - "cross-env": "^7.0.0" + "cross-env": "^10.0.0" }, "bundled": true } diff --git a/packages/techdocs-cli-embedded-app/public/index.html b/packages/techdocs-cli-embedded-app/public/index.html index 18bb3f50a9..196c711417 100644 --- a/packages/techdocs-cli-embedded-app/public/index.html +++ b/packages/techdocs-cli-embedded-app/public/index.html @@ -4,6 +4,7 @@ + ({ useShadowRootElements: jest.fn(() => [ @@ -38,12 +38,11 @@ jest.mock('@backstage/plugin-techdocs-react', () => ({ describe('TechDocsLiveReload', () => { const originalXHR = global.XMLHttpRequest; - let originalLocation: Location; let openSpy: jest.Mock; let sendSpy: jest.Mock; + let reloadPageSpy: jest.SpyInstance; beforeEach(() => { - originalLocation = window.location; openSpy = jest.fn(); sendSpy = jest.fn(function (this: any) { // simulate long-poll response that does NOT trigger reload (epoch unchanged) @@ -65,9 +64,11 @@ describe('TechDocsLiveReload', () => { global.XMLHttpRequest = MockXHR as any; - // Replace window.location with a mutable object for tests - delete (window as any).location; - (window as any).location = { ...originalLocation, reload: jest.fn() }; + // Spy on the utils object's reloadPage method + reloadPageSpy = jest + .spyOn(utils, 'reloadPage') + .mockImplementation(() => {}); + jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); jest.spyOn(window, 'removeEventListener').mockImplementation(() => {}); Object.defineProperty(document, 'visibilityState', { @@ -79,23 +80,17 @@ describe('TechDocsLiveReload', () => { afterEach(() => { global.XMLHttpRequest = originalXHR; jest.restoreAllMocks(); - // restore original window.location - delete (window as any).location; - (window as any).location = originalLocation; }); it('polls livereload endpoint and does not reload when epoch unchanged', async () => { - const reloadSpy = window.location.reload as unknown as jest.Mock; render(); expect(openSpy).toHaveBeenCalledWith('GET', '/.livereload/10/1'); // give microtask queue a tick await new Promise(res => setTimeout(res, 0)); - expect(reloadSpy).not.toHaveBeenCalled(); + expect(reloadPageSpy).not.toHaveBeenCalled(); }); it('reloads when server epoch increases', async () => { - const reloadSpy = window.location.reload as unknown as jest.Mock; - sendSpy.mockImplementation(function (this: any) { setTimeout(() => { (this as any).status = 200; @@ -106,6 +101,6 @@ describe('TechDocsLiveReload', () => { render(); await new Promise(res => setTimeout(res, 0)); - expect(reloadSpy).toHaveBeenCalled(); + expect(reloadPageSpy).toHaveBeenCalled(); }); }); diff --git a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx index 905a339c72..393704da97 100644 --- a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx +++ b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx @@ -17,6 +17,13 @@ import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; import { useEffect, useRef } from 'react'; +/** @internal Exported for testing - allows spying on reloads without spying + * on window.location. + */ +export const utils = { + reloadPage: () => window.location.reload(), +}; + interface TechDocsLiveReloadProps { /** Whether to enable livereload (default: true in development) */ enabled?: boolean; @@ -69,7 +76,7 @@ export const TechDocsLiveReload = ({ reqRef.current = new XMLHttpRequest(); reqRef.current.onloadend = function handleLoadEnd(this: XMLHttpRequest) { if (parseFloat(this.responseText) > epoch) { - window.location.reload(); + utils.reloadPage(); } else { timeoutRef.current = setTimeout(poll, this.status === 200 ? 0 : 3000); } diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index 479ad3161d..dfe728da86 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -49,15 +49,15 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -enum Themes { - LIGHT = 'light', - DARK = 'dark', -} +const Themes = { + LIGHT: 'light', + DARK: 'dark', +} as const; export const TechDocsThemeToggle = () => { const appThemeApi = useApi(appThemeApiRef); const classes = useStyles(); - const [theme, setTheme] = useState( + const [theme, setTheme] = useState<(typeof Themes)[keyof typeof Themes]>( appThemeApi.getActiveThemeId() === Themes.DARK ? Themes.DARK : Themes.LIGHT, ); diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 13413f46c8..c89643f5d1 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,93 @@ # @techdocs/cli +## 1.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/cli-common@0.1.17 + - @backstage/plugin-techdocs-node@1.14.0 + +## 1.10.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## 1.10.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.1 + - @backstage/plugin-techdocs-node@1.13.11-next.0 + +## 1.10.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/cli-common@0.1.16 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.13.10 + +## 1.10.3 + +### Patch Changes + +- 43629b1: Updated to use new utilities from `@backstage/cli-common`. +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/plugin-techdocs-node@1.13.10 + - @backstage/cli-common@0.1.16 + +## 1.10.3-next.2 + +### Patch Changes + +- 2bae83a: Bumped dev dependencies `@types/node` +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/plugin-techdocs-node@1.13.10-next.1 + - @backstage/cli-common@0.1.16-next.2 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## 1.10.3-next.1 + +### Patch Changes + +- 43629b1: Updated to use new utilities from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.1.16-next.1 + +## 1.10.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/cli-common@0.1.16-next.0 + - @backstage/plugin-techdocs-node@1.13.10-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## 1.10.2 + +### Patch Changes + +- c2a2017: Fix for missing styles due to move to BUI. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-techdocs-node@1.13.9 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + ## 1.10.1-next.0 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index bf8755612c..7882ebb9e7 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,6 +1,6 @@ { "name": "@techdocs/cli", - "version": "1.10.1-next.0", + "version": "1.10.4", "description": "Utility CLI for managing TechDocs sites in Backstage.", "backstage": { "role": "cli" @@ -62,7 +62,7 @@ "@types/commander": "^2.12.2", "@types/fs-extra": "^11.0.0", "@types/http-proxy": "^1.17.4", - "@types/node": "^20.16.0", + "@types/node": "^22.13.14", "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", "find-process": "^1.4.5", diff --git a/packages/techdocs-cli/src/commands/serve/mkdocs.ts b/packages/techdocs-cli/src/commands/serve/mkdocs.ts index 00c5e5f0ed..cd1072a88f 100644 --- a/packages/techdocs-cli/src/commands/serve/mkdocs.ts +++ b/packages/techdocs-cli/src/commands/serve/mkdocs.ts @@ -18,7 +18,7 @@ import { OptionValues } from 'commander'; import openBrowser from 'react-dev-utils/openBrowser'; import { createLogger } from '../../lib/utility'; import { runMkdocsServer } from '../../lib/mkdocsServer'; -import { LogFunc, waitForSignal } from '../../lib/run'; +import { RunOnOutput } from '@backstage/cli-common'; import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; import fs from 'fs-extra'; import { checkIfDockerIsOperational } from './utils'; @@ -45,7 +45,7 @@ export default async function serveMkdocs(opts: OptionValues) { // We want to open browser only once based on a log. let boolOpenBrowserTriggered = false; - const logFunc: LogFunc = data => { + const logFunc: RunOnOutput = data => { // Sometimes the lines contain an unnecessary extra new line in between const logLines = data.toString().split('\n'); const logPrefix = opts.docker ? '[docker/mkdocs]' : '[mkdocs]'; @@ -74,18 +74,18 @@ export default async function serveMkdocs(opts: OptionValues) { // Had me questioning this whole implementation for half an hour. // Commander stores --no-docker in cmd.docker variable - const childProcess = await runMkdocsServer({ + const childProcess = runMkdocsServer({ port: opts.port, dockerImage: opts.dockerImage, dockerEntrypoint: opts.dockerEntrypoint, dockerOptions: opts.dockerOption, useDocker: opts.docker, - stdoutLogFunc: logFunc, - stderrLogFunc: logFunc, + onStdout: logFunc, + onStderr: logFunc, }); // Keep waiting for user to cancel the process - await waitForSignal([childProcess]); + await childProcess.waitForExit(); if (configIsTemporary) { process.on('exit', async () => { diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 27f432c22d..a0d0030c6b 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -17,10 +17,9 @@ import { OptionValues } from 'commander'; import path from 'path'; import openBrowser from 'react-dev-utils/openBrowser'; -import { findPaths } from '@backstage/cli-common'; +import { findPaths, RunOnOutput } from '@backstage/cli-common'; import HTTPServer from '../../lib/httpServer'; import { runMkdocsServer } from '../../lib/mkdocsServer'; -import { LogFunc, waitForSignal } from '../../lib/run'; import { createLogger } from '../../lib/utility'; import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; import fs from 'fs-extra'; @@ -83,7 +82,7 @@ export default async function serve(opts: OptionValues) { } let mkdocsServerHasStarted = false; - const mkdocsLogFunc: LogFunc = data => { + const mkdocsLogFunc: RunOnOutput = data => { // Sometimes the lines contain an unnecessary extra new line const logLines = data.toString().split('\n'); const logPrefix = opts.docker ? '[docker/mkdocs]' : '[mkdocs]'; @@ -107,14 +106,14 @@ export default async function serve(opts: OptionValues) { // https://github.com/mkdocs/mkdocs/issues/879#issuecomment-203536006 // Had me questioning this whole implementation for half an hour. logger.info('Starting mkdocs server.'); - const mkdocsChildProcess = await runMkdocsServer({ + const mkdocsChildProcess = runMkdocsServer({ port: opts.mkdocsPort, dockerImage: opts.dockerImage, dockerEntrypoint: opts.dockerEntrypoint, dockerOptions: opts.dockerOption, useDocker: opts.docker, - stdoutLogFunc: mkdocsLogFunc, - stderrLogFunc: mkdocsLogFunc, + onStdout: mkdocsLogFunc, + onStderr: mkdocsLogFunc, mkdocsConfigFileName: mkdocsYmlPath, mkdocsParameterClean: opts.mkdocsParameterClean, mkdocsParameterDirtyReload: opts.mkdocsParameterDirtyreload, @@ -161,7 +160,7 @@ export default async function serve(opts: OptionValues) { ); }); - await waitForSignal([mkdocsChildProcess]); + await mkdocsChildProcess.waitForExit(); if (configIsTemporary) { process.on('exit', async () => { diff --git a/packages/techdocs-cli/src/commands/serve/utils.ts b/packages/techdocs-cli/src/commands/serve/utils.ts index 2b19b0fbc0..9ecb8bc29b 100644 --- a/packages/techdocs-cli/src/commands/serve/utils.ts +++ b/packages/techdocs-cli/src/commands/serve/utils.ts @@ -14,25 +14,22 @@ * limitations under the License. */ -import { promisify } from 'util'; import * as winston from 'winston'; -import { execFile } from 'child_process'; +import { runCheck } from '@backstage/cli-common'; export async function checkIfDockerIsOperational( logger: winston.Logger, ): Promise { logger.info('Checking Docker status...'); - try { - const runCheck = promisify(execFile); - await runCheck('docker', ['info'], { shell: true }); + const isOperational = await runCheck(['docker', 'info']); + if (isOperational) { logger.info( 'Docker is up and running. Proceed to starting up mkdocs server', ); return true; - } catch { - logger.error( - 'Docker is not running. Exiting. Please check status of Docker daemon with `docker info` before re-running', - ); - return false; } + logger.error( + 'Docker is not running. Exiting. Please check status of Docker daemon with `docker info` before re-running', + ); + return false; } diff --git a/packages/techdocs-cli/src/lib/mkdocsServer.test.ts b/packages/techdocs-cli/src/lib/mkdocsServer.test.ts index 83f9c9f382..5acd1784a3 100644 --- a/packages/techdocs-cli/src/lib/mkdocsServer.test.ts +++ b/packages/techdocs-cli/src/lib/mkdocsServer.test.ts @@ -15,9 +15,9 @@ */ import { runMkdocsServer } from './mkdocsServer'; -import { run } from './run'; +import { run } from '@backstage/cli-common'; -jest.mock('./run', () => { +jest.mock('@backstage/cli-common', () => { return { run: jest.fn(), }; @@ -29,12 +29,12 @@ describe('runMkdocsServer', () => { }); describe('docker', () => { - it('should run docker directly by default', async () => { - await runMkdocsServer({}); + it('should run docker directly by default', () => { + runMkdocsServer({}); expect(run).toHaveBeenCalledWith( - 'docker', expect.arrayContaining([ + 'docker', 'run', `${process.cwd()}:/content`, '8000:8000', @@ -47,26 +47,24 @@ describe('runMkdocsServer', () => { ); }); - it('should accept port option', async () => { - await runMkdocsServer({ port: '5678' }); + it('should accept port option', () => { + runMkdocsServer({ port: '5678' }); expect(run).toHaveBeenCalledWith( - 'docker', - expect.arrayContaining(['5678:5678', '0.0.0.0:5678']), + expect.arrayContaining(['docker', '5678:5678', '0.0.0.0:5678']), expect.objectContaining({}), ); }); - it('should accept custom docker image', async () => { - await runMkdocsServer({ dockerImage: 'my-org/techdocs' }); + it('should accept custom docker image', () => { + runMkdocsServer({ dockerImage: 'my-org/techdocs' }); expect(run).toHaveBeenCalledWith( - 'docker', - expect.arrayContaining(['my-org/techdocs']), + expect.arrayContaining(['docker', 'my-org/techdocs']), expect.objectContaining({}), ); }); - it('should accept custom docker options', async () => { - await runMkdocsServer({ + it('should accept custom docker options', () => { + runMkdocsServer({ dockerOptions: [ '--add-host=internal.host:192.168.11.12', '--name', @@ -75,8 +73,8 @@ describe('runMkdocsServer', () => { }); expect(run).toHaveBeenCalledWith( - 'docker', expect.arrayContaining([ + 'docker', 'run', '--rm', '-w', @@ -98,14 +96,14 @@ describe('runMkdocsServer', () => { ); }); - it('should accept additinoal mkdocs CLI parameters', async () => { - await runMkdocsServer({ + it('should accept additinoal mkdocs CLI parameters', () => { + runMkdocsServer({ mkdocsParameterClean: true, mkdocsParameterStrict: true, }); expect(run).toHaveBeenCalledWith( - 'docker', expect.arrayContaining([ + 'docker', 'serve', '--dev-addr', '0.0.0.0:8000', @@ -118,21 +116,24 @@ describe('runMkdocsServer', () => { }); describe('mkdocs', () => { - it('should run mkdocs if specified', async () => { - await runMkdocsServer({ useDocker: false }); + it('should run mkdocs if specified', () => { + runMkdocsServer({ useDocker: false }); expect(run).toHaveBeenCalledWith( - 'mkdocs', - expect.arrayContaining(['serve', '--dev-addr', '127.0.0.1:8000']), + expect.arrayContaining([ + 'mkdocs', + 'serve', + '--dev-addr', + '127.0.0.1:8000', + ]), expect.objectContaining({}), ); }); - it('should accept port option', async () => { - await runMkdocsServer({ useDocker: false, port: '5678' }); + it('should accept port option', () => { + runMkdocsServer({ useDocker: false, port: '5678' }); expect(run).toHaveBeenCalledWith( - 'mkdocs', - expect.arrayContaining(['127.0.0.1:5678']), + expect.arrayContaining(['mkdocs', '127.0.0.1:5678']), expect.objectContaining({}), ); }); diff --git a/packages/techdocs-cli/src/lib/mkdocsServer.ts b/packages/techdocs-cli/src/lib/mkdocsServer.ts index a793643367..c557015256 100644 --- a/packages/techdocs-cli/src/lib/mkdocsServer.ts +++ b/packages/techdocs-cli/src/lib/mkdocsServer.ts @@ -14,30 +14,29 @@ * limitations under the License. */ -import { ChildProcess } from 'child_process'; -import { run, LogFunc } from './run'; +import { run, RunChildProcess, RunOnOutput } from '@backstage/cli-common'; -export const runMkdocsServer = async (options: { +export const runMkdocsServer = (options: { port?: string; useDocker?: boolean; dockerImage?: string; dockerEntrypoint?: string; dockerOptions?: string[]; - stdoutLogFunc?: LogFunc; - stderrLogFunc?: LogFunc; + onStdout?: RunOnOutput; + onStderr?: RunOnOutput; mkdocsConfigFileName?: string; mkdocsParameterClean?: boolean; mkdocsParameterDirtyReload?: boolean; mkdocsParameterStrict?: boolean; -}): Promise => { +}): RunChildProcess => { const port = options.port ?? '8000'; const useDocker = options.useDocker ?? true; const dockerImage = options.dockerImage ?? 'spotify/techdocs'; if (useDocker) { - return await run( - 'docker', + return run( [ + 'docker', 'run', '--rm', '-w', @@ -63,15 +62,15 @@ export const runMkdocsServer = async (options: { ...(options.mkdocsParameterStrict ? ['--strict'] : []), ], { - stdoutLogFunc: options.stdoutLogFunc, - stderrLogFunc: options.stderrLogFunc, + onStdout: options.onStdout, + onStderr: options.onStderr, }, ); } - return await run( - 'mkdocs', + return run( [ + 'mkdocs', 'serve', '--dev-addr', `127.0.0.1:${port}`, @@ -83,8 +82,8 @@ export const runMkdocsServer = async (options: { ...(options.mkdocsParameterStrict ? ['--strict'] : []), ], { - stdoutLogFunc: options.stdoutLogFunc, - stderrLogFunc: options.stderrLogFunc, + onStdout: options.onStdout, + onStderr: options.onStderr, }, ); }; diff --git a/packages/techdocs-cli/src/lib/run.ts b/packages/techdocs-cli/src/lib/run.ts deleted file mode 100644 index 13ac7d9d4c..0000000000 --- a/packages/techdocs-cli/src/lib/run.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { spawn, SpawnOptions, ChildProcess } from 'child_process'; - -export type LogFunc = (data: Buffer | string) => void; -type SpawnOptionsPartialEnv = Omit & { - env?: Partial; - // Pipe stdout to this log function - stdoutLogFunc?: LogFunc; - // Pipe stderr to this log function - stderrLogFunc?: LogFunc; -}; - -// TODO: Accept log functions to pipe logs with. -// Runs a child command, returning the child process instance. -// Use it along with waitForSignal to run a long running process e.g. mkdocs serve -export const run = async ( - name: string, - args: string[] = [], - options: SpawnOptionsPartialEnv = {}, -): Promise => { - const { stdoutLogFunc, stderrLogFunc } = options; - - const env: NodeJS.ProcessEnv = { - ...process.env, - FORCE_COLOR: 'true', - ...(options.env ?? {}), - }; - - // Refer: https://nodejs.org/api/child_process.html#child_process_subprocess_stdio - const stdio = [ - 'inherit', - stdoutLogFunc ? 'pipe' : 'inherit', - stderrLogFunc ? 'pipe' : 'inherit', - ] as ('inherit' | 'pipe')[]; - - const childProcess = spawn(name, args, { - stdio: stdio, - ...options, - env, - }); - - if (stdoutLogFunc && childProcess.stdout) { - childProcess.stdout.on('data', stdoutLogFunc); - } - if (stderrLogFunc && childProcess.stderr) { - childProcess.stderr.on('data', stderrLogFunc); - } - - return childProcess; -}; - -// Block indefinitely and wait for a signal to stop the child process(es) -// Throw error if any child process errors -// Resolves only when all processes exit with status code 0 -export async function waitForSignal( - childProcesses: Array, -): Promise { - const promises: Array> = []; - - for (const signal of ['SIGINT', 'SIGTERM'] as const) { - process.on(signal, () => { - childProcesses.forEach(childProcess => { - childProcess.kill(); - }); - }); - } - - childProcesses.forEach(childProcess => { - if (typeof childProcess.exitCode === 'number') { - if (childProcess.exitCode) { - throw new Error(`Non zero exit code from child process`); - } - return; - } - - promises.push( - new Promise((resolve, reject) => { - childProcess.once('error', reject); - childProcess.once('exit', resolve); - }), - ); - }); - - await Promise.all(promises); -} diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 7beeed27c1..71c5aca50a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/test-utils +## 1.7.14 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-permission-react@0.4.39 + +## 1.7.14-next.0 + +### Patch Changes + +- 97cd16f: Internal update of translation imports. +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.3 + +## 1.7.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-common@0.9.3 + - @backstage/core-app-api@1.19.2 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/config@1.3.6 + ## 1.7.13-next.0 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index b40fa8a0d8..2e12255bd0 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.7.13-next.0", + "version": "1.7.14", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts index 3d02db0124..661d202379 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts @@ -127,7 +127,9 @@ function baseImplementation( ): typeof crossFetch { const implementation = options?.baseImplementation; if (!implementation) { - return crossFetch; + // Return a wrapper that evaluates global.fetch at call time, not construction time. + // This allows MSW to patch global.fetch after MockFetchApi is constructed. + return (input, init) => global.fetch(input, init); } else if (implementation === 'none') { return () => Promise.resolve(new Response()); } diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts index 344679355a..2989207743 100644 --- a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts @@ -25,7 +25,7 @@ import ObservableImpl from 'zen-observable'; import { Observable } from '@backstage/types'; // Internal import to avoid code duplication, this will lead to duplication in build output // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef'; +import { toInternalTranslationRef } from '../../../../../frontend-plugin-api/src/translation/TranslationRef'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { JsxInterpolator } from '../../../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi'; diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 42a3f8e092..158499374e 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -92,17 +92,15 @@ describe('wrapInTestApp', () => { }); expect(error).toEqual([ - expect.objectContaining({ - detail: new Error( - 'MockErrorApi received unexpected error, Error: NOPE', - ), - }), - expect.objectContaining({ - detail: new Error( - 'MockErrorApi received unexpected error, Error: NOPE', - ), - }), - expect.stringMatching(/^The above error occurred in the component:/), + expect.stringContaining( + 'Error: MockErrorApi received unexpected error, Error: NOPE', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining( + 'Error: MockErrorApi received unexpected error, Error: NOPE', + ), + expect.objectContaining({ type: 'unhandled-exception' }), + expect.stringContaining('The above error occurred in the component:'), ]); }); diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 1b5b158102..bbe9ae3fc9 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/theme +## 0.7.1 + +### Patch Changes + +- fa06f6b: Added a `themeName` prop to `UnifiedThemeProvider`, enabling Backstage UI `data-theme-name` CSS attribute to be set based on active theme. + +## 0.7.1-next.0 + +### Patch Changes + +- fa06f6b: Added a `themeName` prop to `UnifiedThemeProvider`, enabling Backstage UI `data-theme-name` CSS attribute to be set based on active theme. + ## 0.7.0 ### Minor Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index cb7d5c12ce..645255d601 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.7.0", + "version": "0.7.1", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/packages/theme/report.api.md b/packages/theme/report.api.md index 18ac96ecec..f4e31db8e9 100644 --- a/packages/theme/report.api.md +++ b/packages/theme/report.api.md @@ -460,5 +460,6 @@ export interface UnifiedThemeProviderProps { children: ReactNode; // (undocumented) theme: UnifiedTheme; + themeName?: string; } ``` diff --git a/packages/theme/src/unified/UnifiedThemeProvider.test.tsx b/packages/theme/src/unified/UnifiedThemeProvider.test.tsx index 85a47dbb1e..52769b65a3 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.test.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.test.tsx @@ -20,11 +20,17 @@ import { } from '@material-ui/core/styles'; import { useTheme as useV5Theme } from '@mui/material/styles'; import { makeStyles as makeV5Styles } from '@mui/styles'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { UnifiedThemeProvider } from './UnifiedThemeProvider'; import { themes } from './themes'; describe('UnifiedThemeProvider', () => { + afterEach(() => { + document.body.removeAttribute('data-theme-name'); + document.body.removeAttribute('data-theme-mode'); + document.body.removeAttribute('data-unified-theme-stack'); + }); + it('provides a themes for v4 and v5 directly', () => { function MyV4Component() { const theme = useV4Theme(); @@ -81,4 +87,17 @@ describe('UnifiedThemeProvider', () => { 'rgb(158, 158, 158)', ); }); + + it('applies theme attributes based on the provided options', async () => { + render( + + theme + , + ); + + await waitFor(() => + expect(document.body.getAttribute('data-theme-name')).toBe('aperture'), + ); + expect(document.body.getAttribute('data-theme-mode')).toBe('light'); + }); }); diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index d0886cd146..69d843ed3e 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -37,6 +37,8 @@ import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material export interface UnifiedThemeProviderProps { children: ReactNode; theme: UnifiedTheme; + /** Optional override for the value written to the `data-theme-name` attribute. */ + themeName?: string; } /** @@ -66,15 +68,14 @@ import { useApplyThemeAttributes } from './useApplyThemeAttributes'; export function UnifiedThemeProvider( props: UnifiedThemeProviderProps, ): JSX.Element { - const { children, theme } = props; + const { children, theme, themeName } = props; const v4Theme = theme.getTheme('v4') as Mui4Theme; const v5Theme = theme.getTheme('v5') as Mui5Theme; - useApplyThemeAttributes( - v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode, - 'backstage', - ); + const themeMode = v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode; + + useApplyThemeAttributes(themeMode, themeName ?? 'backstage'); let result = children as JSX.Element; diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 2eec31def7..944c521afa 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,655 @@ # @backstage/ui +## 0.11.0 + +### Minor Changes + +- 243e5e7: **BREAKING**: Redesigned Table component with new `useTable` hook API. + + - The `Table` component (React Aria wrapper) is renamed to `TableRoot` + - New high-level `Table` component that handles data display, pagination, sorting, and selection + - The `useTable` hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor) + - New types: `ColumnConfig`, `TableProps`, `TableItem`, `UseTableOptions`, `UseTableResult` + + New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. + + **Migration guide:** + + 1. Update imports and use the new `useTable` hook: + + ```diff + -import { Table, useTable } from '@backstage/ui'; + -const { data, paginationProps } = useTable({ data: items, pagination: {...} }); + +import { Table, useTable, type ColumnConfig } from '@backstage/ui'; + +const { tableProps } = useTable({ + + mode: 'complete', + + getData: () => items, + +}); + ``` + + 2. Define columns and render with the new Table API: + + ```diff + -
    + - ... + - ... + -
    + - + +const columns: ColumnConfig[] = [ + + { id: 'name', label: 'Name', isRowHeader: true, cell: item => }, + + { id: 'type', label: 'Type', cell: item => }, + +]; + + + + + ``` + + Affected components: Table, TableRoot, TablePagination + +- 95246eb: **Breaking** Updating color tokens to match the new neutral style on different surfaces. + + ## Migration notes + + There's no direct replacement for the old tint tokens but you can use the new neutral set of color tokens on surface 0 or 1 as a replacement. + + - `--bui-bg-tint` can be replaced by `--bui-bg-neutral-on-surface-0` + - `--bui-bg-tint-hover` can be replaced by `--bui-bg-neutral-on-surface-0-hover` + - `--bui-bg-tint-pressed` can be replaced by `--bui-bg-neutral-on-surface-0-pressed` + - `--bui-bg-tint-disabled` can be replaced by `--bui-bg-neutral-on-surface-0-disabled` + +- ea0c6d8: Introduce new `ToggleButton` & `ToggleButtonGroup` components in Backstage UI +- 4ea1d15: **BREAKING**: Renamed CSS variable `--bui-bg` to `--bui-bg-surface-0` for consistency. + +### Patch Changes + +- 1880402: Fixes app background color on dark mode. +- d2fdded: Added indeterminate state support to the Checkbox component for handling partial selection scenarios like table header checkboxes. + + Affected components: Checkbox + +- 4fb15d2: Added missing `aria-label` attributes to `SearchField` components in `Select`, `MenuAutocomplete`, and `MenuAutocompleteListbox` to fix accessibility warnings. + + Affected components: Select, MenuAutocomplete, MenuAutocompleteListbox + +- 21c87cc: Fixes disabled state in primary and secondary buttons in Backstage UI. +- 9c76682: build(deps-dev): bump `storybook` from 10.1.9 to 10.1.10 +- de80336: Fixed disabled tertiary buttons incorrectly showing hover effects on surfaces. +- 133d5c6: Added new Popover component for Backstage UI with automatic overflow handling, and full placement support. Also introduced `--bui-shadow` token for consistent elevation styling across overlay components (Popover, Tooltip, Menu). +- 973c839: Fixed Table sorting indicator not being visible when a column is actively sorted. + + Affected components: Table, Column + +- df40cfc: Fixed Menu component trigger button not toggling correctly. Removed custom click-outside handler that was interfering with React Aria's built-in state management, allowing the menu to properly open and close when clicking the trigger button. +- b01ab96: Added support for column width configuration in Table component. Columns now accept `width`, `defaultWidth`, `minWidth`, and `maxWidth` props for responsive layout control. + + Affected components: Table, Column + +- b4a4911: Fixed SearchField `startCollapsed` prop not working correctly in Backstage UI. The field now properly starts in a collapsed state, expands when clicked and focused, and collapses back when unfocused with no input. Also fixed CSS logic to work correctly in all layout contexts (flex row, flex column, and regular containers). + + Affected components: SearchField + +- b3253b6: Fixed `Link` component causing hard page refreshes for internal routes. The component now properly uses React Router's navigation instead of full page reloads. +- fe7fe69: Added support for custom pagination options in `useTable` hook and `Table` component. You can now configure `pageSizeOptions` to customize the page size dropdown, and hook into pagination events via `onPageSizeChange`, `onNextPage`, and `onPreviousPage` callbacks. When `pageSize` doesn't match any option, the first option is used and a warning is logged. + + Affected components: Table, TablePagination + +- cfac8a4: Fixed missing border styles on table selection cells in multi-select mode. + + Affected components: Table + +- 2532d2a: Added `className` and `style` props to the `Table` component. + + Affected components: Table + +## 0.11.0-next.1 + +### Minor Changes + +- 243e5e7: **BREAKING**: Redesigned Table component with new `useTable` hook API. + + - The `Table` component (React Aria wrapper) is renamed to `TableRoot` + - New high-level `Table` component that handles data display, pagination, sorting, and selection + - The `useTable` hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor) + - New types: `ColumnConfig`, `TableProps`, `TableItem`, `UseTableOptions`, `UseTableResult` + + New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. + + **Migration guide:** + + 1. Update imports and use the new `useTable` hook: + + ```diff + -import { Table, useTable } from '@backstage/ui'; + -const { data, paginationProps } = useTable({ data: items, pagination: {...} }); + +import { Table, useTable, type ColumnConfig } from '@backstage/ui'; + +const { tableProps } = useTable({ + + mode: 'complete', + + getData: () => items, + +}); + ``` + + 2. Define columns and render with the new Table API: + + ```diff + -
    + - ... + - ... + -
    + - + +const columns: ColumnConfig[] = [ + + { id: 'name', label: 'Name', isRowHeader: true, cell: item => }, + + { id: 'type', label: 'Type', cell: item => }, + +]; + + + + + ``` + + Affected components: Table, TableRoot, TablePagination + +- 95246eb: **Breaking** Updating color tokens to match the new neutral style on different surfaces. + + ## Migration notes + + There's no direct replacement for the old tint tokens but you can use the new neutral set of color tokens on surface 0 or 1 as a replacement. + + - `--bui-bg-tint` can be replaced by `--bui-bg-neutral-on-surface-0` + - `--bui-bg-tint-hover` can be replaced by `--bui-bg-neutral-on-surface-0-hover` + - `--bui-bg-tint-pressed` can be replaced by `--bui-bg-neutral-on-surface-0-pressed` + - `--bui-bg-tint-disabled` can be replaced by `--bui-bg-neutral-on-surface-0-disabled` + +- ea0c6d8: Introduce new `ToggleButton` & `ToggleButtonGroup` components in Backstage UI + +### Patch Changes + +- 21c87cc: Fixes disabled state in primary and secondary buttons in Backstage UI. +- b3253b6: Fixed `Link` component causing hard page refreshes for internal routes. The component now properly uses React Router's navigation instead of full page reloads. + +## 0.11.0-next.0 + +### Minor Changes + +- 4ea1d15: **BREAKING**: Renamed CSS variable `--bui-bg` to `--bui-bg-surface-0` for consistency. + +### Patch Changes + +- 1880402: Fixes app background color on dark mode. +- 9c76682: build(deps-dev): bump `storybook` from 10.1.9 to 10.1.10 +- b4a4911: Fixed SearchField `startCollapsed` prop not working correctly in Backstage UI. The field now properly starts in a collapsed state, expands when clicked and focused, and collapses back when unfocused with no input. Also fixed CSS logic to work correctly in all layout contexts (flex row, flex column, and regular containers). + + Affected components: SearchField + +## 0.10.0 + +### Minor Changes + +- 16543fa: **Breaking change** The `Cell` component has been refactored to be a generic wrapper component that accepts `children` for custom cell content. The text-specific functionality (previously part of `Cell`) has been moved to a new `CellText` component. + + ### Migration Guide + + If you were using `Cell` with text-specific props (`title`, `description`, `leadingIcon`, `href`), you need to update your code to use `CellText` instead: + + **Before:** + + ```tsx + } + href="/path" + /> + ``` + + **After:** + + ```tsx + } + href="/path" + /> + ``` + + For custom cell content, use the new generic `Cell` component: + + ```tsx + {/* Your custom content */} + ``` + +### Patch Changes + +- 50b7927: Fixed Checkbox indicator showing checkmark color when unchecked. + + Affected components: Checkbox + +- 5bacf55: Fixed `ButtonIcon` incorrectly applying `className` to inner elements instead of only the root element. + + Affected components: ButtonIcon + +- b3ad928: Fixed Table Row component to correctly handle cases where no `href` is provided, preventing unnecessary router provider wrapping and fixing the cursor incorrectly showing as a pointer despite the element not being a link. + + Affected components: Row + +- a20d317: Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode. + + Affected components: Table, TableHeader, Row, Column + +- fe7c751: Fixed `useTable` hook to prioritize `providedRowCount` over data length for accurate row count in server-side pagination scenarios. +- c145031: Fixed Table column sorting indicator to show up arrow when no sort is active, correctly indicating that clicking will sort ascending. + + Affected components: Column + +## 0.10.0-next.1 + +### Minor Changes + +- 16543fa: **Breaking change** The `Cell` component has been refactored to be a generic wrapper component that accepts `children` for custom cell content. The text-specific functionality (previously part of `Cell`) has been moved to a new `CellText` component. + + ### Migration Guide + + If you were using `Cell` with text-specific props (`title`, `description`, `leadingIcon`, `href`), you need to update your code to use `CellText` instead: + + **Before:** + + ```tsx + } + href="/path" + /> + ``` + + **After:** + + ```tsx + } + href="/path" + /> + ``` + + For custom cell content, use the new generic `Cell` component: + + ```tsx + {/* Your custom content */} + ``` + +### Patch Changes + +- 50b7927: Fixed Checkbox indicator showing checkmark color when unchecked. + + Affected components: Checkbox + +- 5bacf55: Fixed `ButtonIcon` incorrectly applying `className` to inner elements instead of only the root element. + + Affected components: ButtonIcon + +- a20d317: Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode. + + Affected components: Table, TableHeader, Row, Column + +## 0.9.1-next.0 + +### Patch Changes + +- b3ad928: Fixed Table Row component to correctly handle cases where no `href` is provided, preventing unnecessary router provider wrapping and fixing the cursor incorrectly showing as a pointer despite the element not being a link. + + Affected components: Row + +- fe7c751: Fixed `useTable` hook to prioritize `providedRowCount` over data length for accurate row count in server-side pagination scenarios. +- c145031: Fixed Table column sorting indicator to show up arrow when no sort is active, correctly indicating that clicking will sort ascending. + + Affected components: Column + +## 0.9.0 + +### Minor Changes + +- 539cf26: **BREAKING**: Migrated Avatar component from Base UI to custom implementation with size changes: + + - Base UI-specific props are no longer supported + - Size values have been updated: + - New `x-small` size added (1.25rem / 20px) + - `small` size unchanged (1.5rem / 24px) + - `medium` size unchanged (2rem / 32px, default) + - `large` size **changed from 3rem to 2.5rem** (40px) + - New `x-large` size added (3rem / 48px) + + Migration: + + ```diff + # Remove Base UI-specific props + - + + + + # Update large size usage to x-large for same visual size + - + + + ``` + + Added `purpose` prop for accessibility control (`'informative'` or `'decoration'`). + +- 5c614ff: **BREAKING**: Migrated Checkbox component from Base UI to React Aria Components. + + API changes required: + + - `checked` → `isSelected` + - `defaultChecked` → `defaultSelected` + - `disabled` → `isDisabled` + - `required` → `isRequired` + - `label` prop removed - use `children` instead + - CSS: `bui-CheckboxLabel` class removed + - Data attribute: `data-checked` → `data-selected` + - Use without label is no longer supported + + Migration examples: + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accept terms + + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + Option + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accessible label + + ``` + +- 134151f: Fixing styles on SearchField in Backstage UI after migration to CSS modules. `SearchField` has now its own set of class names. We previously used class names from `TextField` but this approach was creating some confusion so going forward in your theme you'll be able to theme `TextField` and `SearchField` separately. +- a67670d: **BREAKING**: Removed central `componentDefinitions` object and related type utilities (`ComponentDefinitionName`, `ComponentClassNames`). + + Component definitions are primarily intended for documenting the CSS class API for theming purposes, not for programmatic use in JavaScript/TypeScript. + + **Migration Guide:** + + If you were using component definitions or class names to build custom components, we recommend migrating to either: + + - Use Backstage UI components directly as building blocks, or + - Duplicate the component CSS in your own stylesheets instead of relying on internal class names + +- b78fc45: **BREAKING**: Changed className prop behavior to augment default styles instead of being ignored or overriding them. + + Affected components: + + - Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator + - Switch + - Skeleton + - FieldLabel + - Header, HeaderToolbar + - HeaderPage + - Tabs, TabList, Tab, TabPanel + + If you were passing custom className values to any of these components that relied on the previous behavior, you may need to adjust your styles to account for the default classes now being applied alongside your custom classes. + +- 83c100e: **BREAKING**: Removed `Collapsible` component. Migrate to `Accordion` or use React Aria `Disclosure`. + + ## Migration Path 1: Accordion (Opinionated Styled Component) + + Accordion provides preset styling with a similar component structure. + + ```diff + - import { Collapsible } from '@backstage/ui'; + + import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui'; + + - + - } /> + - Content + - + + + + + + + Content + + + ``` + + CSS classes: `.bui-CollapsibleRoot` → `.bui-Accordion`, `.bui-CollapsibleTrigger` → `.bui-AccordionTrigger` (now on heading element), `.bui-CollapsiblePanel` → `.bui-AccordionPanel` + + ## Migration Path 2: React Aria Disclosure (Full Customization) + + For custom styling without preset styles: + + ```tsx + import { Disclosure, Button, DisclosurePanel } from 'react-aria-components'; + + + + Content + ; + ``` + +- 816af0f: **BREAKING**: The `SelectProps` interface now accepts a generic type parameter for selection mode. + + Added searchable and multiple selection support to Select component. The component now accepts `searchable`, `selectionMode`, and `searchPlaceholder` props to enable filtering and multi-selection modes. + + Migration: If you're using `SelectProps` type directly, update from `SelectProps` to `SelectProps<'single' | 'multiple'>`. Component usage remains backward compatible. + +### Patch Changes + +- d01de00: Fix broken external links in Backstage UI Header component. +- 35a3614: Fixed CSS issues in Select component including popover width constraints, focus outline behavior, and overflow handling. +- 01476f0: Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components. +- 26c6a78: Fix default text color in Backstage UI +- deaa427: Fixed Text component to prevent `truncate` prop from being spread to the underlying DOM element. +- 1059f95: Improved the Link component structure in Backstage UI. +- 836b0c7: Fixed dialog backdrop appearance in dark mode. +- 6874094: Migrated CellProfile component from Base UI Avatar to Backstage UI Avatar component. +- 719d772: Avatar components in x-small and small sizes now display only one initial instead of two, improving readability at smaller dimensions. +- 6d35a6b: Removed `@base-ui-components/react` dependency as all components now use React Aria Components. +- dac851f: Fix the default font size in Backstage UI. +- 3c0ea67: Fix CSS layer ordering in Backstage UI to make sure component styles are loaded after tokens and base declarations. +- 3b18d80: Fixed RadioGroup radio button ellipse distortion by preventing flex shrink and grow. +- 4eb455c: Fix font smoothing as default in Backstage UI. +- ff9f0c3: Enable tree-shaking of imports other than `*.css`. +- 7839e7b: Added `loading` prop to Button and ButtonIcon components for displaying spinner during async operations. +- a00fb88: Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the `href` prop. +- e16ece5: Set the color-scheme property depending on theme +- 1ef3ca4: Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. +- 00bfb83: Fix default font wight and font family in Backstage UI. + +## 0.9.0-next.3 + +### Minor Changes + +- 83c100e: **BREAKING**: Removed `Collapsible` component. Migrate to `Accordion` or use React Aria `Disclosure`. + + ## Migration Path 1: Accordion (Opinionated Styled Component) + + Accordion provides preset styling with a similar component structure. + + ```diff + - import { Collapsible } from '@backstage/ui'; + + import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui'; + + - + - } /> + - Content + - + + + + + + + Content + + + ``` + + CSS classes: `.bui-CollapsibleRoot` → `.bui-Accordion`, `.bui-CollapsibleTrigger` → `.bui-AccordionTrigger` (now on heading element), `.bui-CollapsiblePanel` → `.bui-AccordionPanel` + + ## Migration Path 2: React Aria Disclosure (Full Customization) + + For custom styling without preset styles: + + ```tsx + import { Disclosure, Button, DisclosurePanel } from 'react-aria-components'; + + + + Content + ; + ``` + +- 816af0f: **BREAKING**: The `SelectProps` interface now accepts a generic type parameter for selection mode. + + Added searchable and multiple selection support to Select component. The component now accepts `searchable`, `selectionMode`, and `searchPlaceholder` props to enable filtering and multi-selection modes. + + Migration: If you're using `SelectProps` type directly, update from `SelectProps` to `SelectProps<'single' | 'multiple'>`. Component usage remains backward compatible. + +### Patch Changes + +- 35a3614: Fixed CSS issues in Select component including popover width constraints, focus outline behavior, and overflow handling. +- 01476f0: Improved visual consistency of PasswordField, SearchField, and MenuAutocomplete components. +- 836b0c7: Fixed dialog backdrop appearance in dark mode. +- 6d35a6b: Removed `@base-ui-components/react` dependency as all components now use React Aria Components. +- 7839e7b: Added `loading` prop to Button and ButtonIcon components for displaying spinner during async operations. +- a00fb88: Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the href prop. + +## 0.9.0-next.2 + +### Minor Changes + +- 539cf26: **BREAKING**: Migrated Avatar component from Base UI to custom implementation with size changes: + + - Base UI-specific props are no longer supported + - Size values have been updated: + - New `x-small` size added (1.25rem / 20px) + - `small` size unchanged (1.5rem / 24px) + - `medium` size unchanged (2rem / 32px, default) + - `large` size **changed from 3rem to 2.5rem** (40px) + - New `x-large` size added (3rem / 48px) + + Migration: + + ```diff + # Remove Base UI-specific props + - + + + + # Update large size usage to x-large for same visual size + - + + + ``` + + Added `purpose` prop for accessibility control (`'informative'` or `'decoration'`). + +- 134151f: Fixing styles on SearchField in Backstage UI after migration to CSS modules. `SearchField` has now its own set of class names. We previously used class names from `TextField` but this approach was creating some confusion so going forward in your theme you'll be able to theme `TextField` and `SearchField` separately. + +### Patch Changes + +- d01de00: Fix broken external links in Backstage UI Header component. +- deaa427: Fixed Text component to prevent `truncate` prop from being spread to the underlying DOM element. +- 1059f95: Improved the Link component structure in Backstage UI. +- 6874094: Migrated CellProfile component from Base UI Avatar to Backstage UI Avatar component. +- 719d772: Avatar components in x-small and small sizes now display only one initial instead of two, improving readability at smaller dimensions. +- 3b18d80: Fixed RadioGroup radio button ellipse distortion by preventing flex shrink and grow. +- e16ece5: Set the color-scheme property depending on theme + +## 0.9.0-next.1 + +### Minor Changes + +- 5c614ff: **BREAKING**: Migrated Checkbox component from Base UI to React Aria Components. + + API changes required: + + - `checked` → `isSelected` + - `defaultChecked` → `defaultSelected` + - `disabled` → `isDisabled` + - `required` → `isRequired` + - `label` prop removed - use `children` instead + - CSS: `bui-CheckboxLabel` class removed + - Data attribute: `data-checked` → `data-selected` + - Use without label is no longer supported + + Migration examples: + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accept terms + + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + Option + ``` + + Before: + + ```tsx + + ``` + + After: + + ```tsx + + Accessible label + + ``` + +- b78fc45: **BREAKING**: Changed className prop behavior to augment default styles instead of being ignored or overriding them. + + Affected components: + + - Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator + - Switch + - Skeleton + - FieldLabel + - Header, HeaderToolbar + - HeaderPage + - Tabs, TabList, Tab, TabPanel + + If you were passing custom className values to any of these components that relied on the previous behavior, you may need to adjust your styles to account for the default classes now being applied alongside your custom classes. + +### Patch Changes + +- ff9f0c3: Enable tree-shaking of imports other than `*.css`. +- 1ef3ca4: Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. + ## 0.8.2-next.0 ### Patch Changes diff --git a/packages/ui/README.md b/packages/ui/README.md index 1a0fb84a0c..271b78fca4 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -16,3 +16,26 @@ yarn add @backstage/ui - [Backstage UI Documentation](https://ui.backstage.io) - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) - [Backstage Documentation](https://backstage.io/docs) + +## Writing Changesets for Components + +When creating changesets for component-specific changes, add component metadata to help maintain documentation: + +```markdown +--- +'@backstage/ui': patch +--- + +Fixed size prop handling for Avatar component. + +Affected components: Avatar +``` + +**Guidelines:** + +- **Component names**: Use PascalCase as they appear in imports (Avatar, ButtonIcon, SearchField) +- **Multiple components**: `Affected components: Button, ButtonLink, ButtonIcon` +- **General changes**: Omit the metadata line (build changes, package-level updates) +- **Placement**: The line can appear anywhere in the description + +The changelog sync tool will parse these tags and update the documentation site automatically. diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 862de8e0e3..4f0f333c25 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -51,17 +51,30 @@ --bui-gray-6: #8c8c8c; --bui-gray-7: #757575; --bui-gray-8: #595959; - --bui-bg: var(--bui-gray-1); + --bui-bg-surface-0: var(--bui-gray-1); --bui-bg-surface-1: var(--bui-white); - --bui-bg-surface-2: var(--bui-gray-2); + --bui-bg-surface-2: var(--bui-gray-1); + --bui-bg-surface-3: var(--bui-gray-2); --bui-bg-solid: #1f5493; --bui-bg-solid-hover: #163a66; --bui-bg-solid-pressed: #0f2b4e; - --bui-bg-solid-disabled: #ebebeb; - --bui-bg-tint: transparent; - --bui-bg-tint-hover: #1f549366; - --bui-bg-tint-pressed: #1f549399; - --bui-bg-tint-disabled: #ebebeb; + --bui-bg-solid-disabled: #163a66; + --bui-bg-neutral-on-surface-0: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-0-hover: oklch(0% 0 0 / .12); + --bui-bg-neutral-on-surface-0-pressed: oklch(0% 0 0 / .16); + --bui-bg-neutral-on-surface-0-disabled: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-1: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-1-hover: oklch(0% 0 0 / .12); + --bui-bg-neutral-on-surface-1-pressed: oklch(0% 0 0 / .16); + --bui-bg-neutral-on-surface-1-disabled: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-2: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-2-hover: oklch(0% 0 0 / .12); + --bui-bg-neutral-on-surface-2-pressed: oklch(0% 0 0 / .16); + --bui-bg-neutral-on-surface-2-disabled: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-3: oklch(0% 0 0 / .06); + --bui-bg-neutral-on-surface-3-hover: oklch(0% 0 0 / .12); + --bui-bg-neutral-on-surface-3-pressed: oklch(0% 0 0 / .16); + --bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / .06); --bui-bg-danger: #feebe7; --bui-bg-warning: #fff2b2; --bui-bg-success: #e6f6eb; @@ -71,7 +84,7 @@ --bui-fg-link-hover: #1f2d5c; --bui-fg-disabled: #9e9e9e; --bui-fg-solid: var(--bui-white); - --bui-fg-solid-disabled: #9c9c9c; + --bui-fg-solid-disabled: #98a8bc; --bui-fg-tint: #1f5493; --bui-fg-tint-disabled: var(--bui-gray-5); --bui-fg-danger: #e22b2b; @@ -87,6 +100,7 @@ --bui-ring: #1f5493; --bui-scrollbar: #a0a0a03b; --bui-scrollbar-thumb: #a0a0a0; + --bui-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a; --bui-animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; } @@ -99,17 +113,30 @@ --bui-gray-6: #7b7b7b; --bui-gray-7: #9e9e9e; --bui-gray-8: #b4b4b4; - --bui-bg: #333; - --bui-bg-surface-1: #424242; + --bui-bg-surface-0: #333; + --bui-bg-surface-1: var(--bui-gray-1); --bui-bg-surface-2: var(--bui-gray-2); + --bui-bg-surface-3: var(--bui-gray-3); --bui-bg-solid: #9cc9ff; --bui-bg-solid-hover: #83b9fd; --bui-bg-solid-pressed: #83b9fd; - --bui-bg-solid-disabled: #222; - --bui-bg-tint: transparent; - --bui-bg-tint-hover: #9cc9ff1f; - --bui-bg-tint-pressed: #9cc9ff29; - --bui-bg-tint-disabled: transparent; + --bui-bg-solid-disabled: #1b3d68; + --bui-bg-neutral-on-surface-0: oklch(100% 0 0 / .1); + --bui-bg-neutral-on-surface-0-hover: oklch(100% 0 0 / .14); + --bui-bg-neutral-on-surface-0-pressed: oklch(100% 0 0 / .2); + --bui-bg-neutral-on-surface-0-disabled: oklch(100% 0 0 / .1); + --bui-bg-neutral-on-surface-1: oklch(100% 0 0 / .06); + --bui-bg-neutral-on-surface-1-hover: oklch(100% 0 0 / .1); + --bui-bg-neutral-on-surface-1-pressed: oklch(100% 0 0 / .16); + --bui-bg-neutral-on-surface-1-disabled: oklch(100% 0 0 / .06); + --bui-bg-neutral-on-surface-2: oklch(100% 0 0 / .08); + --bui-bg-neutral-on-surface-2-hover: oklch(100% 0 0 / .12); + --bui-bg-neutral-on-surface-2-pressed: oklch(100% 0 0 / .2); + --bui-bg-neutral-on-surface-2-disabled: oklch(100% 0 0 / .08); + --bui-bg-neutral-on-surface-3: oklch(100% 0 0 / .08); + --bui-bg-neutral-on-surface-3-hover: oklch(100% 0 0 / .12); + --bui-bg-neutral-on-surface-3-pressed: oklch(100% 0 0 / .2); + --bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / .08); --bui-bg-danger: #3b1219; --bui-bg-warning: #302008; --bui-bg-success: #132d21; @@ -119,7 +146,7 @@ --bui-fg-link-hover: #7eb5f7; --bui-fg-disabled: var(--bui-gray-7); --bui-fg-solid: #101821; - --bui-fg-solid-disabled: var(--bui-gray-5); + --bui-fg-solid-disabled: #6191cc; --bui-fg-tint: #9cc9ff; --bui-fg-tint-disabled: var(--bui-gray-5); --bui-fg-danger: #e22b2b; @@ -135,6 +162,7 @@ --bui-ring: #1f5493; --bui-scrollbar: #3636363a; --bui-scrollbar-thumb: #575757; + --bui-shadow: none; } } @@ -239,7 +267,7 @@ } body { - background-color: var(--bui-bg); + background-color: var(--bui-bg-surface-0); color: var(--bui-fg-primary); font-family: var(--bui-font-regular); font-weight: var(--bui-font-weight-regular); @@ -247,6 +275,14 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + + [data-theme-mode="dark"] { + color-scheme: dark; + } + + [data-theme-mode="light"] { + color-scheme: light; + } } @layer components; diff --git a/packages/ui/package.json b/packages/ui/package.json index bef759681c..9eeb512376 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.8.2-next.0", + "version": "0.11.0", "backstage": { "role": "web-library" }, @@ -40,7 +40,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@base-ui-components/react": "1.0.0-alpha.7", + "@backstage/version-bridge": "workspace:^", "@remixicon/react": "^4.6.0", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", @@ -51,7 +51,7 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "chalk": "^5.4.1", - "eslint-plugin-storybook": "^9.1.7", + "eslint-plugin-storybook": "^10.2.0-alpha.7", "glob": "^11.0.1", "globals": "^15.11.0", "lightningcss": "^1.29.1", @@ -59,7 +59,7 @@ "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.3.0", - "storybook": "^9.1.7" + "storybook": "^10.2.0-alpha.7" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 7cdee27d0e..fa50111cde 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -3,16 +3,19 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { ButtonProps as ButtonProps_2 } from 'react-aria-components'; import { CellProps as CellProps_2 } from 'react-aria-components'; import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components'; -import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ColumnProps as ColumnProps_2 } from 'react-aria-components'; +import type { ColumnSize } from '@react-types/table'; +import type { ColumnStaticSize } from '@react-types/table'; import { ComponentProps } from 'react'; import type { ComponentPropsWithRef } from 'react'; import { DetailedHTMLProps } from 'react'; import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components'; +import type { DisclosureGroupProps } from 'react-aria-components'; +import type { DisclosurePanelProps } from 'react-aria-components'; +import type { DisclosureProps } from 'react-aria-components'; import type { ElementType } from 'react'; import { ForwardRefExoticComponent } from 'react'; import type { HeadingProps } from 'react-aria-components'; @@ -26,7 +29,7 @@ import type { MenuProps as MenuProps_2 } from 'react-aria-components'; import type { MenuSectionProps as MenuSectionProps_2 } from 'react-aria-components'; import type { MenuTriggerProps as MenuTriggerProps_2 } from 'react-aria-components'; import type { ModalOverlayProps } from 'react-aria-components'; -import type { PopoverProps } from 'react-aria-components'; +import { PopoverProps as PopoverProps_2 } from 'react-aria-components'; import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components'; import type { RadioProps as RadioProps_2 } from 'react-aria-components'; import { ReactElement } from 'react'; @@ -36,11 +39,12 @@ import { RowProps } from 'react-aria-components'; import type { SearchFieldProps as SearchFieldProps_2 } from 'react-aria-components'; import type { SelectProps as SelectProps_2 } from 'react-aria-components'; import type { SeparatorProps } from 'react-aria-components'; +import type { SortDescriptor as SortDescriptor_2 } from 'react-stately'; import type { SubmenuTriggerProps as SubmenuTriggerProps_2 } from 'react-aria-components'; import type { SwitchProps as SwitchProps_2 } from 'react-aria-components'; import { TableBodyProps } from 'react-aria-components'; import { TableHeaderProps } from 'react-aria-components'; -import { TableProps } from 'react-aria-components'; +import { TableProps as TableProps_2 } from 'react-aria-components'; import type { TabListProps as TabListProps_2 } from 'react-aria-components'; import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components'; import type { TabProps as TabProps_2 } from 'react-aria-components'; @@ -49,25 +53,101 @@ import type { TagGroupProps as TagGroupProps_2 } from 'react-aria-components'; import type { TagListProps } from 'react-aria-components'; import type { TagProps as TagProps_2 } from 'react-aria-components'; import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components'; +import type { ToggleButtonGroupProps as ToggleButtonGroupProps_2 } from 'react-aria-components'; +import type { ToggleButtonProps as ToggleButtonProps_2 } from 'react-aria-components'; import { TooltipProps as TooltipProps_2 } from 'react-aria-components'; import { TooltipTriggerComponentProps } from 'react-aria-components'; +// @public (undocumented) +export const Accordion: ForwardRefExoticComponent< + AccordionProps & RefAttributes +>; + +// @public +export const AccordionDefinition: { + readonly classNames: { + readonly root: 'bui-Accordion'; + readonly trigger: 'bui-AccordionTrigger'; + readonly triggerButton: 'bui-AccordionTriggerButton'; + readonly triggerTitle: 'bui-AccordionTriggerTitle'; + readonly triggerSubtitle: 'bui-AccordionTriggerSubtitle'; + readonly triggerIcon: 'bui-AccordionTriggerIcon'; + readonly panel: 'bui-AccordionPanel'; + readonly group: 'bui-AccordionGroup'; + }; +}; + +// @public (undocumented) +export const AccordionGroup: ForwardRefExoticComponent< + AccordionGroupProps & RefAttributes +>; + +// @public +export interface AccordionGroupProps extends DisclosureGroupProps { + allowsMultiple?: boolean; + // (undocumented) + className?: string; +} + +// @public (undocumented) +export const AccordionPanel: ForwardRefExoticComponent< + AccordionPanelProps & RefAttributes +>; + +// @public +export interface AccordionPanelProps extends DisclosurePanelProps { + // (undocumented) + className?: string; +} + +// @public +export interface AccordionProps extends DisclosureProps { + // (undocumented) + className?: string; +} + +// @public (undocumented) +export const AccordionTrigger: ForwardRefExoticComponent< + AccordionTriggerProps & RefAttributes +>; + +// @public +export interface AccordionTriggerProps extends HeadingProps { + // (undocumented) + children?: React.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + subtitle?: string; + // (undocumented) + title?: string; +} + // @public (undocumented) export type AlignItems = 'stretch' | 'start' | 'center' | 'end'; // @public (undocumented) export const Avatar: ForwardRefExoticComponent< - AvatarProps & RefAttributes + AvatarProps & RefAttributes >; +// @public +export const AvatarDefinition: { + readonly classNames: { + readonly root: 'bui-AvatarRoot'; + readonly image: 'bui-AvatarImage'; + readonly fallback: 'bui-AvatarFallback'; + }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium', 'large']; + }; +}; + // @public (undocumented) -export interface AvatarProps - extends React.ComponentPropsWithoutRef { - // (undocumented) +export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> { name: string; - // (undocumented) - size?: 'small' | 'medium' | 'large'; - // (undocumented) + purpose?: 'decoration' | 'informative'; + size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; src: string; } @@ -90,6 +170,48 @@ export const Box: ForwardRefExoticComponent< BoxProps & RefAttributes >; +// @public +export const BoxDefinition: { + readonly classNames: { + readonly root: 'bui-Box'; + }; + readonly utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'position', + 'display', + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + ]; + readonly dataAttributes: { + readonly surface: readonly [ + '0', + '1', + '2', + '3', + 'danger', + 'warning', + 'success', + ]; + }; +}; + // @public (undocumented) export interface BoxProps extends SpaceProps { // (undocumented) @@ -117,27 +239,54 @@ export interface BoxProps extends SpaceProps { // (undocumented) style?: React.CSSProperties; // (undocumented) + surface?: Responsive; + // (undocumented) width?: Responsive; } // @public (undocumented) export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; -// @public (undocumented) +// @public export const Button: ForwardRefExoticComponent< ButtonProps & RefAttributes >; +// @public +export const ButtonDefinition: { + readonly classNames: { + readonly root: 'bui-Button'; + readonly content: 'bui-ButtonContent'; + readonly spinner: 'bui-ButtonSpinner'; + }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium', 'large']; + readonly variant: readonly ['primary', 'secondary', 'tertiary']; + readonly loading: readonly [true, false]; + }; +}; + // @public (undocumented) export const ButtonIcon: ForwardRefExoticComponent< ButtonIconProps & RefAttributes >; +// @public +export const ButtonIconDefinition: { + readonly classNames: { + readonly root: 'bui-ButtonIcon'; + readonly content: 'bui-ButtonIconContent'; + readonly spinner: 'bui-ButtonIconSpinner'; + }; +}; + // @public export interface ButtonIconProps extends ButtonProps_2 { // (undocumented) icon?: ReactElement; // (undocumented) + loading?: boolean; + // (undocumented) size?: 'small' | 'medium' | Partial>; // (undocumented) variant?: @@ -152,6 +301,13 @@ export const ButtonLink: ForwardRefExoticComponent< ButtonLinkProps & RefAttributes >; +// @public +export const ButtonLinkDefinition: { + readonly classNames: { + readonly root: 'bui-ButtonLink'; + }; +}; + // @public export interface ButtonLinkProps extends LinkProps_2 { // (undocumented) @@ -179,6 +335,9 @@ export interface ButtonProps extends ButtonProps_2 { // (undocumented) iconStart?: ReactElement; // (undocumented) + loading?: boolean; + onSurface?: Responsive; + // (undocumented) size?: 'small' | 'medium' | Partial>; // (undocumented) variant?: @@ -204,6 +363,16 @@ export interface CardBodyProps extends React.HTMLAttributes { children?: React.ReactNode; } +// @public +export const CardDefinition: { + readonly classNames: { + readonly root: 'bui-Card'; + readonly header: 'bui-CardHeader'; + readonly body: 'bui-CardBody'; + readonly footer: 'bui-CardFooter'; + }; +}; + // @public export const CardFooter: ForwardRefExoticComponent< CardFooterProps & RefAttributes @@ -244,7 +413,7 @@ export const CellProfile: (props: CellProfileProps) => JSX_2.Element; // @public (undocumented) export interface CellProfileProps extends CellProps_2 { // (undocumented) - color?: 'primary' | 'secondary'; + color?: TextColors; // (undocumented) description?: string; // (undocumented) @@ -256,9 +425,18 @@ export interface CellProfileProps extends CellProps_2 { } // @public (undocumented) -export interface CellProps extends CellProps_2 { +export interface CellProps extends CellProps_2 {} + +// @public (undocumented) +export const CellText: { + (props: CellTextProps): JSX_2.Element; + displayName: string; +}; + +// @public (undocumented) +export interface CellTextProps extends CellProps_2 { // (undocumented) - color?: 'primary' | 'secondary'; + color?: TextColors; // (undocumented) description?: string; // (undocumented) @@ -274,6 +452,18 @@ export const Checkbox: ForwardRefExoticComponent< CheckboxProps & RefAttributes >; +// @public +export const CheckboxDefinition: { + readonly classNames: { + readonly root: 'bui-Checkbox'; + readonly indicator: 'bui-CheckboxIndicator'; + }; + readonly dataAttributes: { + readonly selected: readonly [true, false]; + readonly indeterminate: readonly [true, false]; + }; +}; + // @public (undocumented) export interface CheckboxProps extends CheckboxProps_2 { // (undocumented) @@ -283,28 +473,35 @@ export interface CheckboxProps extends CheckboxProps_2 { // @public export type ClassNamesMap = Record; -// @public -export const Collapsible: { - Root: ForwardRefExoticComponent< - Omit, 'ref'> & - RefAttributes - >; - Trigger: ForwardRefExoticComponent< - Omit< - Collapsible_2.Trigger.Props & RefAttributes, - 'ref' - > & - RefAttributes - >; - Panel: ForwardRefExoticComponent< - Omit, 'ref'> & - RefAttributes - >; -}; - // @public (undocumented) export const Column: (props: ColumnProps) => JSX_2.Element; +// @public (undocumented) +export interface ColumnConfig { + // (undocumented) + cell: (item: T) => ReactNode; + // (undocumented) + defaultWidth?: ColumnSize | null; + // (undocumented) + header?: () => ReactNode; + // (undocumented) + id: string; + // (undocumented) + isHidden?: boolean; + // (undocumented) + isRowHeader?: boolean; + // (undocumented) + isSortable?: boolean; + // (undocumented) + label: string; + // (undocumented) + maxWidth?: ColumnStaticSize | null; + // (undocumented) + minWidth?: ColumnStaticSize | null; + // (undocumented) + width?: ColumnSize | null; +} + // @public (undocumented) export interface ColumnProps extends Omit { // (undocumented) @@ -327,10 +524,6 @@ export type Columns = | '12' | 'auto'; -// @public -export type ComponentClassNames = - (typeof componentDefinitions)[T]['classNames']; - // @public export interface ComponentDefinition { // (undocumented) @@ -341,402 +534,19 @@ export interface ComponentDefinition { utilityProps?: string[]; } -// @public -export type ComponentDefinitionName = keyof typeof componentDefinitions; - -// @public -export const componentDefinitions: { - readonly Avatar: { - readonly classNames: { - readonly root: 'bui-AvatarRoot'; - readonly image: 'bui-AvatarImage'; - readonly fallback: 'bui-AvatarFallback'; - }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium', 'large']; - }; - }; - readonly Box: { - readonly classNames: { - readonly root: 'bui-Box'; - }; - readonly utilityProps: [ - 'm', - 'mb', - 'ml', - 'mr', - 'mt', - 'mx', - 'my', - 'p', - 'pb', - 'pl', - 'pr', - 'pt', - 'px', - 'py', - 'position', - 'display', - 'width', - 'minWidth', - 'maxWidth', - 'height', - 'minHeight', - 'maxHeight', - ]; - }; - readonly Button: { - readonly classNames: { - readonly root: 'bui-Button'; - }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium', 'large']; - readonly variant: readonly ['primary', 'secondary', 'tertiary']; - }; - }; - readonly ButtonIcon: { - readonly classNames: { - readonly root: 'bui-ButtonIcon'; - }; - }; - readonly ButtonLink: { - readonly classNames: { - readonly root: 'bui-ButtonLink'; - }; - }; - readonly Card: { - readonly classNames: { - readonly root: 'bui-Card'; - readonly header: 'bui-CardHeader'; - readonly body: 'bui-CardBody'; - readonly footer: 'bui-CardFooter'; - }; - }; - readonly Checkbox: { - readonly classNames: { - readonly root: 'bui-Checkbox'; - readonly indicator: 'bui-CheckboxIndicator'; - }; - readonly dataAttributes: { - readonly selected: readonly [true, false]; - }; - }; - readonly Collapsible: { - readonly classNames: { - readonly root: 'bui-CollapsibleRoot'; - readonly trigger: 'bui-CollapsibleTrigger'; - readonly panel: 'bui-CollapsiblePanel'; - }; - }; - readonly Container: { - readonly classNames: { - readonly root: 'bui-Container'; - }; - readonly utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display']; - }; - readonly Dialog: { - readonly classNames: { - readonly overlay: 'bui-DialogOverlay'; - readonly dialog: 'bui-Dialog'; - readonly header: 'bui-DialogHeader'; - readonly headerTitle: 'bui-DialogHeaderTitle'; - readonly body: 'bui-DialogBody'; - readonly footer: 'bui-DialogFooter'; - }; - }; - readonly FieldError: { - readonly classNames: { - readonly root: 'bui-FieldError'; - }; - }; - readonly FieldLabel: { - readonly classNames: { - readonly root: 'bui-FieldLabelWrapper'; - readonly label: 'bui-FieldLabel'; - readonly secondaryLabel: 'bui-FieldSecondaryLabel'; - readonly description: 'bui-FieldDescription'; - }; - }; - readonly Flex: { - readonly classNames: { - readonly root: 'bui-Flex'; - }; - readonly utilityProps: [ - 'm', - 'mb', - 'ml', - 'mr', - 'mt', - 'mx', - 'my', - 'p', - 'pb', - 'pl', - 'pr', - 'pt', - 'px', - 'py', - 'gap', - 'align', - 'justify', - 'direction', - ]; - }; - readonly Grid: { - readonly classNames: { - readonly root: 'bui-Grid'; - }; - readonly utilityProps: [ - 'columns', - 'gap', - 'm', - 'mb', - 'ml', - 'mr', - 'mt', - 'mx', - 'my', - 'p', - 'pb', - 'pl', - 'pr', - 'pt', - 'px', - 'py', - ]; - }; - readonly GridItem: { - readonly classNames: { - readonly root: 'bui-GridItem'; - }; - readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan']; - }; - readonly Header: { - readonly classNames: { - readonly toolbar: 'bui-HeaderToolbar'; - readonly toolbarWrapper: 'bui-HeaderToolbarWrapper'; - readonly toolbarContent: 'bui-HeaderToolbarContent'; - readonly toolbarControls: 'bui-HeaderToolbarControls'; - readonly toolbarIcon: 'bui-HeaderToolbarIcon'; - readonly toolbarName: 'bui-HeaderToolbarName'; - readonly tabsWrapper: 'bui-HeaderTabsWrapper'; - }; - }; - readonly HeaderPage: { - readonly classNames: { - readonly root: 'bui-HeaderPage'; - readonly content: 'bui-HeaderPageContent'; - readonly breadcrumbs: 'bui-HeaderPageBreadcrumbs'; - readonly tabsWrapper: 'bui-HeaderPageTabsWrapper'; - readonly controls: 'bui-HeaderPageControls'; - }; - }; - readonly Heading: { - readonly classNames: { - readonly root: 'bui-Heading'; - }; - readonly dataAttributes: { - readonly variant: readonly ['title1', 'title2', 'title3', 'subtitle']; - readonly color: readonly ['primary', 'secondary', 'muted']; - readonly truncate: readonly [true, false]; - }; - }; - readonly Icon: { - readonly classNames: { - readonly root: 'bui-Icon'; - }; - }; - readonly Link: { - readonly classNames: { - readonly root: 'bui-Link'; - }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - }; - }; - readonly List: { - readonly classNames: { - readonly root: 'bui-List'; - readonly row: 'bui-ListRow'; - readonly label: 'bui-ListLabel'; - }; - }; - readonly Menu: { - readonly classNames: { - readonly root: 'bui-Menu'; - readonly popover: 'bui-MenuPopover'; - readonly content: 'bui-MenuContent'; - readonly section: 'bui-MenuSection'; - readonly sectionHeader: 'bui-MenuSectionHeader'; - readonly item: 'bui-MenuItem'; - readonly itemListBox: 'bui-MenuItemListBox'; - readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck'; - readonly itemWrapper: 'bui-MenuItemWrapper'; - readonly itemContent: 'bui-MenuItemContent'; - readonly itemArrow: 'bui-MenuItemArrow'; - readonly separator: 'bui-MenuSeparator'; - readonly searchField: 'bui-MenuSearchField'; - readonly searchFieldInput: 'bui-MenuSearchFieldInput'; - readonly searchFieldClear: 'bui-MenuSearchFieldClear'; - readonly emptyState: 'bui-MenuEmptyState'; - }; - }; - readonly PasswordField: { - readonly classNames: { - readonly root: 'bui-PasswordField'; - readonly inputVisibility: 'bui-InputVisibility'; - }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; - }; - }; - readonly Popover: { - readonly classNames: { - readonly root: 'bui-Popover'; - }; - }; - readonly RadioGroup: { - readonly classNames: { - readonly root: 'bui-RadioGroup'; - readonly content: 'bui-RadioGroupContent'; - readonly radio: 'bui-Radio'; - }; - }; - readonly SearchField: { - readonly classNames: { - readonly root: 'bui-SearchField'; - readonly clear: 'bui-InputClear'; - }; - readonly dataAttributes: { - readonly startCollapsed: readonly [true, false]; - readonly size: readonly ['small', 'medium']; - }; - }; - readonly Select: { - readonly classNames: { - readonly root: 'bui-Select'; - readonly trigger: 'bui-SelectTrigger'; - readonly value: 'bui-SelectValue'; - readonly icon: 'bui-SelectIcon'; - readonly list: 'bui-SelectList'; - readonly item: 'bui-SelectItem'; - readonly itemIndicator: 'bui-SelectItemIndicator'; - readonly itemLabel: 'bui-SelectItemLabel'; - }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; - }; - }; - readonly Skeleton: { - readonly classNames: { - readonly root: 'bui-Skeleton'; - }; - }; - readonly Switch: { - readonly classNames: { - readonly root: 'bui-Switch'; - readonly indicator: 'bui-SwitchIndicator'; - }; - }; - readonly Table: { - readonly classNames: { - readonly table: 'bui-Table'; - readonly header: 'bui-TableHeader'; - readonly body: 'bui-TableBody'; - readonly row: 'bui-TableRow'; - readonly head: 'bui-TableHead'; - readonly headContent: 'bui-TableHeadContent'; - readonly headSortButton: 'bui-TableHeadSortButton'; - readonly caption: 'bui-TableCaption'; - readonly cell: 'bui-TableCell'; - readonly cellContentWrapper: 'bui-TableCellContentWrapper'; - readonly cellContent: 'bui-TableCellContent'; - readonly cellIcon: 'bui-TableCellIcon'; - readonly cellProfileAvatar: 'bui-TableCellProfileAvatar'; - readonly cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage'; - readonly cellProfileAvatarFallback: 'bui-TableCellProfileAvatarFallback'; - readonly cellProfileName: 'bui-TableCellProfileName'; - readonly cellProfileLink: 'bui-TableCellProfileLink'; - }; - }; - readonly TablePagination: { - readonly classNames: { - readonly root: 'bui-TablePagination'; - readonly left: 'bui-TablePaginationLeft'; - readonly right: 'bui-TablePaginationRight'; - readonly select: 'bui-TablePaginationSelect'; - }; - }; - readonly Tabs: { - readonly classNames: { - readonly tabs: 'bui-Tabs'; - readonly tabList: 'bui-TabList'; - readonly tabListWrapper: 'bui-TabListWrapper'; - readonly tab: 'bui-Tab'; - readonly tabActive: 'bui-TabActive'; - readonly tabHovered: 'bui-TabHovered'; - readonly panel: 'bui-TabPanel'; - }; - }; - readonly TagGroup: { - readonly classNames: { - readonly group: 'bui-TagGroup'; - readonly list: 'bui-TagList'; - readonly tag: 'bui-Tag'; - readonly tagIcon: 'bui-TagIcon'; - readonly tagRemoveButton: 'bui-TagRemoveButton'; - }; - }; - readonly Text: { - readonly classNames: { - readonly root: 'bui-Text'; - }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - readonly color: readonly [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - ]; - readonly truncate: readonly [true, false]; - }; - }; - readonly TextField: { - readonly classNames: { - readonly root: 'bui-TextField'; - readonly inputWrapper: 'bui-InputWrapper'; - readonly input: 'bui-Input'; - readonly inputIcon: 'bui-InputIcon'; - readonly inputAction: 'bui-InputAction'; - }; - readonly dataAttributes: { - readonly invalid: readonly [true, false]; - readonly disabled: readonly [true, false]; - readonly size: readonly ['small', 'medium']; - }; - }; - readonly Tooltip: { - readonly classNames: { - readonly tooltip: 'bui-Tooltip'; - readonly arrow: 'bui-TooltipArrow'; - }; - }; - readonly VisuallyHidden: { - readonly classNames: { - readonly root: 'bui-VisuallyHidden'; - }; - }; -}; - // @public (undocumented) export const Container: ForwardRefExoticComponent< ContainerProps & RefAttributes >; +// @public +export const ContainerDefinition: { + readonly classNames: { + readonly root: 'bui-Container'; + }; + readonly utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display']; +}; + // @public (undocumented) export interface ContainerProps { // (undocumented) @@ -759,6 +569,34 @@ export interface ContainerProps { style?: React.CSSProperties; } +// @public (undocumented) +export interface CursorParams { + // (undocumented) + cursor: string | undefined; + // (undocumented) + filter: TFilter | undefined; + // (undocumented) + pageSize: number; + // (undocumented) + search: string; + // (undocumented) + signal: AbortSignal; + // (undocumented) + sort: SortDescriptor | null; +} + +// @public (undocumented) +export interface CursorResponse { + // (undocumented) + data: T[]; + // (undocumented) + nextCursor?: string; + // (undocumented) + prevCursor?: string; + // (undocumented) + totalCount?: number; +} + // @public export type DataAttributesMap = Record; @@ -783,6 +621,18 @@ export interface DialogBodyProps { className?: string; } +// @public +export const DialogDefinition: { + readonly classNames: { + readonly overlay: 'bui-DialogOverlay'; + readonly dialog: 'bui-Dialog'; + readonly header: 'bui-DialogHeader'; + readonly headerTitle: 'bui-DialogHeaderTitle'; + readonly body: 'bui-DialogBody'; + readonly footer: 'bui-DialogFooter'; + }; +}; + // @public (undocumented) export const DialogFooter: ForwardRefExoticComponent< Omit< @@ -831,6 +681,16 @@ export const FieldLabel: ForwardRefExoticComponent< FieldLabelProps & RefAttributes >; +// @public +export const FieldLabelDefinition: { + readonly classNames: { + readonly root: 'bui-FieldLabelWrapper'; + readonly label: 'bui-FieldLabel'; + readonly secondaryLabel: 'bui-FieldSecondaryLabel'; + readonly description: 'bui-FieldDescription'; + }; +}; + // @public (undocumented) export interface FieldLabelProps extends Pick, 'className'> { @@ -841,11 +701,57 @@ export interface FieldLabelProps secondaryLabel?: string | null; } +// @public (undocumented) +export interface FilterState { + // (undocumented) + onChange: (value: TFilter) => void; + // (undocumented) + value: TFilter | undefined; +} + // @public (undocumented) export const Flex: ForwardRefExoticComponent< FlexProps & RefAttributes >; +// @public +export const FlexDefinition: { + readonly classNames: { + readonly root: 'bui-Flex'; + }; + readonly utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'gap', + 'align', + 'justify', + 'direction', + ]; + readonly dataAttributes: { + readonly surface: readonly [ + '0', + '1', + '2', + '3', + 'danger', + 'warning', + 'success', + ]; + }; +}; + // @public (undocumented) export type FlexDirection = 'row' | 'column'; @@ -865,6 +771,8 @@ export interface FlexProps extends SpaceProps { justify?: Responsive<'start' | 'center' | 'end' | 'between'>; // (undocumented) style?: React.CSSProperties; + // (undocumented) + surface?: Responsive; } // @public (undocumented) @@ -878,6 +786,61 @@ export const Grid: { >; }; +// @public +export const GridDefinition: { + readonly classNames: { + readonly root: 'bui-Grid'; + }; + readonly utilityProps: [ + 'columns', + 'gap', + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + ]; + readonly dataAttributes: { + readonly surface: readonly [ + '0', + '1', + '2', + '3', + 'danger', + 'warning', + 'success', + ]; + }; +}; + +// @public +export const GridItemDefinition: { + readonly classNames: { + readonly root: 'bui-GridItem'; + }; + readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan']; + readonly dataAttributes: { + readonly surface: readonly [ + '0', + '1', + '2', + '3', + 'danger', + 'warning', + 'success', + ]; + }; +}; + // @public (undocumented) export interface GridItemProps { // (undocumented) @@ -894,6 +857,8 @@ export interface GridItemProps { rowSpan?: Responsive; // (undocumented) style?: React.CSSProperties; + // (undocumented) + surface?: Responsive; } // @public (undocumented) @@ -908,11 +873,26 @@ export interface GridProps extends SpaceProps { gap?: Responsive; // (undocumented) style?: React.CSSProperties; + // (undocumented) + surface?: Responsive; } // @public export const Header: (props: HeaderProps) => JSX_2.Element; +// @public +export const HeaderDefinition: { + readonly classNames: { + readonly toolbar: 'bui-HeaderToolbar'; + readonly toolbarWrapper: 'bui-HeaderToolbarWrapper'; + readonly toolbarContent: 'bui-HeaderToolbarContent'; + readonly toolbarControls: 'bui-HeaderToolbarControls'; + readonly toolbarIcon: 'bui-HeaderToolbarIcon'; + readonly toolbarName: 'bui-HeaderToolbarName'; + readonly tabsWrapper: 'bui-HeaderTabsWrapper'; + }; +}; + // @public export const HeaderPage: (props: HeaderPageProps) => JSX_2.Element; @@ -924,6 +904,17 @@ export interface HeaderPageBreadcrumb { label: string; } +// @public +export const HeaderPageDefinition: { + readonly classNames: { + readonly root: 'bui-HeaderPage'; + readonly content: 'bui-HeaderPageContent'; + readonly breadcrumbs: 'bui-HeaderPageBreadcrumbs'; + readonly tabsWrapper: 'bui-HeaderPageTabsWrapper'; + readonly controls: 'bui-HeaderPageControls'; + }; +}; + // @public export interface HeaderPageProps { // (undocumented) @@ -981,14 +972,37 @@ export const Link: ForwardRefExoticComponent< LinkProps & RefAttributes >; +// @public +export const LinkDefinition: { + readonly classNames: { + readonly root: 'bui-Link'; + }; + readonly dataAttributes: { + readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; + readonly weight: readonly ['regular', 'bold']; + readonly color: readonly [ + 'primary', + 'secondary', + 'danger', + 'warning', + 'success', + ]; + readonly truncate: readonly [true, false]; + }; +}; + // @public (undocumented) export interface LinkProps extends LinkProps_2 { + // (undocumented) + children?: ReactNode; // (undocumented) color?: | TextColors | TextColorStatus | Partial>; // (undocumented) + title?: string; + // (undocumented) truncate?: boolean; // (undocumented) variant?: TextVariants | Partial>; @@ -1020,7 +1034,7 @@ export interface MenuAutocompleteListBoxProps // (undocumented) placeholder?: string; // (undocumented) - placement?: PopoverProps['placement']; + placement?: PopoverProps_2['placement']; // (undocumented) virtualized?: boolean; } @@ -1036,11 +1050,33 @@ export interface MenuAutocompleteProps // (undocumented) placeholder?: string; // (undocumented) - placement?: PopoverProps['placement']; + placement?: PopoverProps_2['placement']; // (undocumented) virtualized?: boolean; } +// @public +export const MenuDefinition: { + readonly classNames: { + readonly root: 'bui-Menu'; + readonly popover: 'bui-MenuPopover'; + readonly content: 'bui-MenuContent'; + readonly section: 'bui-MenuSection'; + readonly sectionHeader: 'bui-MenuSectionHeader'; + readonly item: 'bui-MenuItem'; + readonly itemListBox: 'bui-MenuItemListBox'; + readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck'; + readonly itemWrapper: 'bui-MenuItemWrapper'; + readonly itemContent: 'bui-MenuItemContent'; + readonly itemArrow: 'bui-MenuItemArrow'; + readonly separator: 'bui-MenuSeparator'; + readonly searchField: 'bui-MenuSearchField'; + readonly searchFieldInput: 'bui-MenuSearchFieldInput'; + readonly searchFieldClear: 'bui-MenuSearchFieldClear'; + readonly emptyState: 'bui-MenuEmptyState'; + }; +}; + // @public (undocumented) export const MenuItem: (props: MenuItemProps) => JSX_2.Element; @@ -1079,7 +1115,7 @@ export interface MenuListBoxProps // (undocumented) maxWidth?: string; // (undocumented) - placement?: PopoverProps['placement']; + placement?: PopoverProps_2['placement']; // (undocumented) virtualized?: boolean; } @@ -1093,7 +1129,7 @@ export interface MenuProps // (undocumented) maxWidth?: string; // (undocumented) - placement?: PopoverProps['placement']; + placement?: PopoverProps_2['placement']; // (undocumented) virtualized?: boolean; } @@ -1123,6 +1159,118 @@ export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element; // @public (undocumented) export interface MenuTriggerProps extends MenuTriggerProps_2 {} +// @public (undocumented) +export interface NoPagination { + // (undocumented) + type: 'none'; +} + +// @public (undocumented) +export interface OffsetParams { + // (undocumented) + filter: TFilter | undefined; + // (undocumented) + offset: number; + // (undocumented) + pageSize: number; + // (undocumented) + search: string; + // (undocumented) + signal: AbortSignal; + // (undocumented) + sort: SortDescriptor | null; +} + +// @public (undocumented) +export interface OffsetResponse { + // (undocumented) + data: T[]; + // (undocumented) + totalCount: number; +} + +// @public (undocumented) +type Option_2 = { + value: string; + label: string; + disabled?: boolean; +}; +export { Option_2 as Option }; + +// @public (undocumented) +export interface PagePagination extends TablePaginationProps { + // (undocumented) + type: 'page'; +} + +// @public (undocumented) +export interface PageSizeOption { + // (undocumented) + label: string; + // (undocumented) + value: number; +} + +// @public (undocumented) +export interface PaginationOptions + extends Partial< + Pick< + TablePaginationProps, + | 'pageSize' + | 'pageSizeOptions' + | 'onPageSizeChange' + | 'onNextPage' + | 'onPreviousPage' + | 'showPageSizeOptions' + | 'getLabel' + > + > { + // (undocumented) + initialOffset?: number; +} + +// @public +export const Popover: ForwardRefExoticComponent< + PopoverProps & RefAttributes +>; + +// @public +export const PopoverDefinition: { + readonly classNames: { + readonly root: 'bui-Popover'; + readonly arrow: 'bui-PopoverArrow'; + readonly content: 'bui-PopoverContent'; + }; +}; + +// @public +export interface PopoverProps extends Omit { + children: React.ReactNode; + hideArrow?: boolean; +} + +// @public (undocumented) +export interface QueryOptions { + // (undocumented) + filter?: TFilter; + // (undocumented) + initialFilter?: TFilter; + // (undocumented) + initialSearch?: string; + // (undocumented) + initialSort?: SortDescriptor; + // (undocumented) + onFilterChange?: (filter: TFilter) => void; + // (undocumented) + onSearchChange?: (search: string) => void; + // (undocumented) + onSortChange?: (sort: SortDescriptor) => void; + // (undocumented) + search?: string; + // (undocumented) + sort?: SortDescriptor | null; +} + // @public (undocumented) export const Radio: ForwardRefExoticComponent< RadioProps & RefAttributes @@ -1133,6 +1281,15 @@ export const RadioGroup: ForwardRefExoticComponent< RadioGroupProps & RefAttributes >; +// @public +export const RadioGroupDefinition: { + readonly classNames: { + readonly root: 'bui-RadioGroup'; + readonly content: 'bui-RadioGroupContent'; + readonly radio: 'bui-Radio'; + }; +}; + // @public (undocumented) export interface RadioGroupProps extends Omit, @@ -1150,11 +1307,42 @@ export type Responsive = T | Partial>; // @public (undocumented) export function Row(props: RowProps): JSX_2.Element; +// @public (undocumented) +export interface RowConfig { + // (undocumented) + getHref?: (item: T) => string | undefined; + // (undocumented) + getIsDisabled?: (item: T) => boolean; + // (undocumented) + onClick?: (item: T) => void; +} + +// @public (undocumented) +export type RowRenderFn = (params: { + item: T; + index: number; +}) => ReactNode; + // @public (undocumented) export const SearchField: ForwardRefExoticComponent< SearchFieldProps & RefAttributes >; +// @public +export const SearchFieldDefinition: { + readonly classNames: { + readonly root: 'bui-SearchField'; + readonly clear: 'bui-SearchFieldClear'; + readonly inputWrapper: 'bui-SearchFieldInputWrapper'; + readonly input: 'bui-SearchFieldInput'; + readonly inputIcon: 'bui-SearchFieldInputIcon'; + }; + readonly dataAttributes: { + readonly startCollapsed: readonly [true, false]; + readonly size: readonly ['small', 'medium']; + }; +}; + // @public (undocumented) export interface SearchFieldProps extends SearchFieldProps_2, @@ -1166,29 +1354,62 @@ export interface SearchFieldProps } // @public (undocumented) -export const Select: ForwardRefExoticComponent< - SelectProps & RefAttributes ->; +export interface SearchState { + // (undocumented) + onChange: (value: string) => void; + // (undocumented) + value: string; +} // @public (undocumented) -export interface SelectProps - extends SelectProps_2<{ - name: string; - value: string; - }>, +export const Select: ForwardRefExoticComponent< + SelectProps<'multiple' | 'single'> & RefAttributes +>; + +// @public +export const SelectDefinition: { + readonly classNames: { + readonly root: 'bui-Select'; + readonly popover: 'bui-SelectPopover'; + readonly trigger: 'bui-SelectTrigger'; + readonly chevron: 'bui-SelectTriggerChevron'; + readonly value: 'bui-SelectValue'; + readonly list: 'bui-SelectList'; + readonly item: 'bui-SelectItem'; + readonly itemIndicator: 'bui-SelectItemIndicator'; + readonly itemLabel: 'bui-SelectItemLabel'; + readonly searchWrapper: 'bui-SelectSearchWrapper'; + readonly search: 'bui-SelectSearch'; + readonly searchClear: 'bui-SelectSearchClear'; + readonly noResults: 'bui-SelectNoResults'; + }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium']; + }; +}; + +// @public (undocumented) +export interface SelectProps + extends SelectProps_2, Omit { icon?: ReactNode; - options?: Array<{ - value: string; - label: string; - disabled?: boolean; - }>; + options?: Array; + searchable?: boolean; + searchPlaceholder?: string; + selectionMode?: T; size?: 'small' | 'medium' | Partial>; } // @public (undocumented) export const Skeleton: (props: SkeletonProps) => JSX_2.Element; +// @public +export const SkeletonDefinition: { + readonly classNames: { + readonly root: 'bui-Skeleton'; + }; +}; + // @public (undocumented) export interface SkeletonProps extends ComponentProps<'div'> { // (undocumented) @@ -1199,6 +1420,17 @@ export interface SkeletonProps extends ComponentProps<'div'> { width?: number | string; } +// @public (undocumented) +export type SortDescriptor = SortDescriptor_2; + +// @public (undocumented) +export interface SortState { + // (undocumented) + descriptor: SortDescriptor | null; + // (undocumented) + onSortChange: (descriptor: SortDescriptor) => void; +} + // @public (undocumented) export type Space = | '0.5' @@ -1257,11 +1489,30 @@ export const SubmenuTrigger: (props: SubmenuTriggerProps) => JSX_2.Element; // @public (undocumented) export interface SubmenuTriggerProps extends SubmenuTriggerProps_2 {} +// @public +export type Surface = + | '0' + | '1' + | '2' + | '3' + | 'danger' + | 'warning' + | 'success' + | 'auto'; + // @public (undocumented) export const Switch: ForwardRefExoticComponent< SwitchProps & RefAttributes >; +// @public +export const SwitchDefinition: { + readonly classNames: { + readonly root: 'bui-Switch'; + readonly indicator: 'bui-SwitchIndicator'; + }; +}; + // @public (undocumented) export interface SwitchProps extends SwitchProps_2 { label?: string; @@ -1271,42 +1522,170 @@ export interface SwitchProps extends SwitchProps_2 { export const Tab: (props: TabProps) => JSX_2.Element; // @public (undocumented) -export const Table: (props: TableProps) => JSX_2.Element; +export function Table({ + columnConfig, + data, + loading, + isStale, + error, + pagination, + sort, + rowConfig, + selection, + emptyState, + className, + style, +}: TableProps): JSX_2.Element; // @public (undocumented) export const TableBody: ( props: TableBodyProps, ) => JSX_2.Element; +// @public +export const TableDefinition: { + readonly classNames: { + readonly table: 'bui-Table'; + readonly header: 'bui-TableHeader'; + readonly body: 'bui-TableBody'; + readonly row: 'bui-TableRow'; + readonly head: 'bui-TableHead'; + readonly headContent: 'bui-TableHeadContent'; + readonly headSortButton: 'bui-TableHeadSortButton'; + readonly caption: 'bui-TableCaption'; + readonly cell: 'bui-TableCell'; + readonly cellContentWrapper: 'bui-TableCellContentWrapper'; + readonly cellContent: 'bui-TableCellContent'; + readonly cellIcon: 'bui-TableCellIcon'; + readonly cellProfileAvatar: 'bui-TableCellProfileAvatar'; + readonly cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage'; + readonly cellProfileAvatarFallback: 'bui-TableCellProfileAvatarFallback'; + readonly cellProfileName: 'bui-TableCellProfileName'; + readonly cellProfileLink: 'bui-TableCellProfileLink'; + readonly headSelection: 'bui-TableHeadSelection'; + readonly cellSelection: 'bui-TableCellSelection'; + }; + readonly dataAttributes: { + readonly stale: readonly [true, false]; + }; +}; + // @public (undocumented) export const TableHeader: ( props: TableHeaderProps, ) => JSX_2.Element; +// @public (undocumented) +export interface TableItem { + // (undocumented) + id: string | number; +} + // @public -export function TablePagination(props: TablePaginationProps): JSX_2.Element; +export function TablePagination({ + pageSize, + pageSizeOptions, + offset, + totalCount, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + onPageSizeChange, + showPageSizeOptions, + getLabel, +}: TablePaginationProps): JSX_2.Element; + +// @public +export const TablePaginationDefinition: { + readonly classNames: { + readonly root: 'bui-TablePagination'; + readonly left: 'bui-TablePaginationLeft'; + readonly right: 'bui-TablePaginationRight'; + readonly select: 'bui-TablePaginationSelect'; + }; +}; // @public (undocumented) -export interface TablePaginationProps - extends React.HTMLAttributes { +export interface TablePaginationProps { + // (undocumented) + getLabel?: (params: { + pageSize: number; + offset?: number; + totalCount?: number; + }) => string; + // (undocumented) + hasNextPage: boolean; + // (undocumented) + hasPreviousPage: boolean; // (undocumented) offset?: number; // (undocumented) - onNextPage?: () => void; + onNextPage: () => void; // (undocumented) - onPageSizeChange?: (pageSize: number) => void; + onPageSizeChange?: (size: number) => void; // (undocumented) - onPreviousPage?: () => void; + onPreviousPage: () => void; // (undocumented) - pageSize?: number; + pageSize: number; // (undocumented) - rowCount?: number; - // (undocumented) - setOffset?: (offset: number) => void; - // (undocumented) - setPageSize?: (pageSize: number) => void; + pageSizeOptions?: number[] | PageSizeOption[]; // (undocumented) showPageSizeOptions?: boolean; + // (undocumented) + totalCount?: number; +} + +// @public (undocumented) +export type TablePaginationType = NoPagination | PagePagination; + +// @public (undocumented) +export interface TableProps { + // (undocumented) + className?: string; + // (undocumented) + columnConfig: readonly ColumnConfig[]; + // (undocumented) + data: T[] | undefined; + // (undocumented) + emptyState?: ReactNode; + // (undocumented) + error?: Error; + // (undocumented) + isStale?: boolean; + // (undocumented) + loading?: boolean; + // (undocumented) + pagination: TablePaginationType; + // (undocumented) + rowConfig?: RowConfig | RowRenderFn; + // (undocumented) + selection?: TableSelection; + // (undocumented) + sort?: SortState; + // (undocumented) + style?: React.CSSProperties; +} + +// @public (undocumented) +export const TableRoot: (props: TableRootProps) => JSX_2.Element; + +// @public (undocumented) +export interface TableRootProps extends TableProps_2 { + // (undocumented) + stale?: boolean; +} + +// @public (undocumented) +export interface TableSelection { + // (undocumented) + behavior?: TableProps_2['selectionBehavior']; + // (undocumented) + mode?: TableProps_2['selectionMode']; + // (undocumented) + onSelectionChange?: TableProps_2['onSelectionChange']; + // (undocumented) + selected?: TableProps_2['selectedKeys']; } // @public @@ -1332,6 +1711,19 @@ export interface TabProps extends TabProps_2 { // @public export const Tabs: (props: TabsProps) => JSX_2.Element | null; +// @public +export const TabsDefinition: { + readonly classNames: { + readonly tabs: 'bui-Tabs'; + readonly tabList: 'bui-TabList'; + readonly tabListWrapper: 'bui-TabListWrapper'; + readonly tab: 'bui-Tab'; + readonly tabActive: 'bui-TabActive'; + readonly tabHovered: 'bui-TabHovered'; + readonly panel: 'bui-TabPanel'; + }; +}; + // @public export interface TabsProps extends TabsProps_2 {} @@ -1343,6 +1735,17 @@ export const TagGroup: ( props: TagGroupProps, ) => JSX_2.Element; +// @public +export const TagGroupDefinition: { + readonly classNames: { + readonly group: 'bui-TagGroup'; + readonly list: 'bui-TagList'; + readonly tag: 'bui-Tag'; + readonly tagIcon: 'bui-TagIcon'; + readonly tagRemoveButton: 'bui-TagRemoveButton'; + }; +}; + // @public export interface TagGroupProps extends Omit, @@ -1371,11 +1774,46 @@ export type TextColors = 'primary' | 'secondary'; // @public (undocumented) export type TextColorStatus = 'danger' | 'warning' | 'success'; +// @public +export const TextDefinition: { + readonly classNames: { + readonly root: 'bui-Text'; + }; + readonly dataAttributes: { + readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; + readonly weight: readonly ['regular', 'bold']; + readonly color: readonly [ + 'primary', + 'secondary', + 'danger', + 'warning', + 'success', + ]; + readonly truncate: readonly [true, false]; + }; +}; + // @public (undocumented) export const TextField: ForwardRefExoticComponent< TextFieldProps & RefAttributes >; +// @public +export const TextFieldDefinition: { + readonly classNames: { + readonly root: 'bui-TextField'; + readonly inputWrapper: 'bui-InputWrapper'; + readonly input: 'bui-Input'; + readonly inputIcon: 'bui-InputIcon'; + readonly inputAction: 'bui-InputAction'; + }; + readonly dataAttributes: { + readonly invalid: readonly [true, false]; + readonly disabled: readonly [true, false]; + readonly size: readonly ['small', 'medium']; + }; +}; + // @public (undocumented) export interface TextFieldProps extends TextFieldProps_2, @@ -1430,11 +1868,68 @@ export type TextVariants = // @public (undocumented) export type TextWeights = 'regular' | 'bold'; +// @public (undocumented) +export const ToggleButton: ForwardRefExoticComponent< + ToggleButtonProps & RefAttributes +>; + +// @public +export const ToggleButtonDefinition: { + readonly classNames: { + readonly root: 'bui-ToggleButton'; + readonly content: 'bui-ToggleButtonContent'; + }; + readonly dataAttributes: { + readonly size: readonly ['small', 'medium']; + }; +}; + +// @public (undocumented) +export const ToggleButtonGroup: ForwardRefExoticComponent< + ToggleButtonGroupProps & RefAttributes +>; + +// @public +export const ToggleButtonGroupDefinition: { + readonly classNames: { + readonly root: 'bui-ToggleButtonGroup'; + }; + readonly dataAttributes: { + readonly orientation: readonly ['horizontal', 'vertical']; + }; +}; + +// @public (undocumented) +export interface ToggleButtonGroupProps + extends Omit { + // (undocumented) + orientation?: NonNullable; +} + +// @public +export interface ToggleButtonProps extends ToggleButtonProps_2 { + // (undocumented) + iconEnd?: ReactElement; + // (undocumented) + iconStart?: ReactElement; + onSurface?: Responsive; + // (undocumented) + size?: 'small' | 'medium' | Partial>; +} + // @public (undocumented) export const Tooltip: ForwardRefExoticComponent< TooltipProps & RefAttributes >; +// @public +export const TooltipDefinition: { + readonly classNames: { + readonly tooltip: 'bui-Tooltip'; + readonly arrow: 'bui-TooltipArrow'; + }; +}; + // @public (undocumented) export interface TooltipProps extends Omit { // (undocumented) @@ -1453,48 +1948,69 @@ export const useBreakpoint: () => { down: (key: Breakpoint) => boolean; }; -// @public -export function useTable( - config?: UseTableConfig, -): UseTableResult; +// @public (undocumented) +export function useTable( + options: UseTableOptions, +): UseTableResult; // @public (undocumented) -export interface UseTableConfig { - data?: T[]; - pagination?: UseTablePaginationConfig; +export interface UseTableCompleteOptions + extends QueryOptions { + // (undocumented) + filterFn?: (data: T[], filter: TFilter) => T[]; + // (undocumented) + getData: () => T[] | Promise; + // (undocumented) + mode: 'complete'; + // (undocumented) + paginationOptions?: PaginationOptions; + // (undocumented) + searchFn?: (data: T[], search: string) => T[]; + // (undocumented) + sortFn?: (data: T[], sort: SortDescriptor) => T[]; } // @public (undocumented) -export interface UseTablePagination { - data?: T[]; - nextPage: () => void; - offset: number; - pageSize: number; - paginationProps: TablePaginationProps; - previousPage: () => void; - setOffset: (offset: number) => void; - setPageSize: (pageSize: number) => void; +export interface UseTableCursorOptions + extends QueryOptions { + // (undocumented) + getData: (params: CursorParams) => Promise>; + // (undocumented) + mode: 'cursor'; + // (undocumented) + paginationOptions?: Omit; } // @public (undocumented) -export interface UseTablePaginationConfig { - defaultOffset?: number; - defaultPageSize?: number; - offset?: number; - onNextPage?: () => void; - onOffsetChange?: (offset: number) => void; - onPageSizeChange?: (pageSize: number) => void; - onPreviousPage?: () => void; - pageSize?: number; - rowCount?: number; - showPageSizeOptions?: boolean; +export interface UseTableOffsetOptions + extends QueryOptions { + // (undocumented) + getData: (params: OffsetParams) => Promise>; + // (undocumented) + mode: 'offset'; + // (undocumented) + paginationOptions?: PaginationOptions; } // @public (undocumented) -export interface UseTableResult { - data?: T[]; - pagination: UseTablePagination; - paginationProps: TablePaginationProps; +export type UseTableOptions = + | UseTableCompleteOptions + | UseTableOffsetOptions + | UseTableCursorOptions; + +// @public (undocumented) +export interface UseTableResult { + // (undocumented) + filter: FilterState; + // (undocumented) + reload: () => void; + // (undocumented) + search: SearchState; + // (undocumented) + tableProps: Omit< + TableProps, + 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState' + >; } // @public (undocumented) @@ -1530,6 +2046,13 @@ export interface UtilityProps extends SpaceProps { // @public export const VisuallyHidden: (props: VisuallyHiddenProps) => JSX_2.Element; +// @public +export const VisuallyHiddenDefinition: { + readonly classNames: { + readonly root: 'bui-VisuallyHidden'; + }; +}; + // @public export interface VisuallyHiddenProps extends ComponentProps<'div'> { // (undocumented) diff --git a/packages/ui/src/components/Accordion/Accordion.module.css b/packages/ui/src/components/Accordion/Accordion.module.css new file mode 100644 index 0000000000..a86400986d --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.module.css @@ -0,0 +1,93 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-Accordion { + width: 100%; + background-color: var(--bui-bg-surface-1); + border-radius: var(--bui-radius-3); + padding: var(--bui-space-3); + } + + .bui-AccordionTrigger { + all: unset; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + } + + .bui-AccordionTriggerButton { + all: unset; + width: 100%; + color: var(--bui-fg-primary); + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + text-align: left; + + &:focus-visible { + outline: none; + transition: none; + box-shadow: inset 0 0 0 2px var(--bui-ring); + } + + &[data-disabled='true'] { + background-color: transparent; + color: var(--bui-fg-disabled); + cursor: not-allowed; + } + } + + .bui-AccordionTriggerTitle { + font-size: var(--bui-font-size-4); + font-weight: var(--bui-font-weight-bold); + line-height: 140%; + } + + .bui-AccordionTriggerSubtitle { + font-size: var(--bui-font-size-2); + line-height: 140%; + color: var(--bui-fg-secondary); + } + + .bui-AccordionTriggerIcon { + transition: transform 150ms ease-out; + flex-shrink: 0; + width: 1rem; + height: 1rem; + + [data-expanded='true'] & { + transform: rotate(180deg); + } + } + + .bui-AccordionPanel { + [data-expanded='true'] & { + padding-top: var(--bui-space-1); + } + } + + .bui-AccordionGroup { + display: flex; + flex-direction: column; + gap: var(--bui-space-3); + width: 100%; + } +} diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx new file mode 100644 index 0000000000..541cce5c84 --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx @@ -0,0 +1,173 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import preview from '../../../../../.storybook/preview'; +import { + Accordion, + AccordionTrigger, + AccordionPanel, + AccordionGroup, +} from './Accordion'; +import { Box } from '../Box'; +import { Text } from '../Text'; + +const Content = () => ( + + + It's the edge of the world and all of Western civilization + + + The sun may rise in the East, at least it settled in a final location + + It's understood that Hollywood sells Californication + +); + +const meta = preview.meta({ + title: 'Backstage UI/Accordion', + component: Accordion, +}); + +export const Default = meta.story({ + render: () => ( + + + + + + + ), +}); + +export const WithSubtitle = meta.story({ + render: () => ( + + + + + + + ), +}); + +export const CustomTrigger = meta.story({ + render: () => ( + + + + + Custom Multi-line Trigger + + + Click to expand additional details and configuration options + + + + + + + + ), +}); + +export const DefaultExpanded = meta.story({ + render: () => ( + + + + + + + ), +}); + +export const GroupSingleOpen = meta.story({ + render: () => ( + + + + + + + It's the edge of the world and all of Western civilization + + + + + + + + + + The sun may rise in the East, at least it settled in a final + location + + + + + + + + + + It's understood that Hollywood sells Californication + + + + + + ), +}); + +export const GroupMultipleOpen = meta.story({ + render: () => ( + + + + + + + It's the edge of the world and all of Western civilization + + + + + + + + + + The sun may rise in the East, at least it settled in a final + location + + + + + + + + + + It's understood that Hollywood sells Californication + + + + + + ), +}); diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000000..f31b923490 --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef } from 'react'; +import { + Disclosure as RADisclosure, + Button as RAButton, + DisclosurePanel as RADisclosurePanel, + DisclosureGroup as RADisclosureGroup, + Heading as RAHeading, +} from 'react-aria-components'; +import { RiArrowDownSLine } from '@remixicon/react'; +import clsx from 'clsx'; +import type { + AccordionProps, + AccordionTriggerProps, + AccordionPanelProps, + AccordionGroupProps, +} from './types'; +import { useStyles } from '../../hooks/useStyles'; +import { AccordionDefinition } from './definition'; +import styles from './Accordion.module.css'; +import { Flex } from '../Flex'; + +/** @public */ +export const Accordion = forwardRef< + React.ElementRef, + AccordionProps +>(({ className, ...props }, ref) => { + const { classNames, cleanedProps } = useStyles(AccordionDefinition, props); + + return ( + + ); +}); + +Accordion.displayName = 'Accordion'; + +/** @public */ +export const AccordionTrigger = forwardRef< + React.ElementRef, + AccordionTriggerProps +>(({ className, title, subtitle, children, ...props }, ref) => { + const { classNames, cleanedProps } = useStyles(AccordionDefinition, props); + + return ( + + + {children ? ( + children + ) : ( + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + )} + + + + + ); +}); + +AccordionTrigger.displayName = 'AccordionTrigger'; + +/** @public */ +export const AccordionPanel = forwardRef< + React.ElementRef, + AccordionPanelProps +>(({ className, ...props }, ref) => { + const { classNames, cleanedProps } = useStyles(AccordionDefinition, props); + + return ( + + ); +}); + +AccordionPanel.displayName = 'AccordionPanel'; + +/** @public */ +export const AccordionGroup = forwardRef< + React.ElementRef, + AccordionGroupProps +>(({ className, allowsMultiple = false, ...props }, ref) => { + const { classNames, cleanedProps } = useStyles(AccordionDefinition, props); + + return ( + + ); +}); + +AccordionGroup.displayName = 'AccordionGroup'; diff --git a/packages/ui/src/components/Accordion/definition.ts b/packages/ui/src/components/Accordion/definition.ts new file mode 100644 index 0000000000..afc508b922 --- /dev/null +++ b/packages/ui/src/components/Accordion/definition.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Accordion + * @public + */ +export const AccordionDefinition = { + classNames: { + root: 'bui-Accordion', + trigger: 'bui-AccordionTrigger', + triggerButton: 'bui-AccordionTriggerButton', + triggerTitle: 'bui-AccordionTriggerTitle', + triggerSubtitle: 'bui-AccordionTriggerSubtitle', + triggerIcon: 'bui-AccordionTriggerIcon', + panel: 'bui-AccordionPanel', + group: 'bui-AccordionGroup', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Accordion/index.ts b/packages/ui/src/components/Accordion/index.ts new file mode 100644 index 0000000000..e20f49bec7 --- /dev/null +++ b/packages/ui/src/components/Accordion/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + Accordion, + AccordionTrigger, + AccordionPanel, + AccordionGroup, +} from './Accordion'; +export { AccordionDefinition } from './definition'; +export type { + AccordionProps, + AccordionTriggerProps, + AccordionPanelProps, + AccordionGroupProps, +} from './types'; diff --git a/packages/ui/src/components/Accordion/types.ts b/packages/ui/src/components/Accordion/types.ts new file mode 100644 index 0000000000..06c8549ebd --- /dev/null +++ b/packages/ui/src/components/Accordion/types.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + DisclosureProps as RADisclosureProps, + HeadingProps as RAHeadingProps, + DisclosurePanelProps as RADisclosurePanelProps, + DisclosureGroupProps as RADisclosureGroupProps, +} from 'react-aria-components'; + +/** + * Props for the Accordion component. + * @public + */ +export interface AccordionProps extends RADisclosureProps { + className?: string; +} + +/** + * Props for the AccordionTrigger component. + * @public + */ +export interface AccordionTriggerProps extends RAHeadingProps { + className?: string; + title?: string; + subtitle?: string; + children?: React.ReactNode; +} + +/** + * Props for the AccordionPanel component. + * @public + */ +export interface AccordionPanelProps extends RADisclosurePanelProps { + className?: string; +} + +/** + * Props for the AccordionGroup component. + * @public + */ +export interface AccordionGroupProps extends RADisclosureGroupProps { + className?: string; + /** + * Whether multiple accordions can be expanded at the same time. + * @defaultValue false + */ + allowsMultiple?: boolean; +} diff --git a/packages/ui/src/components/Avatar/Avatar.module.css b/packages/ui/src/components/Avatar/Avatar.module.css index f1517db3c2..c1fec3df9b 100644 --- a/packages/ui/src/components/Avatar/Avatar.module.css +++ b/packages/ui/src/components/Avatar/Avatar.module.css @@ -34,6 +34,11 @@ width: 2rem; } + .bui-AvatarRoot[data-size='x-small'] { + height: 1.25rem; + width: 1.25rem; + } + .bui-AvatarRoot[data-size='small'] { height: 1.5rem; width: 1.5rem; @@ -45,6 +50,11 @@ } .bui-AvatarRoot[data-size='large'] { + height: 2.5rem; + width: 2.5rem; + } + + .bui-AvatarRoot[data-size='x-large'] { height: 3rem; width: 3rem; } @@ -53,6 +63,7 @@ object-fit: cover; height: 100%; width: 100%; + display: block; } .bui-AvatarFallback { diff --git a/packages/ui/src/components/Avatar/Avatar.stories.tsx b/packages/ui/src/components/Avatar/Avatar.stories.tsx index c91fe736db..75c1e68a67 100644 --- a/packages/ui/src/components/Avatar/Avatar.stories.tsx +++ b/packages/ui/src/components/Avatar/Avatar.stories.tsx @@ -13,42 +13,80 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Avatar } from './index'; -import { Flex } from '../..'; +import { Flex, Text } from '../..'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Avatar', component: Avatar, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { src: 'https://avatars.githubusercontent.com/u/1540635?v=4', name: 'Charles de Dreuille', }, -}; +}); -export const Fallback: Story = { +export const Fallback = meta.story({ args: { - ...Default.args, + ...Default.input.args, src: 'https://avatars.githubusercontent.com/u/15406AAAAAAAAA', }, -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( - - - - + + + + + + + + + + + + + + + ), -}; +}); + +export const Purpose = meta.story({ + args: { + ...Default.input.args, + }, + render: args => ( + + + Informative (default) + + Use when avatar appears alone. Announced as "{args.name}" to screen + readers: + + + + + + + Decoration + + Use when name appears adjacent to avatar. Hidden from screen readers + to avoid redundancy: + + + + {args.name} + + + + ), +}); diff --git a/packages/ui/src/components/Avatar/Avatar.tsx b/packages/ui/src/components/Avatar/Avatar.tsx index 000087d451..920a9b127f 100644 --- a/packages/ui/src/components/Avatar/Avatar.tsx +++ b/packages/ui/src/components/Avatar/Avatar.tsx @@ -14,48 +14,80 @@ * limitations under the License. */ -import { forwardRef, ElementRef } from 'react'; -import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar'; +import { forwardRef, useState, useEffect } from 'react'; import clsx from 'clsx'; import { AvatarProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { AvatarDefinition } from './definition'; import styles from './Avatar.module.css'; /** @public */ -export const Avatar = forwardRef< - ElementRef, - AvatarProps ->((props, ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles('Avatar', { - size: 'medium', - ...props, - }); +export const Avatar = forwardRef((props, ref) => { + const { classNames, dataAttributes, cleanedProps } = useStyles( + AvatarDefinition, + { + size: 'medium', + purpose: 'informative', + ...props, + }, + ); - const { className, src, name, ...rest } = cleanedProps; + const { className, src, name, purpose, ...rest } = cleanedProps; + + const [imageStatus, setImageStatus] = useState< + 'loading' | 'loaded' | 'error' + >('loading'); + + useEffect(() => { + setImageStatus('loading'); + const img = new Image(); + img.onload = () => setImageStatus('loaded'); + img.onerror = () => setImageStatus('error'); + img.src = src; + + return () => { + img.onload = null; + img.onerror = null; + }; + }, [src]); + + const initialsCount = ['x-small', 'small'].includes(cleanedProps.size) + ? 1 + : 2; + + const initials = name + .split(' ') + .map(word => word[0]) + .join('') + .toLocaleUpperCase('en-US') + .slice(0, initialsCount); return ( - - - - {(name || '') - .split(' ') - .map(word => word[0]) - .join('') - .toLocaleUpperCase('en-US') - .slice(0, 2)} - - + {imageStatus === 'loaded' ? ( + + ) : ( + + )} + ); }); -Avatar.displayName = AvatarPrimitive.Root.displayName; +Avatar.displayName = 'Avatar'; diff --git a/packages/ui/src/components/Avatar/definition.ts b/packages/ui/src/components/Avatar/definition.ts new file mode 100644 index 0000000000..8ad2c31bdc --- /dev/null +++ b/packages/ui/src/components/Avatar/definition.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Avatar + * @public + */ +export const AvatarDefinition = { + classNames: { + root: 'bui-AvatarRoot', + image: 'bui-AvatarImage', + fallback: 'bui-AvatarFallback', + }, + dataAttributes: { + size: ['small', 'medium', 'large'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Avatar/index.ts b/packages/ui/src/components/Avatar/index.ts index d5ab42beb2..38e896bdb4 100644 --- a/packages/ui/src/components/Avatar/index.ts +++ b/packages/ui/src/components/Avatar/index.ts @@ -15,4 +15,5 @@ */ export { Avatar } from './Avatar'; +export { AvatarDefinition } from './definition'; export type { AvatarProps } from './types'; diff --git a/packages/ui/src/components/Avatar/types.ts b/packages/ui/src/components/Avatar/types.ts index b2e6cf39cf..a252861ea4 100644 --- a/packages/ui/src/components/Avatar/types.ts +++ b/packages/ui/src/components/Avatar/types.ts @@ -14,12 +14,29 @@ * limitations under the License. */ -import { Avatar } from '@base-ui-components/react/avatar'; - /** @public */ -export interface AvatarProps - extends React.ComponentPropsWithoutRef { +export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> { + /** + * URL of the image to display + */ src: string; + + /** + * Name of the person - used for generating initials and accessibility labels + */ name: string; - size?: 'small' | 'medium' | 'large'; + + /** + * Size of the avatar + * @defaultValue 'medium' + */ + size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; + + /** + * Determines how the avatar is presented to assistive technologies. + * - 'informative': Avatar is announced as "\{name\}" to screen readers + * - 'decoration': Avatar is hidden from screen readers (use when name appears in adjacent text) + * @defaultValue 'informative' + */ + purpose?: 'decoration' | 'informative'; } diff --git a/packages/ui/src/components/Box/Box.module.css b/packages/ui/src/components/Box/Box.module.css index 688358fda0..f5af895f85 100644 --- a/packages/ui/src/components/Box/Box.module.css +++ b/packages/ui/src/components/Box/Box.module.css @@ -22,4 +22,32 @@ font-weight: var(--bui-font-weight-regular); color: var(--bui-fg-primary); } + + .bui-Box[data-surface='0'] { + background-color: var(--bui-bg-surface-0); + } + + .bui-Box[data-surface='1'] { + background-color: var(--bui-bg-surface-1); + } + + .bui-Box[data-surface='2'] { + background-color: var(--bui-bg-surface-2); + } + + .bui-Box[data-surface='3'] { + background-color: var(--bui-bg-surface-3); + } + + .bui-Box[data-surface='danger'] { + background-color: var(--bui-bg-danger); + } + + .bui-Box[data-surface='warning'] { + background-color: var(--bui-bg-warning); + } + + .bui-Box[data-surface='success'] { + background-color: var(--bui-bg-success); + } } diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index 6637f45740..db0c05a24b 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -1,3 +1,4 @@ +import preview from '../../../../../.storybook/preview'; /* * Copyright 2024 The Backstage Authors * @@ -15,11 +16,11 @@ */ import { ReactNode } from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from './Box'; import { Flex } from '../Flex'; +import { Button } from '../Button'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Box', component: Box, argTypes: { @@ -31,10 +32,7 @@ const meta = { control: false, }, }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const diagonalStripePattern = (() => { const svg = ` @@ -47,7 +45,7 @@ const diagonalStripePattern = (() => { return `data:image/svg+xml,${encodeURIComponent(svg)}`; })(); -export const Default: Story = { +export const Default = meta.story({ args: { width: '64px', height: '64px', @@ -63,10 +61,10 @@ export const Default: Story = { color: '#2563eb', }, }, -}; +}); -export const Margin: Story = { - args: { ...Default.args }, +export const Margin = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -89,10 +87,10 @@ export const Margin: Story = { ), -}; +}); -export const ResponsiveMargin: Story = { - args: { ...Default.args }, +export const ResponsiveMargin = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -115,10 +113,10 @@ export const ResponsiveMargin: Story = { ), -}; +}); -export const CustomMargin: Story = { - args: { ...Default.args }, +export const CustomMargin = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -141,10 +139,10 @@ export const CustomMargin: Story = { ), -}; +}); -export const CustomResponsiveMargin: Story = { - args: { ...Default.args }, +export const CustomResponsiveMargin = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -167,10 +165,10 @@ export const CustomResponsiveMargin: Story = { ), -}; +}); -export const Padding: Story = { - args: { ...Default.args }, +export const Padding = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -193,10 +191,10 @@ export const Padding: Story = { ), -}; +}); -export const ResponsivePadding: Story = { - args: { ...Default.args }, +export const ResponsivePadding = meta.story({ + args: { ...Default.input.args }, render: args => ( ), -}; +}); -export const CustomPadding: Story = { - args: { ...Default.args }, +export const CustomPadding = meta.story({ + args: { ...Default.input.args }, render: args => ( @@ -263,10 +261,10 @@ export const CustomPadding: Story = { ), -}; +}); -export const CustomResponsivePadding: Story = { - args: { ...Default.args }, +export const CustomResponsivePadding = meta.story({ + args: { ...Default.input.args }, render: args => ( ), -}; +}); const CardDisplay = ({ children }: { children?: ReactNode }) => { return ( @@ -327,7 +325,7 @@ const CardDisplay = ({ children }: { children?: ReactNode }) => { ); }; -export const Display: Story = { +export const Display = meta.story({ render: args => ( @@ -346,4 +344,92 @@ export const Display: Story = { ), -}; +}); + +export const Surfaces = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + Default + + Surface 0 + + + Surface 1 + + + Surface 2 + + + Surface 3 + + + Responsive Surface + + + Surface Danger + + + Surface Warning + + + Surface Success + + + ), +}); + +export const SurfacesNested = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + + In this test, we are nesting boxes and buttons on different surfaces to + ensure that the correct surface is applied to each element. If a Button + is placed on a surface that doesn't have the surface prop set, it will + inherit the surface from the parent. + + + + + + + + + + + + ), +}); + +export const SurfacesAutoIncrement = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + + Using surface="auto" automatically increments from the parent surface + level. This makes components more reusable as they don't need to know + their absolute surface level. Notice how each nested Box with + surface="auto" automatically increments: 0 → 1 → 2 → 3 (capped at 3). + + + Surface 0 (explicit) + + Surface auto (becomes 1) + + + Surface auto (becomes 2) + + Surface auto (becomes 3) + + Surface auto (stays 3 - capped) + + + + + + + ), +}); diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index fdcee3d382..49e6fae201 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -19,15 +19,24 @@ import { BoxProps } from './types'; import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; import styles from './Box.module.css'; +import { BoxDefinition } from './definition'; +import { SurfaceProvider, useSurface } from '../../hooks/useSurface'; /** @public */ export const Box = forwardRef((props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( - 'Box', - props, - ); + // Resolve the surface this Box creates for its children + // Using 'surface' parameter = container behavior (auto increments) + const { surface: resolvedSurface } = useSurface({ + surface: props.surface, + }); - const { as = 'div', children, className, ...rest } = cleanedProps; + const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = + useStyles(BoxDefinition, { + ...props, + surface: resolvedSurface, // Use resolved surface for data attribute + }); + + const { as = 'div', children, className, surface, ...rest } = cleanedProps; return createElement( as, @@ -40,9 +49,14 @@ export const Box = forwardRef((props, ref) => { className, ), style, + ...dataAttributes, ...rest, }, - children, + resolvedSurface ? ( + {children} + ) : ( + children + ), ); }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts new file mode 100644 index 0000000000..a9989daf46 --- /dev/null +++ b/packages/ui/src/components/Box/definition.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Box + * @public + */ +export const BoxDefinition = { + classNames: { + root: 'bui-Box', + }, + utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'position', + 'display', + 'width', + 'minWidth', + 'maxWidth', + 'height', + 'minHeight', + 'maxHeight', + ], + dataAttributes: { + surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Box/index.tsx b/packages/ui/src/components/Box/index.tsx index d47e653dfd..a2b2861efb 100644 --- a/packages/ui/src/components/Box/index.tsx +++ b/packages/ui/src/components/Box/index.tsx @@ -14,4 +14,5 @@ * limitations under the License. */ export { Box } from './Box'; +export { BoxDefinition } from './definition'; export type * from './types'; diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index c0f4a71035..dd72c4b820 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { SpaceProps, Responsive } from '../../types'; +import type { SpaceProps, Responsive, Surface } from '../../types'; /** @public */ export interface BoxProps extends SpaceProps { @@ -32,4 +32,5 @@ export interface BoxProps extends SpaceProps { children?: React.ReactNode; className?: string; style?: React.CSSProperties; + surface?: Responsive; } diff --git a/packages/ui/src/components/Button/Button.module.css b/packages/ui/src/components/Button/Button.module.css index 2216d0882b..fd57682406 100644 --- a/packages/ui/src/components/Button/Button.module.css +++ b/packages/ui/src/components/Button/Button.module.css @@ -18,60 +18,96 @@ @layer components { .bui-Button { - border: none; + --loading-duration: 200ms; + --bg: transparent; + --bg-hover: transparent; + --bg-active: transparent; + --fg: inherit; + + position: relative; display: inline-flex; - align-items: center; - justify-content: center; + border: none; user-select: none; font-family: var(--bui-font-regular); font-weight: var(--bui-font-weight-bold); padding: 0; cursor: pointer; border-radius: var(--bui-radius-2); - gap: var(--bui-space-1_5); flex-shrink: 0; + transition: background-color var(--loading-duration) ease-out, + box-shadow var(--loading-duration) ease-out; - &[data-disabled='true'] { - cursor: not-allowed; - } - } - - .bui-Button[data-variant='primary'] { - background-color: var(--bui-bg-solid); - color: var(--bui-fg-solid); + /* Apply variables */ + color: var(--fg); + background-color: var(--bg); &:hover { - background-color: var(--bui-bg-solid-hover); + background-color: var(--bg-hover); transition: background-color 150ms ease; } &:active { - background-color: var(--bui-bg-solid-pressed); + background-color: var(--bg-active); + } + + &[data-disabled='true'] { + cursor: not-allowed; + } + + &[data-loading='true'] { + cursor: wait; + } + } + + .bui-Button[data-variant='primary'] { + --bg: var(--bui-bg-solid); + --bg-hover: var(--bui-bg-solid-hover); + --bg-active: var(--bui-bg-solid-pressed); + --fg: var(--bui-fg-solid); + + &[data-disabled='true'], + &[data-loading='true'] { + --bg: var(--bui-bg-solid-disabled); + --bg-hover: var(--bui-bg-solid-disabled); + --bg-active: var(--bui-bg-solid-disabled); + --fg: var(--bui-fg-solid-disabled); } &:focus-visible { outline: 2px solid var(--bui-ring); outline-offset: 2px; } - - &[data-disabled='true'] { - background-color: var(--bui-bg-solid-disabled); - color: var(--bui-fg-solid-disabled); - } } .bui-Button[data-variant='secondary'] { - background-color: var(--bui-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--bui-border); - color: var(--bui-fg-primary); + --bg: var(--bui-bg-neutral-on-surface-0); + --bg-hover: var(--bui-bg-neutral-on-surface-0-hover); + --bg-active: var(--bui-bg-neutral-on-surface-0-pressed); + --fg: var(--bui-fg-primary); - &:hover { - box-shadow: inset 0 0 0 1px var(--bui-border-hover); - transition: box-shadow 150ms ease; + &[data-on-surface='1'] { + --bg: var(--bui-bg-neutral-on-surface-1); + --bg-hover: var(--bui-bg-neutral-on-surface-1-hover); + --bg-active: var(--bui-bg-neutral-on-surface-1-pressed); } - &:active { - box-shadow: inset 0 0 0 1px var(--bui-border-pressed); + &[data-on-surface='2'] { + --bg: var(--bui-bg-neutral-on-surface-2); + --bg-hover: var(--bui-bg-neutral-on-surface-2-hover); + --bg-active: var(--bui-bg-neutral-on-surface-2-pressed); + } + + &[data-on-surface='3'] { + --bg: var(--bui-bg-neutral-on-surface-3); + --bg-hover: var(--bui-bg-neutral-on-surface-3-hover); + --bg-active: var(--bui-bg-neutral-on-surface-3-pressed); + } + + &[data-disabled='true'], + &[data-loading='true'] { + --bg-hover: var(--bg); + --bg-active: var(--bg); + --fg: var(--bui-fg-disabled); } &:focus-visible { @@ -79,24 +115,33 @@ transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); } - - &[data-disabled='true'] { - box-shadow: inset 0 0 0 1px var(--bui-border-disabled); - color: var(--bui-fg-disabled); - } } .bui-Button[data-variant='tertiary'] { - background-color: transparent; - color: var(--bui-fg-primary); + --bg-hover: var(--bui-bg-neutral-on-surface-0-hover); + --bg-active: var(--bui-bg-neutral-on-surface-0-pressed); + --fg: var(--bui-fg-primary); - &:hover { - background-color: var(--bui-bg-surface-1); - transition: background-color 200ms ease; + &[data-on-surface='1'] { + --bg-hover: var(--bui-bg-neutral-on-surface-1-hover); + --bg-active: var(--bui-bg-neutral-on-surface-1-pressed); } - &:active { - background-color: var(--bui-bg-surface-2); + &[data-on-surface='2'] { + --bg-hover: var(--bui-bg-neutral-on-surface-2-hover); + --bg-active: var(--bui-bg-neutral-on-surface-2-pressed); + } + + &[data-on-surface='3'] { + --bg-hover: var(--bui-bg-neutral-on-surface-3-hover); + --bg-active: var(--bui-bg-neutral-on-surface-3-pressed); + } + + &[data-disabled='true'], + &[data-loading='true'] { + --bg-hover: var(--bg); + --bg-active: var(--bg); + --fg: var(--bui-fg-disabled); } &:focus-visible { @@ -104,10 +149,16 @@ transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); } + } - &[data-disabled='true'] { - background-color: transparent; - color: var(--bui-fg-disabled); + .bui-Button[data-size='small'] { + font-size: var(--bui-font-size-3); + padding: 0 var(--bui-space-2); + height: 2rem; + + svg { + width: 1rem; + height: 1rem; } } @@ -115,21 +166,70 @@ font-size: var(--bui-font-size-4); padding: 0 var(--bui-space-3); height: 2.5rem; + + svg { + width: 1.25rem; + height: 1.25rem; + } } - .bui-Button[data-size='small'] { - font-size: var(--bui-font-size-3); - padding: 0 var(--bui-space-2); - height: 2rem; + .bui-ButtonContent { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--bui-space-1_5); + height: 100%; + width: 100%; + transition: opacity var(--loading-duration) ease-out; + + .bui-Button[data-loading='true'] & { + opacity: 0; + } } - .bui-Button[data-size='small'] svg { - width: 1rem; - height: 1rem; + .bui-ButtonSpinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + opacity: 0; + transition: opacity var(--loading-duration) ease-in; + + .bui-Button[data-loading='true'] & { + opacity: 1; + } + + & svg { + animation: bui-spin 1s linear infinite; + } } - .bui-Button[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; + @media (prefers-reduced-motion: reduce) { + .bui-Button { + transition-duration: 50ms; + } + + .bui-ButtonContent { + transition-duration: 50ms; + } + + .bui-ButtonSpinner { + transition-duration: 50ms; + } + + .bui-ButtonSpinner svg { + animation: none; + } + } + + @keyframes bui-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } } } diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 6b74615eeb..22d21b08f9 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Button } from './Button'; import { Flex } from '../Flex'; +import { Box } from '../Box'; import { Text } from '../Text'; import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react'; +import { useState } from 'react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Button', component: Button, argTypes: { @@ -33,18 +34,15 @@ const meta = { options: ['primary', 'secondary'], }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { children: 'Button', }, -}; +}); -export const Variants: Story = { +export const Variants = meta.story({ args: { children: 'Button', }, @@ -56,21 +54,82 @@ export const Variants: Story = { }, }, render: () => ( - - - - + + + Default + + + + + + + + On Surface 0 + + + + + + + + On Surface 1 + + + + + + + + On Surface 2 + + + + + + + + On Surface 3 + + + + + + ), -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ args: { children: 'Button', }, @@ -84,9 +143,9 @@ export const Sizes: Story = { ), -}; +}); -export const WithIcons: Story = { +export const WithIcons = meta.story({ args: { children: 'Button', }, @@ -101,9 +160,9 @@ export const WithIcons: Story = { /> ), -}; +}); -export const FullWidth: Story = { +export const FullWidth = meta.story({ args: { children: 'Button', }, @@ -118,9 +177,9 @@ export const FullWidth: Story = { /> ), -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ render: () => ( ), -}; +}); -export const Responsive: Story = { +export const Responsive = meta.story({ args: { children: 'Button', variant: { @@ -148,67 +207,130 @@ export const Responsive: Story = { sm: 'medium', }, }, -}; +}); -const variants = ['primary', 'secondary'] as const; -const sizes = ['small', 'medium'] as const; +export const Loading = meta.story({ + render: () => { + const [isLoading, setIsLoading] = useState(false); -export const Playground: Story = { - args: { - children: 'Button', + const handleClick = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 3000); + }; + + return ( + + ); }, +}); + +export const LoadingVariants = meta.story({ render: () => ( - - {variants.map(variant => ( - - {variant} - {sizes.map(size => ( - - - - - - - - - - ))} - - ))} + + Primary + + + + + + + Secondary + + + + + + + Tertiary + + + + + + + Loading vs Disabled + + + + + ), -}; +}); + +export const OnSurfaceAuto = meta.story({ + render: () => ( + +
    + Using onSurface="auto" on buttons inherits their container's surface + level, making them reusable. This is equivalent to not specifying + onSurface. To override, use explicit surface values like onSurface="0" + or onSurface="2". +
    + + Surface 0 container + + + + + + + + Surface 1 container + + + + + + + + Surface 2 container + + + + + + +
    + ), +}); diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index d22a3036e6..fcacf7c22d 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -16,32 +16,97 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Button as RAButton } from 'react-aria-components'; +import { Button as RAButton, ProgressBar } from 'react-aria-components'; +import { RiLoader4Line } from '@remixicon/react'; import type { ButtonProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { ButtonDefinition } from './definition'; import styles from './Button.module.css'; +import { useSurface } from '../../hooks/useSurface'; -/** @public */ +/** + * A button component built on React Aria Components that provides accessible + * interactive elements for triggering actions. + * + * @remarks + * The Button component supports multiple variants (primary, secondary, tertiary, danger), + * sizes (small, medium), and states including loading and disabled. It automatically + * handles keyboard navigation, focus management, and ARIA attributes for accessibility. + * + * @example + * Basic usage: + * ```tsx + * + * ``` + * + * @example + * With icons and loading state: + * ```tsx + * + * ``` + * + * @public + */ export const Button = forwardRef( (props: ButtonProps, ref: Ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { - size: 'small', - variant: 'primary', - ...props, - }); + const { classNames, dataAttributes, cleanedProps } = useStyles( + ButtonDefinition, + { + size: 'small', + variant: 'primary', + ...props, + }, + ); - const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + const { + children, + className, + iconStart, + iconEnd, + loading, + onSurface, + ...rest + } = cleanedProps; + + const { surface } = useSurface({ onSurface }); return ( - {iconStart} - {children} - {iconEnd} + {({ isPending }) => ( + <> + + {iconStart} + {children} + {iconEnd} + + + {isPending && ( + + + )} + + )} ); }, diff --git a/packages/core-plugin-api/src/apis/definitions/AppLanguageApi.ts b/packages/ui/src/components/Button/definition.ts similarity index 53% rename from packages/core-plugin-api/src/apis/definitions/AppLanguageApi.ts rename to packages/ui/src/components/Button/definition.ts index 1207e84755..a017157774 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppLanguageApi.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 The 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,23 +14,21 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; - -/** @alpha */ -export type AppLanguageApi = { - getAvailableLanguages(): { languages: string[] }; - - setLanguage(language?: string): void; - - getLanguage(): { language: string }; - - language$(): Observable<{ language: string }>; -}; +import type { ComponentDefinition } from '../../types'; /** - * @alpha + * Component definition for Button + * @public */ -export const appLanguageApiRef: ApiRef = createApiRef({ - id: 'core.applanguage', -}); +export const ButtonDefinition = { + classNames: { + root: 'bui-Button', + content: 'bui-ButtonContent', + spinner: 'bui-ButtonSpinner', + }, + dataAttributes: { + size: ['small', 'medium', 'large'] as const, + variant: ['primary', 'secondary', 'tertiary'] as const, + loading: [true, false] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Button/index.ts b/packages/ui/src/components/Button/index.ts index b9c497ae1f..e624fcedde 100644 --- a/packages/ui/src/components/Button/index.ts +++ b/packages/ui/src/components/Button/index.ts @@ -16,3 +16,4 @@ export * from './Button'; export * from './types'; +export { ButtonDefinition } from './definition'; diff --git a/packages/ui/src/components/Button/types.ts b/packages/ui/src/components/Button/types.ts index cbe5bd3608..964b439bf6 100644 --- a/packages/ui/src/components/Button/types.ts +++ b/packages/ui/src/components/Button/types.ts @@ -17,6 +17,7 @@ import { Breakpoint } from '../..'; import { ReactElement, ReactNode } from 'react'; import { ButtonProps as RAButtonProps } from 'react-aria-components'; +import { Responsive, Surface } from '../../types'; /** * Properties for {@link Button} @@ -33,4 +34,7 @@ export interface ButtonProps extends RAButtonProps { iconStart?: ReactElement; iconEnd?: ReactElement; children?: ReactNode; + loading?: boolean; + /** Surface the button is placed on. Defaults to context surface if available */ + onSurface?: Responsive; } diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx index c1a908119f..be6dad7754 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { ButtonIcon } from './ButtonIcon'; import { Flex } from '../Flex'; +import { Text } from '../Text'; import { RiCloudLine } from '@remixicon/react'; +import { useState } from 'react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/ButtonIcon', component: ButtonIcon, argTypes: { @@ -32,16 +33,13 @@ const meta = { options: ['primary', 'secondary'], }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ render: () => } />, -}; +}); -export const Variants: Story = { +export const Variants = meta.story({ render: () => ( } variant="primary" /> @@ -49,18 +47,18 @@ export const Variants: Story = { } variant="tertiary" /> ), -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ render: () => ( } size="small" /> } size="medium" /> ), -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ render: () => ( } variant="primary" /> @@ -68,9 +66,9 @@ export const Disabled: Story = { } variant="tertiary" /> ), -}; +}); -export const Responsive: Story = { +export const Responsive = meta.story({ args: { variant: { initial: 'primary', @@ -82,4 +80,92 @@ export const Responsive: Story = { }, }, render: args => } />, -}; +}); + +export const Loading = meta.story({ + render: () => { + const [isLoading, setIsLoading] = useState(false); + + const handleClick = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 3000); + }; + + return ( + } + loading={isLoading} + onPress={handleClick} + /> + ); + }, +}); + +export const LoadingVariants = meta.story({ + render: () => ( + + Primary + + } + loading + /> + } + loading + /> + + + Secondary + + } + loading + /> + } + loading + /> + + + Tertiary + + } + loading + /> + } + loading + /> + + + Loading vs Disabled + + } loading /> + } isDisabled /> + } + loading + isDisabled + /> + + + ), +}); diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx index 2e6a96c4c1..9bbeca9d5b 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx @@ -16,24 +16,31 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Button as RAButton } from 'react-aria-components'; +import { Button as RAButton, ProgressBar } from 'react-aria-components'; +import { RiLoader4Line } from '@remixicon/react'; import type { ButtonIconProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { ButtonDefinition } from '../Button/definition'; +import { ButtonIconDefinition } from './definition'; import stylesButtonIcon from './ButtonIcon.module.css'; import stylesButton from '../Button/Button.module.css'; /** @public */ export const ButtonIcon = forwardRef( (props: ButtonIconProps, ref: Ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { - size: 'small', - variant: 'primary', - ...props, - }); + const { classNames, dataAttributes, cleanedProps } = useStyles( + ButtonDefinition, + { + size: 'small', + variant: 'primary', + ...props, + }, + ); - const { classNames: classNamesButtonIcon } = useStyles('ButtonIcon'); + const { classNames: classNamesButtonIcon } = + useStyles(ButtonIconDefinition); - const { className, icon, ...rest } = cleanedProps; + const { className, icon, loading, ...rest } = cleanedProps; return ( - {icon} + {({ isPending }) => ( + <> + + {icon} + + + {isPending && ( + + + )} + + )} ); }, diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts new file mode 100644 index 0000000000..48d0a558ab --- /dev/null +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for ButtonIcon + * @public + */ +export const ButtonIconDefinition = { + classNames: { + root: 'bui-ButtonIcon', + content: 'bui-ButtonIconContent', + spinner: 'bui-ButtonIconSpinner', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/ButtonIcon/index.tsx b/packages/ui/src/components/ButtonIcon/index.tsx index 686fd844e8..acede912ad 100644 --- a/packages/ui/src/components/ButtonIcon/index.tsx +++ b/packages/ui/src/components/ButtonIcon/index.tsx @@ -15,4 +15,5 @@ */ export * from './ButtonIcon'; +export { ButtonIconDefinition } from './definition'; export * from './types'; diff --git a/packages/ui/src/components/ButtonIcon/types.ts b/packages/ui/src/components/ButtonIcon/types.ts index bc358b6d7d..6af3d12361 100644 --- a/packages/ui/src/components/ButtonIcon/types.ts +++ b/packages/ui/src/components/ButtonIcon/types.ts @@ -31,4 +31,5 @@ export interface ButtonIconProps extends RAButtonProps { | 'tertiary' | Partial>; icon?: ReactElement; + loading?: boolean; } diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx index 7d288065a4..4b9b9b747c 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; +import type { StoryFn } from '@storybook/react-vite'; import { ButtonLink } from './ButtonLink'; import { Flex } from '../Flex'; import { MemoryRouter } from 'react-router-dom'; import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/ButtonLink', component: ButtonLink, decorators: [ @@ -40,18 +41,15 @@ const meta = { options: ['primary', 'secondary'], }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { children: 'Button', }, -}; +}); -export const Variants: Story = { +export const Variants = meta.story({ render: () => ( ), -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ args: { children: 'Button', }, @@ -96,9 +94,9 @@ export const Sizes: Story = {
    ), -}; +}); -export const WithIcons: Story = { +export const WithIcons = meta.story({ args: { children: 'Button', }, @@ -113,9 +111,9 @@ export const WithIcons: Story = { /> ), -}; +}); -export const FullWidth: Story = { +export const FullWidth = meta.story({ args: { children: 'Button', }, @@ -130,9 +128,9 @@ export const FullWidth: Story = { /> ), -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ render: () => ( @@ -146,9 +144,9 @@ export const Disabled: Story = { ), -}; +}); -export const Responsive: Story = { +export const Responsive = meta.story({ args: { children: 'Button', variant: { @@ -160,4 +158,4 @@ export const Responsive: Story = { sm: 'medium', }, }, -}; +}); diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index 70d216b3d8..db3c95ee54 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -20,6 +20,8 @@ import { Link as RALink, RouterProvider } from 'react-aria-components'; import { useNavigate, useHref } from 'react-router-dom'; import type { ButtonLinkProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { ButtonDefinition } from '../Button/definition'; +import { ButtonLinkDefinition } from './definition'; import { isExternalLink } from '../../utils/isExternalLink'; import stylesButton from '../Button/Button.module.css'; @@ -28,60 +30,55 @@ export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { const navigate = useNavigate(); - const { classNames, dataAttributes, cleanedProps } = useStyles('Button', { - size: 'small', - variant: 'primary', - ...props, - }); + const { classNames, dataAttributes, cleanedProps } = useStyles( + ButtonDefinition, + { + size: 'small', + variant: 'primary', + ...props, + }, + ); - const { classNames: classNamesButtonLink } = useStyles('ButtonLink'); + const { classNames: classNamesButtonLink } = + useStyles(ButtonLinkDefinition); const { children, className, iconStart, iconEnd, href, ...rest } = cleanedProps; const isExternal = isExternalLink(href); - // If it's an external link, render RALink without RouterProvider - if (isExternal) { - return ( - + {iconStart} {children} {iconEnd} - - ); + + + ); + + // If it's an external link, render RALink without RouterProvider + if (isExternal) { + return linkButton; } // For internal links, use RouterProvider return ( - - {iconStart} - {children} - {iconEnd} - + {linkButton} ); }, diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts new file mode 100644 index 0000000000..daf3fae1ea --- /dev/null +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for ButtonLink + * @public + */ +export const ButtonLinkDefinition = { + classNames: { + root: 'bui-ButtonLink', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/ButtonLink/index.ts b/packages/ui/src/components/ButtonLink/index.ts index bbfc696c67..b98f84d489 100644 --- a/packages/ui/src/components/ButtonLink/index.ts +++ b/packages/ui/src/components/ButtonLink/index.ts @@ -15,4 +15,5 @@ */ export * from './ButtonLink'; +export { ButtonLinkDefinition } from './definition'; export * from './types'; diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 170d2b7984..438dd0e353 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Card, CardHeader, CardBody, CardFooter } from './Card'; import { Text } from '../..'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Card', component: Card, subcomponents: { CardHeader, CardBody, CardFooter }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ render: args => ( Header @@ -35,19 +31,18 @@ export const Default: Story = { Footer ), -}; +}); -export const CustomSize: Story = { +export const CustomSize = Default.extend({ args: { style: { width: '300px', height: '200px', }, }, - render: Default.render, -}; +}); -export const WithLongBody: Story = { +export const WithLongBody = meta.story({ render: () => ( @@ -77,7 +72,7 @@ export const WithLongBody: Story = { ), -}; +}); const ListRow = ({ children }: { children: React.ReactNode }) => { return ( @@ -99,7 +94,7 @@ const ListRow = ({ children }: { children: React.ReactNode }) => { ); }; -export const WithListRow: Story = { +export const WithListRow = meta.story({ render: () => ( @@ -128,4 +123,4 @@ export const WithListRow: Story = { ), -}; +}); diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index b942b70649..a3f4995588 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -17,6 +17,7 @@ import { forwardRef } from 'react'; import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; +import { CardDefinition } from './definition'; import type { CardProps, CardHeaderProps, @@ -31,7 +32,7 @@ import styles from './Card.module.css'; * @public */ export const Card = forwardRef((props, ref) => { - const { classNames, cleanedProps } = useStyles('Card', props); + const { classNames, cleanedProps } = useStyles(CardDefinition, props); const { className, ...rest } = cleanedProps; return ( @@ -50,7 +51,7 @@ export const Card = forwardRef((props, ref) => { */ export const CardHeader = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles('Card', props); + const { classNames, cleanedProps } = useStyles(CardDefinition, props); const { className, ...rest } = cleanedProps; return ( @@ -74,7 +75,7 @@ export const CardHeader = forwardRef( */ export const CardBody = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles('Card', props); + const { classNames, cleanedProps } = useStyles(CardDefinition, props); const { className, ...rest } = cleanedProps; return ( @@ -94,7 +95,7 @@ export const CardBody = forwardRef( */ export const CardFooter = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles('Card', props); + const { classNames, cleanedProps } = useStyles(CardDefinition, props); const { className, ...rest } = cleanedProps; return ( diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts new file mode 100644 index 0000000000..cd0c5f634b --- /dev/null +++ b/packages/ui/src/components/Card/definition.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Card + * @public + */ +export const CardDefinition = { + classNames: { + root: 'bui-Card', + header: 'bui-CardHeader', + body: 'bui-CardBody', + footer: 'bui-CardFooter', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Card/index.ts b/packages/ui/src/components/Card/index.ts index 4b710ee7f1..dc7dbd4ead 100644 --- a/packages/ui/src/components/Card/index.ts +++ b/packages/ui/src/components/Card/index.ts @@ -15,6 +15,7 @@ */ export { Card, CardHeader, CardBody, CardFooter } from './Card'; +export { CardDefinition } from './definition'; export type { CardProps, diff --git a/packages/ui/src/components/Checkbox/Checkbox.module.css b/packages/ui/src/components/Checkbox/Checkbox.module.css index bc4f700bce..a4647c935a 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.module.css +++ b/packages/ui/src/components/Checkbox/Checkbox.module.css @@ -48,7 +48,30 @@ background-color: var(--bui-bg-surface-1); padding: 0; flex-shrink: 0; - color: var(--bui-fg-solid); + color: transparent; + + .bui-Checkbox[data-focus-visible] & { + transition: none; + outline: 2px solid var(--bui-ring); + outline-offset: 2px; + } + + .bui-Checkbox[data-selected] & { + background-color: var(--bui-bg-solid); + box-shadow: none; + color: var(--bui-fg-solid); + } + + .bui-Checkbox[data-indeterminate] & { + background-color: var(--bui-bg-surface-1); + box-shadow: inset 0 0 0 1px var(--bui-border); + color: var(--bui-fg-primary); + } + + .bui-Checkbox[data-hovered]:not([data-selected]):not([data-indeterminate]) + & { + box-shadow: inset 0 0 0 1px var(--bui-border-hover); + } @media (prefers-reduced-motion: reduce) { & { @@ -56,19 +79,4 @@ } } } - - .bui-Checkbox[data-focus-visible] .bui-CheckboxIndicator { - transition: none; - outline: 2px solid var(--bui-ring); - outline-offset: 2px; - } - - .bui-Checkbox[data-selected] .bui-CheckboxIndicator { - background-color: var(--bui-bg-solid); - box-shadow: none; - } - - .bui-Checkbox[data-hovered]:not([data-selected]) .bui-CheckboxIndicator { - box-shadow: inset 0 0 0 1px var(--bui-border-hover); - } } diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx index a41b7f2c8d..e8f7859785 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,35 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Checkbox } from './Checkbox'; import { Flex } from '../Flex'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Checkbox', component: Checkbox, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { children: 'Accept terms and conditions', }, -}; +}); -export const AllVariants: Story = { - ...Default, +export const Indeterminate = meta.story({ + args: { + children: 'Select all', + isIndeterminate: true, + }, +}); + +export const AllVariants = meta.story({ + ...Default.input, render: () => ( Unchecked Checked + Indeterminate Disabled Checked & Disabled + + Indeterminate & Disabled + ), -}; +}); diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index 49019370a4..6030fbc06b 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -18,14 +18,15 @@ import { forwardRef } from 'react'; import { Checkbox as RACheckbox } from 'react-aria-components'; import type { CheckboxProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { CheckboxDefinition } from './definition'; import clsx from 'clsx'; import styles from './Checkbox.module.css'; -import { RiCheckLine } from '@remixicon/react'; +import { RiCheckLine, RiSubtractLine } from '@remixicon/react'; /** @public */ export const Checkbox = forwardRef( (props, ref) => { - const { classNames } = useStyles('Checkbox'); + const { classNames } = useStyles(CheckboxDefinition); const { className, children, ...rest } = props; return ( @@ -34,12 +35,23 @@ export const Checkbox = forwardRef( className={clsx(classNames.root, styles[classNames.root], className)} {...rest} > -
    - -
    - {children} + {({ isIndeterminate }) => ( + <> +
    + {isIndeterminate ? ( + + ) : ( + + )} +
    + {children} + + )} ); }, diff --git a/packages/ui/src/components/Checkbox/definition.ts b/packages/ui/src/components/Checkbox/definition.ts new file mode 100644 index 0000000000..c7b5b16276 --- /dev/null +++ b/packages/ui/src/components/Checkbox/definition.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Checkbox + * @public + */ +export const CheckboxDefinition = { + classNames: { + root: 'bui-Checkbox', + indicator: 'bui-CheckboxIndicator', + }, + dataAttributes: { + selected: [true, false] as const, + indeterminate: [true, false] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Checkbox/index.ts b/packages/ui/src/components/Checkbox/index.ts index f6dfbf6beb..7c0cb5b414 100644 --- a/packages/ui/src/components/Checkbox/index.ts +++ b/packages/ui/src/components/Checkbox/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { Checkbox } from './Checkbox'; +export { CheckboxDefinition } from './definition'; export type { CheckboxProps } from './types'; diff --git a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx deleted file mode 100644 index 47b76f2f56..0000000000 --- a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 { Meta, StoryObj } from '@storybook/react-vite'; -import { Collapsible } from './Collapsible'; -import { Button } from '../Button'; -import { Box } from '../Box'; -import { Text } from '../Text'; -import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'; - -const meta = { - title: 'Backstage UI/Collapsible', - component: Collapsible.Root, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - style: { - display: 'flex', - flexDirection: 'column', - gap: 'var(--bui-space-2)', - alignItems: 'center', - }, - children: ( - <> - ( - - )} - /> - - - - It's the edge of the world and all of Western civilization - - - The sun may rise in the East, at least it settled in a final - location - - It's understood that Hollywood sells Californication - - - - ), - }, -}; - -export const Open: Story = { - args: { - ...Default.args, - defaultOpen: true, - }, -}; diff --git a/packages/ui/src/components/Collapsible/Collapsible.tsx b/packages/ui/src/components/Collapsible/Collapsible.tsx deleted file mode 100644 index c9417d2019..0000000000 --- a/packages/ui/src/components/Collapsible/Collapsible.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { forwardRef } from 'react'; -import { Collapsible as CollapsiblePrimitive } from '@base-ui-components/react/collapsible'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; -import styles from './Collapsible.module.css'; - -const CollapsibleRoot = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - const { classNames, cleanedProps } = useStyles('Collapsible', props); - - return ( - - ); -}); -CollapsibleRoot.displayName = CollapsiblePrimitive.Root.displayName; - -const CollapsibleTrigger = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - const { classNames, cleanedProps } = useStyles('Collapsible', props); - - return ( - - ); -}); -CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName; - -const CollapsiblePanel = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - const { classNames, cleanedProps } = useStyles('Collapsible', props); - - return ( - - ); -}); -CollapsiblePanel.displayName = CollapsiblePrimitive.Panel.displayName; - -/** - * Collapsible is a component that allows you to collapse and expand content. - * It is a wrapper around the CollapsiblePrimitive component from base-ui-components. - * - * @public - */ -export const Collapsible = { - Root: CollapsibleRoot, - Trigger: CollapsibleTrigger, - Panel: CollapsiblePanel, -}; diff --git a/packages/ui/src/components/Container/Container.stories.tsx b/packages/ui/src/components/Container/Container.stories.tsx index 504cb3de87..44ff6f2d17 100644 --- a/packages/ui/src/components/Container/Container.stories.tsx +++ b/packages/ui/src/components/Container/Container.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Box } from '../Box/Box'; import { Container } from './Container'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Container', component: Container, parameters: { @@ -32,10 +31,7 @@ const meta = { control: 'text', }, }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const DecorativeBox = () => ( ( /> ); -export const Default: Story = { +export const Default = meta.story({ args: { children: , }, -}; +}); -export const Preview: Story = { +export const Preview = meta.story({ render: () => (
    ), -}; +}); -export const WithPaddingY: Story = { +export const WithPaddingY = meta.story({ args: { - ...Default.args, + ...Default.input.args, py: '4', }, -}; +}); -export const WithPaddingTop: Story = { +export const WithPaddingTop = meta.story({ args: { - ...Default.args, + ...Default.input.args, pt: '4', }, -}; +}); -export const WithPaddingBottom: Story = { +export const WithPaddingBottom = meta.story({ args: { - ...Default.args, + ...Default.input.args, pb: '4', }, -}; +}); -export const WithMarginY: Story = { +export const WithMarginY = meta.story({ args: { - ...Default.args, + ...Default.input.args, my: '4', }, -}; +}); -export const WithMarginTop: Story = { +export const WithMarginTop = meta.story({ args: { - ...Default.args, + ...Default.input.args, mt: '4', }, -}; +}); -export const WithMarginBottom: Story = { +export const WithMarginBottom = meta.story({ args: { - ...Default.args, + ...Default.input.args, mb: '4', }, -}; +}); diff --git a/packages/ui/src/components/Container/Container.tsx b/packages/ui/src/components/Container/Container.tsx index 9b15cd162e..7e3627f0b9 100644 --- a/packages/ui/src/components/Container/Container.tsx +++ b/packages/ui/src/components/Container/Container.tsx @@ -18,13 +18,14 @@ import { forwardRef } from 'react'; import { ContainerProps } from './types'; import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; +import { ContainerDefinition } from './definition'; import styles from './Container.module.css'; /** @public */ export const Container = forwardRef( (props, ref) => { const { classNames, utilityClasses, style, cleanedProps } = useStyles( - 'Container', + ContainerDefinition, props, ); diff --git a/packages/ui/src/components/Container/definition.ts b/packages/ui/src/components/Container/definition.ts new file mode 100644 index 0000000000..330e6adf2f --- /dev/null +++ b/packages/ui/src/components/Container/definition.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Container + * @public + */ +export const ContainerDefinition = { + classNames: { + root: 'bui-Container', + }, + utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display'], +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Container/index.tsx b/packages/ui/src/components/Container/index.tsx index 81614c352d..73d1a0ab6e 100644 --- a/packages/ui/src/components/Container/index.tsx +++ b/packages/ui/src/components/Container/index.tsx @@ -14,4 +14,5 @@ * limitations under the License. */ export { Container } from './Container'; +export { ContainerDefinition } from './definition'; export type { ContainerProps } from './types'; diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index 22d96146ea..5101cbf67c 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -23,7 +23,7 @@ left: 0; width: 100%; height: 100%; - background: rgba(232, 232, 232, 0.8); + background: color-mix(in srgb, var(--bui-gray-2) 80%, transparent); display: flex; align-items: center; justify-content: center; diff --git a/packages/ui/src/components/Dialog/Dialog.stories.tsx b/packages/ui/src/components/Dialog/Dialog.stories.tsx index 98d2492913..0392959a59 100644 --- a/packages/ui/src/components/Dialog/Dialog.stories.tsx +++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; + +import preview from '../../../../../.storybook/preview'; import { Dialog, DialogTrigger, @@ -24,7 +25,7 @@ import { import { Button, Flex, Text, TextField, Select } from '@backstage/ui'; import { useArgs } from 'storybook/preview-api'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Dialog', component: Dialog, args: { @@ -35,12 +36,9 @@ const meta = { isOpen: { control: 'boolean' }, defaultOpen: { control: 'boolean' }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ render: args => { return ( @@ -62,17 +60,15 @@ export const Default: Story = { ); }, -}; +}); -export const Open: Story = { +export const Open = Default.extend({ args: { - ...Default.args, defaultOpen: true, }, - render: Default.render, -}; +}); -export const NoTrigger: Story = { +export const NoTrigger = meta.story({ args: { isOpen: true, }, @@ -100,9 +96,9 @@ export const NoTrigger: Story = { ); }, -}; +}); -export const FixedWidth: Story = { +export const FixedWidth = meta.story({ args: { defaultOpen: true, width: 600, @@ -145,35 +141,31 @@ export const FixedWidth: Story = { ), -}; +}); -export const FixedHeight: Story = { +export const FixedHeight = FixedWidth.extend({ args: { defaultOpen: true, + width: undefined, height: 500, }, - render: FixedWidth.render, -}; +}); -export const FixedWidthAndHeight: Story = { +export const FixedWidthAndHeight = FixedWidth.extend({ args: { - defaultOpen: true, - width: 600, height: 400, }, - render: FixedWidth.render, -}; +}); -export const FullWidthAndHeight: Story = { +export const FullWidthAndHeight = FixedWidth.extend({ args: { defaultOpen: true, width: '100%', height: '100%', }, - render: FixedWidth.render, -}; +}); -export const Confirmation: Story = { +export const Confirmation = meta.story({ args: { isOpen: true, }, @@ -199,9 +191,9 @@ export const Confirmation: Story = { ), -}; +}); -export const WithForm: Story = { +export const WithForm = meta.story({ args: { isOpen: true, }, @@ -235,20 +227,18 @@ export const WithForm: Story = { ), -}; +}); -export const PreviewFixedWidthAndHeight: Story = { +export const PreviewFixedWidthAndHeight = FixedWidth.extend({ args: { defaultOpen: undefined, width: 600, height: 400, }, - render: FixedWidth.render, -}; +}); -export const PreviewWithForm: Story = { +export const PreviewWithForm = WithForm.extend({ args: { - defaultOpen: undefined, + isOpen: undefined, }, - render: WithForm.render, -}; +}); diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx index 5dcd421b04..1c33588306 100644 --- a/packages/ui/src/components/Dialog/Dialog.tsx +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -31,6 +31,7 @@ import type { import { RiCloseLine } from '@remixicon/react'; import { Button } from '../Button'; import { useStyles } from '../../hooks/useStyles'; +import { DialogDefinition } from './definition'; import { Flex } from '../Flex'; import styles from './Dialog.module.css'; @@ -42,7 +43,7 @@ export const DialogTrigger = (props: DialogTriggerProps) => { /** @public */ export const Dialog = forwardRef, DialogProps>( (props, ref) => { - const { classNames, cleanedProps } = useStyles('Dialog', props); + const { classNames, cleanedProps } = useStyles(DialogDefinition, props); const { className, children, width, height, style, ...rest } = cleanedProps; return ( @@ -84,7 +85,7 @@ export const DialogHeader = forwardRef< React.ElementRef<'div'>, DialogHeaderProps >((props, ref) => { - const { classNames, cleanedProps } = useStyles('Dialog', props); + const { classNames, cleanedProps } = useStyles(DialogDefinition, props); const { className, children, ...rest } = cleanedProps; return ( @@ -110,7 +111,7 @@ DialogHeader.displayName = 'DialogHeader'; /** @public */ export const DialogBody = forwardRef, DialogBodyProps>( (props, ref) => { - const { classNames, cleanedProps } = useStyles('Dialog', props); + const { classNames, cleanedProps } = useStyles(DialogDefinition, props); const { className, children, ...rest } = cleanedProps; return ( @@ -132,7 +133,7 @@ export const DialogFooter = forwardRef< React.ElementRef<'div'>, React.ComponentPropsWithoutRef<'div'> >((props, ref) => { - const { classNames, cleanedProps } = useStyles('Dialog', props); + const { classNames, cleanedProps } = useStyles(DialogDefinition, props); const { className, children, ...rest } = cleanedProps; return ( diff --git a/packages/ui/src/components/Dialog/definition.ts b/packages/ui/src/components/Dialog/definition.ts new file mode 100644 index 0000000000..d2fcb7bb4d --- /dev/null +++ b/packages/ui/src/components/Dialog/definition.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Dialog + * @public + */ +export const DialogDefinition = { + classNames: { + overlay: 'bui-DialogOverlay', + dialog: 'bui-Dialog', + header: 'bui-DialogHeader', + headerTitle: 'bui-DialogHeaderTitle', + body: 'bui-DialogBody', + footer: 'bui-DialogFooter', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Dialog/index.ts b/packages/ui/src/components/Dialog/index.ts index 8db1b29ff8..57dc8aae4b 100644 --- a/packages/ui/src/components/Dialog/index.ts +++ b/packages/ui/src/components/Dialog/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export * from './Dialog'; +export { DialogDefinition } from './definition'; export * from './types'; diff --git a/packages/ui/src/components/FieldError/FieldError.stories.tsx b/packages/ui/src/components/FieldError/FieldError.stories.tsx index 1922e12a0d..a0b2a05b92 100644 --- a/packages/ui/src/components/FieldError/FieldError.stories.tsx +++ b/packages/ui/src/components/FieldError/FieldError.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { TextField, Input, Form } from 'react-aria-components'; import { FieldError } from './FieldError'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/FieldError', component: FieldError, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); // Show error with server validation using Form component -export const WithServerValidation: Story = { +export const WithServerValidation = meta.story({ render: () => (
    ), -}; +}); // Show error using children -export const WithCustomMessage: Story = { +export const WithCustomMessage = meta.story({ render: () => ( This is a custom error message. ), -}; +}); // Show error with render prop function -export const WithRenderProp: Story = { +export const WithRenderProp = meta.story({ render: () => ( ), -}; +}); diff --git a/packages/ui/src/components/FieldError/FieldError.tsx b/packages/ui/src/components/FieldError/FieldError.tsx index 4c4bfb9455..0016486fe7 100644 --- a/packages/ui/src/components/FieldError/FieldError.tsx +++ b/packages/ui/src/components/FieldError/FieldError.tsx @@ -22,11 +22,12 @@ import { import clsx from 'clsx'; import styles from './FieldError.module.css'; import { useStyles } from '../../hooks/useStyles'; +import { FieldErrorDefinition } from './definition'; /** @public */ export const FieldError = forwardRef( (props: FieldErrorProps, ref) => { - const { classNames, cleanedProps } = useStyles('FieldError', props); + const { classNames, cleanedProps } = useStyles(FieldErrorDefinition, props); const { className, ...rest } = cleanedProps; return ( diff --git a/packages/ui/src/components/FieldError/definition.ts b/packages/ui/src/components/FieldError/definition.ts new file mode 100644 index 0000000000..d7174dfc99 --- /dev/null +++ b/packages/ui/src/components/FieldError/definition.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for FieldError + * @public + */ +export const FieldErrorDefinition = { + classNames: { + root: 'bui-FieldError', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/FieldError/index.ts b/packages/ui/src/components/FieldError/index.ts index 5b61f83ace..1d3abf8398 100644 --- a/packages/ui/src/components/FieldError/index.ts +++ b/packages/ui/src/components/FieldError/index.ts @@ -15,3 +15,4 @@ */ export * from './FieldError'; +export { FieldErrorDefinition } from './definition'; diff --git a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx index d0e8f3e4fb..178df67284 100644 --- a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx +++ b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { FieldLabel } from './FieldLabel'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/FieldLabel', component: FieldLabel, argTypes: { @@ -31,35 +30,32 @@ const meta = { control: 'text', }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { label: 'Label', }, -}; +}); -export const WithSecondaryLabel: Story = { +export const WithSecondaryLabel = meta.story({ args: { - ...Default.args, + ...Default.input.args, secondaryLabel: 'Secondary Label', }, -}; +}); -export const WithDescription: Story = { +export const WithDescription = meta.story({ args: { - ...Default.args, + ...Default.input.args, description: 'Description', }, -}; +}); -export const WithAllFields: Story = { +export const WithAllFields = meta.story({ args: { - ...Default.args, + ...Default.input.args, secondaryLabel: 'Secondary Label', description: 'Description', }, -}; +}); diff --git a/packages/ui/src/components/FieldLabel/FieldLabel.tsx b/packages/ui/src/components/FieldLabel/FieldLabel.tsx index 5b27dd876f..3c685f9479 100644 --- a/packages/ui/src/components/FieldLabel/FieldLabel.tsx +++ b/packages/ui/src/components/FieldLabel/FieldLabel.tsx @@ -17,13 +17,14 @@ import { Label } from 'react-aria-components'; import { forwardRef } from 'react'; import type { FieldLabelProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { FieldLabelDefinition } from './definition'; import styles from './FieldLabel.module.css'; import clsx from 'clsx'; /** @public */ export const FieldLabel = forwardRef( (props: FieldLabelProps, ref) => { - const { classNames, cleanedProps } = useStyles('FieldLabel', props); + const { classNames, cleanedProps } = useStyles(FieldLabelDefinition, props); const { className, label, diff --git a/packages/ui/src/components/FieldLabel/definition.ts b/packages/ui/src/components/FieldLabel/definition.ts new file mode 100644 index 0000000000..d6833a9478 --- /dev/null +++ b/packages/ui/src/components/FieldLabel/definition.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for FieldLabel + * @public + */ +export const FieldLabelDefinition = { + classNames: { + root: 'bui-FieldLabelWrapper', + label: 'bui-FieldLabel', + secondaryLabel: 'bui-FieldSecondaryLabel', + description: 'bui-FieldDescription', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/FieldLabel/index.ts b/packages/ui/src/components/FieldLabel/index.ts index b9cebb1bf7..0b4a8adf5f 100644 --- a/packages/ui/src/components/FieldLabel/index.ts +++ b/packages/ui/src/components/FieldLabel/index.ts @@ -15,4 +15,5 @@ */ export * from './FieldLabel'; +export { FieldLabelDefinition } from './definition'; export * from './types'; diff --git a/packages/ui/src/components/Flex/Flex.module.css b/packages/ui/src/components/Flex/Flex.module.css index 0d44fbd12f..1ca3497cba 100644 --- a/packages/ui/src/components/Flex/Flex.module.css +++ b/packages/ui/src/components/Flex/Flex.module.css @@ -23,4 +23,32 @@ /* This helps when using `truncate` on text inside a flex container */ min-width: 0; } + + .bui-Flex[data-surface='0'] { + background-color: var(--bui-bg-surface-0); + } + + .bui-Flex[data-surface='1'] { + background-color: var(--bui-bg-surface-1); + } + + .bui-Flex[data-surface='2'] { + background-color: var(--bui-bg-surface-2); + } + + .bui-Flex[data-surface='3'] { + background-color: var(--bui-bg-surface-3); + } + + .bui-Flex[data-surface='danger'] { + background-color: var(--bui-bg-danger); + } + + .bui-Flex[data-surface='warning'] { + background-color: var(--bui-bg-warning); + } + + .bui-Flex[data-surface='success'] { + background-color: var(--bui-bg-success); + } } diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 87854e89c7..998d97bc0a 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Flex } from './Flex'; import { Text } from '../Text'; import { Box } from '../Box'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Flex', component: Flex, argTypes: { @@ -36,10 +35,7 @@ const meta = { options: ['row', 'column', 'row-reverse', 'column-reverse'], }, }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const DecorativeBox = ({ width = '48px', @@ -78,7 +74,7 @@ const DecorativeBox = ({ ); }; -export const Default: Story = { +export const Default = meta.story({ args: { children: ( <> @@ -88,23 +84,23 @@ export const Default: Story = { ), }, -}; +}); -export const ColumnDirection: Story = { +export const ColumnDirection = meta.story({ args: { - ...Default.args, + ...Default.input.args, direction: 'column', }, -}; +}); -export const RowDirection: Story = { +export const RowDirection = meta.story({ args: { - ...Default.args, + ...Default.input.args, direction: 'row', }, -}; +}); -export const AlignStartInColumn: Story = { +export const AlignStartInColumn = meta.story({ args: { align: 'start', direction: 'column', @@ -116,9 +112,9 @@ export const AlignStartInColumn: Story = { ), -}; +}); -export const AlignStartInRow: Story = { +export const AlignStartInRow = meta.story({ args: { align: 'start', direction: 'row', @@ -130,9 +126,9 @@ export const AlignStartInRow: Story = { ), -}; +}); -export const AlignCenterInColumn: Story = { +export const AlignCenterInColumn = meta.story({ args: { align: 'center', direction: 'column', @@ -144,9 +140,9 @@ export const AlignCenterInColumn: Story = { ), -}; +}); -export const AlignCenterInRow: Story = { +export const AlignCenterInRow = meta.story({ args: { align: 'center', direction: 'row', @@ -158,9 +154,9 @@ export const AlignCenterInRow: Story = { ), -}; +}); -export const AlignEndInColumn: Story = { +export const AlignEndInColumn = meta.story({ args: { align: 'end', direction: 'column', @@ -172,9 +168,9 @@ export const AlignEndInColumn: Story = { ), -}; +}); -export const AlignEndInRow: Story = { +export const AlignEndInRow = meta.story({ args: { align: 'end', direction: 'row', @@ -186,9 +182,9 @@ export const AlignEndInRow: Story = { ), -}; +}); -export const ResponsiveAlign: Story = { +export const ResponsiveAlign = meta.story({ args: { align: { xs: 'start', md: 'center', lg: 'end' }, }, @@ -199,9 +195,9 @@ export const ResponsiveAlign: Story = { ), -}; +}); -export const ResponsiveGap: Story = { +export const ResponsiveGap = meta.story({ args: { gap: { xs: '4', md: '8', lg: '12' }, }, @@ -212,9 +208,9 @@ export const ResponsiveGap: Story = { ), -}; +}); -export const LargeGap: Story = { +export const LargeGap = meta.story({ args: { gap: '8', }, @@ -225,9 +221,9 @@ export const LargeGap: Story = { ), -}; +}); -export const WithTextTruncate: Story = { +export const WithTextTruncate = meta.story({ render: () => ( @@ -246,4 +242,67 @@ export const WithTextTruncate: Story = { ), -}; +}); + +export const Surfaces = meta.story({ + args: { + px: '6', + py: '4', + }, + render: args => ( + + Default + + Surface 0 + + + Surface 1 + + + Surface 2 + + + Surface 3 + + + Responsive Surface + + + Surface Danger + + + Surface Warning + + + Surface Success + + + ), +}); + +export const SurfacesAutoIncrement = meta.story({ + args: { px: '6', py: '4', gap: '4' }, + render: args => ( + +
    + Using surface="auto" automatically increments from the parent surface. + This allows components to be reusable without hardcoding surface levels. +
    + +
    Surface 0 (explicit)
    + +
    Surface auto (becomes 1)
    + +
    Surface auto (becomes 2)
    + +
    Surface auto (becomes 3)
    + +
    Surface auto (stays 3 - capped)
    +
    +
    +
    +
    +
    +
    + ), +}); diff --git a/packages/ui/src/components/Flex/Flex.tsx b/packages/ui/src/components/Flex/Flex.tsx index 27564539bd..3d29408b2b 100644 --- a/packages/ui/src/components/Flex/Flex.tsx +++ b/packages/ui/src/components/Flex/Flex.tsx @@ -18,18 +18,28 @@ import { forwardRef } from 'react'; import { FlexProps } from './types'; import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; +import { FlexDefinition } from './definition'; import styles from './Flex.module.css'; +import { SurfaceProvider, useSurface } from '../../hooks/useSurface'; /** @public */ export const Flex = forwardRef((props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( - 'Flex', - { gap: '4', ...props }, - ); + // Resolve the surface this Flex creates for its children + // Using 'surface' parameter = container behavior (auto increments) + const { surface: resolvedSurface } = useSurface({ + surface: props.surface, + }); - const { className, ...rest } = cleanedProps; + const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = + useStyles(FlexDefinition, { + gap: '4', + ...props, + surface: resolvedSurface, // Use resolved surface for data attribute + }); - return ( + const { className, surface, ...rest } = cleanedProps; + + const content = (
    ((props, ref) => { className, )} style={style} + {...dataAttributes} {...rest} /> ); + + return resolvedSurface ? ( + {content} + ) : ( + content + ); }); diff --git a/packages/ui/src/components/Flex/definition.ts b/packages/ui/src/components/Flex/definition.ts new file mode 100644 index 0000000000..3f747e153b --- /dev/null +++ b/packages/ui/src/components/Flex/definition.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Flex + * @public + */ +export const FlexDefinition = { + classNames: { + root: 'bui-Flex', + }, + utilityProps: [ + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + 'gap', + 'align', + 'justify', + 'direction', + ], + dataAttributes: { + surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Flex/index.ts b/packages/ui/src/components/Flex/index.ts index 4e4c883119..c9c5890ad9 100644 --- a/packages/ui/src/components/Flex/index.ts +++ b/packages/ui/src/components/Flex/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { Flex } from './Flex'; +export { FlexDefinition } from './definition'; export type { FlexProps } from './types'; diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index 7699e0ddc5..33bc476f93 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Responsive, Space, SpaceProps } from '../../types'; +import type { Responsive, Space, SpaceProps, Surface } from '../../types'; /** @public */ export interface FlexProps extends SpaceProps { @@ -25,4 +25,5 @@ export interface FlexProps extends SpaceProps { direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; className?: string; style?: React.CSSProperties; + surface?: Responsive; } diff --git a/packages/ui/src/components/Grid/Grid.module.css b/packages/ui/src/components/Grid/Grid.module.css index ff98e9337f..1861e38101 100644 --- a/packages/ui/src/components/Grid/Grid.module.css +++ b/packages/ui/src/components/Grid/Grid.module.css @@ -20,4 +20,39 @@ .bui-Grid { display: grid; } + + .bui-Grid[data-surface='0'], + .bui-GridItem[data-surface='0'] { + background-color: var(--bui-bg-surface-0); + } + + .bui-Grid[data-surface='1'], + .bui-GridItem[data-surface='1'] { + background-color: var(--bui-bg-surface-1); + } + + .bui-Grid[data-surface='2'], + .bui-GridItem[data-surface='2'] { + background-color: var(--bui-bg-surface-2); + } + + .bui-Grid[data-surface='3'], + .bui-GridItem[data-surface='3'] { + background-color: var(--bui-bg-surface-3); + } + + .bui-Grid[data-surface='danger'], + .bui-GridItem[data-surface='danger'] { + background-color: var(--bui-bg-danger); + } + + .bui-Grid[data-surface='warning'], + .bui-GridItem[data-surface='warning'] { + background-color: var(--bui-bg-warning); + } + + .bui-Grid[data-surface='success'], + .bui-GridItem[data-surface='success'] { + background-color: var(--bui-bg-success); + } } diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 66ea346b4f..b3524444fd 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { Grid } from './Grid'; import type { GridItemProps } from './types'; import { Box } from '../Box/Box'; import { Flex } from '../Flex'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Grid', component: Grid.Root, -} satisfies Meta; - -export default meta; -type Story = StoryObj; + // We will add this story in the manifest when the component is not composed. + tags: ['!manifest'], +}); const FakeBox = () => ( ( /> ); -export const Default: Story = { +export const Default = meta.story({ args: { children: ( <> @@ -51,16 +49,16 @@ export const Default: Story = { ), }, -}; +}); -export const LargeGap: Story = { +export const LargeGap = meta.story({ args: { - ...Default.args, + ...Default.input.args, gap: '64px', }, -}; +}); -export const ColumnSizes: Story = { +export const ColumnSizes = meta.story({ args: { columns: '12', }, @@ -78,9 +76,9 @@ export const ColumnSizes: Story = { ))} ), -}; +}); -export const RowAndColumns: Story = { +export const RowAndColumns = meta.story({ args: { columns: '12', }, @@ -106,4 +104,107 @@ export const RowAndColumns: Story = { ), -}; +}); + +export const Surfaces = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + + + Surface 0 + + + Surface 1 + + + Surface 2 + + + Surface 3 + + + Responsive Surface + + + Surface Danger + + + Surface Warning + + + Surface Success + + + + + + Surface 0 + + + + + Surface 1 + + + + + Surface 2 + + + + + Surface 3 + + + + + Responsive Surface + + + + + Surface Danger + + + + + Surface Warning + + + + + Surface Success + + + + + ), +}); + +export const SurfacesAutoIncrement = meta.story({ + args: { px: '6', py: '4', columns: '2', gap: '4' }, + render: args => ( + +
    + Using surface="auto" automatically increments from the parent surface. + Each Grid.Item with auto will be one level above its Grid.Root parent. +
    + + Surface 0 (Grid.Root) + Surface auto (becomes 1) + + + Nested: Surface auto (becomes 1) + + Nested: Surface auto (becomes 2) + + + + +
    + ), +}); diff --git a/packages/ui/src/components/Grid/Grid.tsx b/packages/ui/src/components/Grid/Grid.tsx index 61cd2fb7c7..52dd22167e 100644 --- a/packages/ui/src/components/Grid/Grid.tsx +++ b/packages/ui/src/components/Grid/Grid.tsx @@ -18,17 +18,28 @@ import { forwardRef } from 'react'; import clsx from 'clsx'; import type { GridItemProps, GridProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { GridDefinition, GridItemDefinition } from './definition'; import styles from './Grid.module.css'; +import { SurfaceProvider, useSurface } from '../../hooks/useSurface'; const GridRoot = forwardRef((props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( - 'Grid', - { columns: 'auto', gap: '4', ...props }, - ); + // Resolve the surface this Grid creates for its children + // Using 'surface' parameter = container behavior (auto increments) + const { surface: resolvedSurface } = useSurface({ + surface: props.surface, + }); - const { className, ...rest } = cleanedProps; + const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = + useStyles(GridDefinition, { + columns: 'auto', + gap: '4', + ...props, + surface: resolvedSurface, // Use resolved surface for data attribute + }); - return ( + const { className, surface, ...rest } = cleanedProps; + + const content = (
    ((props, ref) => { className, )} style={style} + {...dataAttributes} {...rest} /> ); + + return resolvedSurface ? ( + {content} + ) : ( + content + ); }); const GridItem = forwardRef((props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( - 'GridItem', - props, - ); + // Resolve the surface this GridItem creates for its children + // Using 'surface' parameter = container behavior (auto increments) + const { surface: resolvedSurface } = useSurface({ + surface: props.surface, + }); - const { className, ...rest } = cleanedProps; + const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = + useStyles(GridItemDefinition, { + ...props, + surface: resolvedSurface, // Use resolved surface for data attribute + }); - return ( + const { className, surface, ...rest } = cleanedProps; + + const content = (
    ((props, ref) => { className, )} style={style} + {...dataAttributes} {...rest} /> ); + + return resolvedSurface ? ( + {content} + ) : ( + content + ); }); /** @public */ diff --git a/packages/ui/src/components/Grid/definition.ts b/packages/ui/src/components/Grid/definition.ts new file mode 100644 index 0000000000..a6c5c582c7 --- /dev/null +++ b/packages/ui/src/components/Grid/definition.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Grid + * @public + */ +export const GridDefinition = { + classNames: { + root: 'bui-Grid', + }, + utilityProps: [ + 'columns', + 'gap', + 'm', + 'mb', + 'ml', + 'mr', + 'mt', + 'mx', + 'my', + 'p', + 'pb', + 'pl', + 'pr', + 'pt', + 'px', + 'py', + ], + dataAttributes: { + surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const, + }, +} as const satisfies ComponentDefinition; + +/** + * Component definition for GridItem + * @public + */ +export const GridItemDefinition = { + classNames: { + root: 'bui-GridItem', + }, + utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan'], + dataAttributes: { + surface: ['0', '1', '2', '3', 'danger', 'warning', 'success'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Grid/index.ts b/packages/ui/src/components/Grid/index.ts index 870ab55856..aa95e3370e 100644 --- a/packages/ui/src/components/Grid/index.ts +++ b/packages/ui/src/components/Grid/index.ts @@ -15,4 +15,5 @@ */ export { Grid } from './Grid'; +export { GridDefinition, GridItemDefinition } from './definition'; export type { GridProps, GridItemProps } from './types'; diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index d9d18b4718..f5ce9ca9cb 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import type { Space, SpaceProps, Responsive, Columns } from '../../types'; +import type { + Space, + SpaceProps, + Responsive, + Columns, + Surface, +} from '../../types'; /** @public */ export interface GridProps extends SpaceProps { @@ -23,6 +29,7 @@ export interface GridProps extends SpaceProps { columns?: Responsive; gap?: Responsive; style?: React.CSSProperties; + surface?: Responsive; } /** @public */ @@ -34,4 +41,5 @@ export interface GridItemProps { colStart?: Responsive; rowSpan?: Responsive; style?: React.CSSProperties; + surface?: Responsive; } diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index ad50c42e0c..031cfd4d16 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -27,7 +27,7 @@ left: 0px; right: 0px; height: 16px; - background-color: var(--bui-bg); + background-color: var(--bui-bg-surface-0); z-index: 0; } diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx index 6ccda8bb50..ba2c5b9abf 100644 --- a/packages/ui/src/components/Header/Header.stories.tsx +++ b/packages/ui/src/components/Header/Header.stories.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; +import type { StoryFn } from '@storybook/react-vite'; import { Header } from './Header'; import type { HeaderTab } from './types'; import { @@ -36,16 +37,13 @@ import { } from '@remixicon/react'; import { HeaderPageBreadcrumb } from '../HeaderPage/types'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Header', component: Header, parameters: { layout: 'fullscreen', }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const withRouter = (Story: StoryFn) => ( @@ -199,19 +197,19 @@ const layoutDecorator = [ withRouter, ]; -export const Default: Story = { +export const Default = meta.story({ args: {}, decorators: [withRouter], -}; +}); -export const WithTabs: Story = { +export const WithTabs = meta.story({ args: { tabs, }, decorators: [withRouter], -}; +}); -export const WithCustomActions: Story = { +export const WithCustomActions = meta.story({ args: {}, decorators: [withRouter], render: args => ( @@ -240,20 +238,17 @@ export const WithCustomActions: Story = { } /> ), -}; +}); -export const WithAllOptionsAndTabs: Story = { +export const WithAllOptionsAndTabs = WithCustomActions.extend({ args: { - ...WithCustomActions.args, tabs, }, - decorators: [withRouter], - render: WithCustomActions.render, -}; +}); -export const WithHeaderPage: Story = { +export const WithHeaderPage = meta.story({ args: { - ...WithAllOptionsAndTabs.args, + ...WithAllOptionsAndTabs.input.args, }, decorators: [withRouter], render: args => ( @@ -276,9 +271,9 @@ export const WithHeaderPage: Story = { /> ), -}; +}); -export const WithLayout: Story = { +export const WithLayout = meta.story({ decorators: layoutDecorator, render: args => ( <> @@ -291,9 +286,9 @@ export const WithLayout: Story = { /> ), -}; +}); -export const WithLayoutNoTabs: Story = { +export const WithLayoutNoTabs = meta.story({ decorators: layoutDecorator, render: args => ( <> @@ -301,9 +296,9 @@ export const WithLayoutNoTabs: Story = { ), -}; +}); -export const WithEverything: Story = { +export const WithEverything = meta.story({ args: { tabs, titleLink: '/', @@ -333,9 +328,9 @@ export const WithEverything: Story = { /> ), -}; +}); -export const WithMockedURLCampaigns: Story = { +export const WithMockedURLCampaigns = meta.story({ args: { tabs, }, @@ -353,9 +348,9 @@ export const WithMockedURLCampaigns: Story = { ), -}; +}); -export const WithMockedURLIntegrations: Story = { +export const WithMockedURLIntegrations = meta.story({ args: { tabs, }, @@ -373,9 +368,9 @@ export const WithMockedURLIntegrations: Story = { ), -}; +}); -export const WithMockedURLNoMatch: Story = { +export const WithMockedURLNoMatch = meta.story({ args: { tabs, }, @@ -397,9 +392,9 @@ export const WithMockedURLNoMatch: Story = { ), -}; +}); -export const WithTabsMatchingStrategies: Story = { +export const WithTabsMatchingStrategies = meta.story({ args: { title: 'Route Matching Demo', tabs: [ @@ -457,9 +452,9 @@ export const WithTabsMatchingStrategies: Story = { ), -}; +}); -export const WithTabsExactMatching: Story = { +export const WithTabsExactMatching = meta.story({ args: { title: 'Exact Matching Demo', tabs: [ @@ -496,9 +491,9 @@ export const WithTabsExactMatching: Story = { ), -}; +}); -export const WithTabsPrefixMatchingDeep: Story = { +export const WithTabsPrefixMatchingDeep = meta.story({ args: { title: 'Deep Nesting Demo', tabs: [ @@ -548,4 +543,4 @@ export const WithTabsPrefixMatchingDeep: Story = { ), -}; +}); diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 6f42820be4..56ec200e29 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -18,6 +18,7 @@ import type { HeaderProps } from './types'; import { HeaderToolbar } from './HeaderToolbar'; import { Tabs, TabList, Tab } from '../Tabs'; import { useStyles } from '../../hooks/useStyles'; +import { HeaderDefinition } from './definition'; import { type NavigateOptions } from 'react-router-dom'; import styles from './Header.module.css'; import clsx from 'clsx'; @@ -34,7 +35,7 @@ declare module 'react-aria-components' { * @public */ export const Header = (props: HeaderProps) => { - const { classNames, cleanedProps } = useStyles('Header', props); + const { classNames, cleanedProps } = useStyles(HeaderDefinition, props); const { className, tabs, diff --git a/packages/ui/src/components/Header/HeaderToolbar.tsx b/packages/ui/src/components/Header/HeaderToolbar.tsx index a1754c61f4..6ba03ab3a6 100644 --- a/packages/ui/src/components/Header/HeaderToolbar.tsx +++ b/packages/ui/src/components/Header/HeaderToolbar.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Link, RouterProvider } from 'react-aria-components'; +import { Link } from 'react-aria-components'; import { useStyles } from '../../hooks/useStyles'; +import { HeaderDefinition } from './definition'; import { useRef } from 'react'; import { RiShapesLine } from '@remixicon/react'; import type { HeaderToolbarProps } from './types'; import { Text } from '../Text'; -import { useNavigate, useHref } from 'react-router-dom'; import styles from './Header.module.css'; import clsx from 'clsx'; @@ -30,10 +30,9 @@ import clsx from 'clsx'; * @internal */ export const HeaderToolbar = (props: HeaderToolbarProps) => { - const { classNames, cleanedProps } = useStyles('Header', props); + const { classNames, cleanedProps } = useStyles(HeaderDefinition, props); const { className, icon, title, titleLink, customActions, hasTabs } = cleanedProps; - let navigate = useNavigate(); // Refs for collision detection const toolbarWrapperRef = useRef(null); @@ -52,63 +51,61 @@ export const HeaderToolbar = (props: HeaderToolbarProps) => { ); return ( - +
    -
    + {titleLink ? ( + + {titleContent} + + ) : ( +
    + {titleContent} +
    )} - ref={toolbarContentRef} - > - - {titleLink ? ( - - {titleContent} - - ) : ( -
    - {titleContent} -
    - )} -
    -
    -
    - {customActions} -
    + +
    +
    + {customActions}
    - +
    ); }; diff --git a/packages/ui/src/components/Header/definition.ts b/packages/ui/src/components/Header/definition.ts new file mode 100644 index 0000000000..5969d7d8fa --- /dev/null +++ b/packages/ui/src/components/Header/definition.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Header + * @public + */ +export const HeaderDefinition = { + classNames: { + toolbar: 'bui-HeaderToolbar', + toolbarWrapper: 'bui-HeaderToolbarWrapper', + toolbarContent: 'bui-HeaderToolbarContent', + toolbarControls: 'bui-HeaderToolbarControls', + toolbarIcon: 'bui-HeaderToolbarIcon', + toolbarName: 'bui-HeaderToolbarName', + tabsWrapper: 'bui-HeaderTabsWrapper', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Header/index.tsx b/packages/ui/src/components/Header/index.tsx index b0ae2821b2..60685cfaa6 100644 --- a/packages/ui/src/components/Header/index.tsx +++ b/packages/ui/src/components/Header/index.tsx @@ -15,4 +15,5 @@ */ export { Header } from './Header'; +export { HeaderDefinition } from './definition'; export type { HeaderProps, HeaderTab } from './types'; diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index ab527bcda4..0631a6111d 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -1,3 +1,4 @@ +import preview from '../../../../../.storybook/preview'; /* * Copyright 2025 The Backstage Authors * @@ -14,7 +15,7 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; +import type { StoryFn } from '@storybook/react-vite'; import { HeaderPage } from './HeaderPage'; import type { HeaderTab } from '../Header/types'; import { MemoryRouter } from 'react-router-dom'; @@ -29,16 +30,13 @@ import { } from '../../'; import { RiMore2Line } from '@remixicon/react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/HeaderPage', component: HeaderPage, parameters: { layout: 'fullscreen', }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const tabs: HeaderTab[] = [ { @@ -130,25 +128,25 @@ const layoutDecorator = [ ), ]; -export const Default: Story = { +export const Default = meta.story({ args: { title: 'Page Title', }, -}; +}); -export const WithTabs: Story = { +export const WithTabs = meta.story({ args: { - ...Default.args, + ...Default.input.args, tabs, }, decorators: [withRouter], -}; +}); -export const WithCustomActions: Story = { +export const WithCustomActions = meta.story({ decorators: [withRouter], render: () => ( @@ -174,48 +172,44 @@ export const WithCustomActions: Story = { } /> ), -}; +}); -export const WithBreadcrumbs: Story = { +export const WithBreadcrumbs = meta.story({ decorators: [withRouter], args: { - ...Default.args, + ...Default.input.args, breadcrumbs: [{ label: 'Home', href: '/' }], }, -}; +}); -export const WithLongBreadcrumbs: Story = { +export const WithLongBreadcrumbs = meta.story({ decorators: [withRouter], args: { - ...Default.args, + ...Default.input.args, breadcrumbs: [ { label: 'Home', href: '/' }, { label: 'Long Breadcrumb Name', href: '/long-breadcrumb' }, ], }, -}; +}); -export const WithEverything: Story = { +export const WithEverything = meta.story({ decorators: [withRouter], render: () => ( Custom action} breadcrumbs={[{ label: 'Home', href: '/' }]} /> ), -}; +}); -export const WithLayout: Story = { - args: { - ...WithEverything.args, - }, - decorators: [withRouter, ...layoutDecorator], - render: WithEverything.render, -}; +export const WithLayout = WithEverything.extend({ + decorators: [...layoutDecorator], +}); -export const WithTabsMatchingStrategies: Story = { +export const WithTabsMatchingStrategies = meta.story({ args: { title: 'Route Matching Demo', tabs: [ @@ -273,9 +267,9 @@ export const WithTabsMatchingStrategies: Story = { ), -}; +}); -export const WithTabsExactMatching: Story = { +export const WithTabsExactMatching = meta.story({ args: { title: 'Exact Matching Demo', tabs: [ @@ -312,9 +306,9 @@ export const WithTabsExactMatching: Story = { ), -}; +}); -export const WithTabsPrefixMatchingDeep: Story = { +export const WithTabsPrefixMatchingDeep = meta.story({ args: { title: 'Deep Nesting Demo', tabs: [ @@ -364,4 +358,4 @@ export const WithTabsPrefixMatchingDeep: Story = { ), -}; +}); diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.tsx index 6fcbbe258b..efa7ea8d58 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.tsx @@ -19,6 +19,7 @@ import { Text } from '../Text'; import { RiArrowRightSLine } from '@remixicon/react'; import { Tabs, TabList, Tab } from '../Tabs'; import { useStyles } from '../../hooks/useStyles'; +import { HeaderPageDefinition } from './definition'; import { Container } from '../Container'; import { Link } from '../Link'; import { Fragment } from 'react/jsx-runtime'; @@ -31,7 +32,7 @@ import clsx from 'clsx'; * @public */ export const HeaderPage = (props: HeaderPageProps) => { - const { classNames, cleanedProps } = useStyles('HeaderPage', props); + const { classNames, cleanedProps } = useStyles(HeaderPageDefinition, props); const { className, title, tabs, customActions, breadcrumbs } = cleanedProps; return ( diff --git a/packages/ui/src/components/HeaderPage/definition.ts b/packages/ui/src/components/HeaderPage/definition.ts new file mode 100644 index 0000000000..be93aaae6a --- /dev/null +++ b/packages/ui/src/components/HeaderPage/definition.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for HeaderPage + * @public + */ +export const HeaderPageDefinition = { + classNames: { + root: 'bui-HeaderPage', + content: 'bui-HeaderPageContent', + breadcrumbs: 'bui-HeaderPageBreadcrumbs', + tabsWrapper: 'bui-HeaderPageTabsWrapper', + controls: 'bui-HeaderPageControls', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/HeaderPage/index.tsx b/packages/ui/src/components/HeaderPage/index.tsx index 040d226a1f..7b277149f1 100644 --- a/packages/ui/src/components/HeaderPage/index.tsx +++ b/packages/ui/src/components/HeaderPage/index.tsx @@ -15,4 +15,5 @@ */ export { HeaderPage } from './HeaderPage'; +export { HeaderPageDefinition } from './definition'; export type { HeaderPageProps, HeaderPageBreadcrumb } from './types'; diff --git a/packages/ui/src/components/Link/Link.module.css b/packages/ui/src/components/Link/Link.module.css index 5d017e9681..a319dfb66b 100644 --- a/packages/ui/src/components/Link/Link.module.css +++ b/packages/ui/src/components/Link/Link.module.css @@ -33,4 +33,78 @@ text-decoration-color: color-mix(in srgb, currentColor 30%, transparent); } } + + .bui-Link[data-variant='title-large'] { + font-size: var(--bui-font-size-8); + line-height: 140%; + } + + .bui-Link[data-variant='title-medium'] { + font-size: var(--bui-font-size-7); + line-height: 140%; + } + + .bui-Link[data-variant='title-small'] { + font-size: var(--bui-font-size-6); + line-height: 140%; + } + + .bui-Link[data-variant='title-x-small'] { + font-size: var(--bui-font-size-5); + line-height: 140%; + } + + .bui-Link[data-variant='body-large'] { + font-size: var(--bui-font-size-4); + line-height: 140%; + } + + .bui-Link[data-variant='body-medium'] { + font-size: var(--bui-font-size-3); + line-height: 140%; + } + + .bui-Link[data-variant='body-small'] { + font-size: var(--bui-font-size-2); + line-height: 140%; + } + + .bui-Link[data-variant='body-x-small'] { + font-size: var(--bui-font-size-1); + line-height: 140%; + } + + .bui-Link[data-weight='regular'] { + font-weight: var(--bui-font-weight-regular); + } + + .bui-Link[data-weight='bold'] { + font-weight: var(--bui-font-weight-bold); + } + + .bui-Link[data-color='primary'] { + color: var(--bui-fg-primary); + } + + .bui-Link[data-color='secondary'] { + color: var(--bui-fg-secondary); + } + + .bui-Link[data-color='danger'] { + color: var(--bui-fg-danger); + } + + .bui-Link[data-color='warning'] { + color: var(--bui-fg-warning); + } + + .bui-Link[data-color='success'] { + color: var(--bui-fg-success); + } + + .bui-Link[data-truncate] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } diff --git a/packages/ui/src/components/Link/Link.stories.tsx b/packages/ui/src/components/Link/Link.stories.tsx index 37f142c4dc..7d680e5616 100644 --- a/packages/ui/src/components/Link/Link.stories.tsx +++ b/packages/ui/src/components/Link/Link.stories.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; +import type { StoryFn } from '@storybook/react-vite'; import { Link } from './Link'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { MemoryRouter } from 'react-router-dom'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Link', component: Link, args: { @@ -33,29 +34,26 @@ const meta = { ), ], -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { href: '/', children: 'Sign up for Backstage', }, -}; +}); -export const ExternalLink: Story = { +export const ExternalLink = meta.story({ args: { href: 'https://backstage.io', children: 'Sign up for Backstage', target: '_blank', }, -}; +}); -export const AllVariants: Story = { +export const AllVariants = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( @@ -69,9 +67,9 @@ export const AllVariants: Story = { ), -}; +}); -export const AllColors: Story = { +export const AllColors = meta.story({ render: () => ( ), -}; +}); -export const AllWeights: Story = { +export const AllWeights = meta.story({ render: () => ( @@ -225,9 +223,9 @@ export const AllWeights: Story = { ), -}; +}); -export const Truncate: Story = { +export const Truncate = meta.story({ args: { children: "A man looks at a painting in a museum and says, “Brothers and sisters I have none, but that man's father is my father's son.” Who is in the painting?", @@ -235,21 +233,21 @@ export const Truncate: Story = { truncate: true, style: { width: '480px' }, }, -}; +}); -export const Responsive: Story = { +export const Responsive = meta.story({ args: { - ...Default.args, + ...Default.input.args, variant: { xs: 'title-x-small', md: 'body-x-small', }, }, -}; +}); -export const Playground: Story = { +export const Playground = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( @@ -269,4 +267,4 @@ export const Playground: Story = { ), -}; +}); diff --git a/packages/ui/src/components/Link/Link.tsx b/packages/ui/src/components/Link/Link.tsx index 478b6cc67b..9dd9926f97 100644 --- a/packages/ui/src/components/Link/Link.tsx +++ b/packages/ui/src/components/Link/Link.tsx @@ -14,72 +14,87 @@ * limitations under the License. */ -import { forwardRef } from 'react'; -import { Link as AriaLink, RouterProvider } from 'react-aria-components'; +import { forwardRef, useRef } from 'react'; +import { useLink } from 'react-aria'; +import { RouterProvider } from 'react-aria-components'; import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; +import { LinkDefinition } from './definition'; import type { LinkProps } from './types'; import { useNavigate, useHref } from 'react-router-dom'; import { isExternalLink } from '../../utils/isExternalLink'; -import stylesLink from './Link.module.css'; -import stylesText from '../Text/Text.module.css'; +import styles from './Link.module.css'; + +const LinkInternal = forwardRef((props, ref) => { + const { classNames, dataAttributes, cleanedProps } = useStyles( + LinkDefinition, + { + variant: 'body', + weight: 'regular', + color: 'primary', + ...props, + }, + ); + + const { + className, + href, + title, + children, + onPress, + variant, + weight, + color, + truncate, + slot, + ...restProps + } = cleanedProps; + + const internalRef = useRef(null); + const linkRef = (ref || internalRef) as React.RefObject; + + // Use useLink hook to get link props + // For internal links, this will use the RouterProvider's navigate function + const { linkProps } = useLink( + { + href, + onPress, + ...restProps, + }, + linkRef, + ); + + return ( + )} + ref={linkRef} + href={href} + title={title} + className={clsx(classNames.root, styles[classNames.root], className)} + > + {children} + + ); +}); + +LinkInternal.displayName = 'LinkInternal'; /** @public */ export const Link = forwardRef((props, ref) => { const navigate = useNavigate(); - const { classNames: classNamesLink } = useStyles('Link', props); - const { - classNames: classNamesText, - dataAttributes: textDataAttributes, - cleanedProps, - } = useStyles('Text', { - variant: 'body', - weight: 'regular', - color: 'primary', - ...props, - }); - const { className, variant, weight, color, truncate, href, ...restProps } = - cleanedProps; + const isExternal = isExternalLink(props.href); - const isExternal = isExternalLink(href); - - // If it's an external link, render AriaLink without RouterProvider + // If it's an external link, render without RouterProvider if (isExternal) { - return ( - - ); + return ; } - // For internal links, use RouterProvider + // For internal links, wrap in RouterProvider so useLink can access the router return ( - + ); }); diff --git a/packages/ui/src/components/Link/definition.ts b/packages/ui/src/components/Link/definition.ts new file mode 100644 index 0000000000..cece929f11 --- /dev/null +++ b/packages/ui/src/components/Link/definition.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Link + * @public + */ +export const LinkDefinition = { + classNames: { + root: 'bui-Link', + }, + dataAttributes: { + variant: ['subtitle', 'body', 'caption', 'label'] as const, + weight: ['regular', 'bold'] as const, + color: ['primary', 'secondary', 'danger', 'warning', 'success'] as const, + truncate: [true, false] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Link/index.ts b/packages/ui/src/components/Link/index.ts index 1b38e89d62..7c17668029 100644 --- a/packages/ui/src/components/Link/index.ts +++ b/packages/ui/src/components/Link/index.ts @@ -15,4 +15,5 @@ */ export { Link } from './Link'; +export { LinkDefinition } from './definition'; export type { LinkProps } from './types'; diff --git a/packages/ui/src/components/Link/types.ts b/packages/ui/src/components/Link/types.ts index 33c33b26c0..796676e593 100644 --- a/packages/ui/src/components/Link/types.ts +++ b/packages/ui/src/components/Link/types.ts @@ -22,6 +22,7 @@ import type { TextWeights, } from '../../types'; import type { LinkProps as AriaLinkProps } from 'react-aria-components'; +import type { ReactNode } from 'react'; /** @public */ export interface LinkProps extends AriaLinkProps { @@ -32,4 +33,10 @@ export interface LinkProps extends AriaLinkProps { | TextColorStatus | Partial>; truncate?: boolean; + + // This is used to set the title attribute on the link + title?: string; + + // This is used to set the children of the link + children?: ReactNode; } diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index f97f3f57ff..d8ebf6cba8 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -20,6 +20,7 @@ .bui-MenuPopover { display: flex; flex-direction: column; + box-shadow: var(--bui-shadow); border: 1px solid var(--bui-border); border-radius: var(--bui-radius-2); background: var(--bui-bg-surface-1); @@ -188,48 +189,56 @@ } .bui-MenuSearchField { - position: relative; font-family: var(--bui-font-regular); width: 100%; flex-shrink: 0; + display: flex; + align-items: center; + border-bottom: 1px solid var(--bui-border); + background-color: var(--bui-bg-surface-1); + height: 2rem; &[data-empty] { .bui-MenuSearchFieldClear { - display: none; + visibility: hidden; } } } .bui-MenuSearchFieldInput { + flex: 1; display: flex; align-items: center; - padding: 0 var(--bui-space-3); + padding: 0; border: none; - border-bottom: 1px solid var(--bui-border); - background-color: var(--bui-bg-surface-1); + background-color: transparent; font-size: var(--bui-font-size-3); font-family: var(--bui-font-regular); font-weight: var(--bui-font-weight-regular); color: var(--bui-fg-primary); width: 100%; - height: 2rem; - cursor: inherit; + height: 100%; outline: none; + cursor: inherit; &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } + + &::placeholder { + color: var(--bui-fg-secondary); + } + + &:first-child { + padding-inline: var(--bui-space-3) 0; + } } .bui-MenuSearchFieldClear { - position: absolute; - right: var(--bui-space-2); - top: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; + flex: 0 0 auto; + display: grid; + place-content: center; background-color: transparent; border: none; padding: 0; @@ -237,6 +246,12 @@ cursor: pointer; color: var(--bui-fg-secondary); transition: color 0.2s ease-in-out; + width: 2rem; + height: 2rem; + + &:hover { + color: var(--bui-fg-primary); + } & > svg { width: 1rem; diff --git a/packages/ui/src/components/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx index 7171b3ef83..ffe184d4ae 100644 --- a/packages/ui/src/components/Menu/Menu.stories.tsx +++ b/packages/ui/src/components/Menu/Menu.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { MenuTrigger, SubmenuTrigger, @@ -38,7 +38,7 @@ import { import { MemoryRouter } from 'react-router-dom'; import { useEffect, useState } from 'react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/Menu', component: MenuTrigger, decorators: [ @@ -48,10 +48,7 @@ const meta = { ), ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const options = [ { label: 'Apple', value: 'apple' }, @@ -65,7 +62,7 @@ const options = [ { label: 'Honeydew', value: 'honeydew' }, ]; -export const Preview: Story = { +export const Preview = meta.story({ args: { children: null, }, @@ -91,11 +88,11 @@ export const Preview: Story = { ), -}; +}); -export const PreviewSubmenu: Story = { +export const PreviewSubmenu = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -119,11 +116,11 @@ export const PreviewSubmenu: Story = { ), -}; +}); -export const PreviewIcons: Story = { +export const PreviewIcons = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -135,11 +132,11 @@ export const PreviewIcons: Story = { ), -}; +}); -export const PreviewSections: Story = { +export const PreviewSections = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -159,11 +156,11 @@ export const PreviewSections: Story = { ), -}; +}); -export const PreviewSeparators: Story = { +export const PreviewSeparators = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -180,11 +177,11 @@ export const PreviewSeparators: Story = { ), -}; +}); -export const PreviewLinks: Story = { +export const PreviewLinks = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -198,11 +195,11 @@ export const PreviewLinks: Story = { ), -}; +}); -export const Opened: Story = { +export const Opened = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -214,11 +211,11 @@ export const Opened: Story = { ), -}; +}); -export const WithIcons: Story = { +export const WithIcons = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -230,11 +227,11 @@ export const WithIcons: Story = { ), -}; +}); -export const WithSections: Story = { +export const WithSections = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -254,11 +251,11 @@ export const WithSections: Story = { ), -}; +}); -export const WithSeparators: Story = { +export const WithSeparators = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -275,11 +272,11 @@ export const WithSeparators: Story = { ), -}; +}); -export const WithColors: Story = { +export const WithColors = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -295,11 +292,11 @@ export const WithColors: Story = { ), -}; +}); -export const WithLinks: Story = { +export const WithLinks = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -313,11 +310,11 @@ export const WithLinks: Story = { ), -}; +}); -export const Submenu: Story = { +export const Submenu = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => ( @@ -341,11 +338,11 @@ export const Submenu: Story = { ), -}; +}); -export const Virtualized: Story = { +export const Virtualized = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -376,11 +373,11 @@ export const Virtualized: Story = { ); }, -}; +}); -export const VirtualizedMaxHeight: Story = { +export const VirtualizedMaxHeight = meta.story({ args: { - ...Preview.args, + ...Preview.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -411,9 +408,9 @@ export const VirtualizedMaxHeight: Story = { ); }, -}; +}); -export const WithScroll: Story = { +export const WithScroll = meta.story({ args: { children: null, }, @@ -446,4 +443,4 @@ export const WithScroll: Story = { ), -}; +}); diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index 6edbeaca20..fa86f67700 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -33,9 +33,9 @@ import { RouterProvider, Virtualizer, ListLayout, - OverlayTriggerStateContext, } from 'react-aria-components'; import { useStyles } from '../../hooks/useStyles'; +import { MenuDefinition } from './definition'; import type { MenuTriggerProps, SubmenuTriggerProps, @@ -55,7 +55,6 @@ import { } from '@remixicon/react'; import { useNavigate, useHref } from 'react-router-dom'; import { isExternalLink } from '../../utils/isExternalLink'; -import { useRef, useEffect, useContext } from 'react'; import styles from './Menu.module.css'; import clsx from 'clsx'; @@ -63,7 +62,7 @@ import clsx from 'clsx'; const rowHeight = 32; const MenuEmptyState = () => { - const { classNames } = useStyles('Menu'); + const { classNames } = useStyles(MenuDefinition); return (
    @@ -84,7 +83,7 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => { /** @public */ export const Menu = (props: MenuProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, placement = 'bottom start', @@ -97,33 +96,6 @@ export const Menu = (props: MenuProps) => { const navigate = useNavigate(); let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); - const popoverRef = useRef(null); - const state = useContext(OverlayTriggerStateContext); - - // Custom click-outside handler for non-modal popovers - useEffect(() => { - if (!state?.isOpen) return; - - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Node; - - // Check if click is outside the popover - if (popoverRef.current && !popoverRef.current.contains(target)) { - // Check if click is on a trigger button or submenu - const isOnTrigger = (target as Element).closest('[data-trigger]'); - const isOnSubmenu = (target as Element).closest('[role="menu"]'); - - if (!isOnTrigger && !isOnSubmenu) { - state.close(); - } - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [state]); const menuContent = ( ) => { return ( {virtualized ? ( @@ -166,7 +135,7 @@ export const Menu = (props: MenuProps) => { /** @public */ export const MenuListBox = (props: MenuListBoxProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, selectionMode = 'single', @@ -215,7 +184,7 @@ export const MenuListBox = (props: MenuListBoxProps) => { /** @public */ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, placement = 'bottom start', @@ -254,13 +223,13 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { classNames.searchField, styles[classNames.searchField], )} + aria-label={props.placeholder || 'Search'} > ) => { export const MenuAutocompleteListbox = ( props: MenuAutocompleteListBoxProps, ) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, selectionMode = 'single', @@ -333,13 +302,13 @@ export const MenuAutocompleteListbox = ( classNames.searchField, styles[classNames.searchField], )} + aria-label={props.placeholder || 'Search'} > { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, iconStart, @@ -449,7 +418,7 @@ export const MenuItem = (props: MenuItemProps) => { /** @public */ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { children, className, ...rest } = cleanedProps; return ( @@ -490,7 +459,7 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { /** @public */ export const MenuSection = (props: MenuSectionProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { children, className, title, ...rest } = cleanedProps; return ( @@ -517,7 +486,7 @@ export const MenuSection = (props: MenuSectionProps) => { /** @public */ export const MenuSeparator = (props: MenuSeparatorProps) => { - const { classNames, cleanedProps } = useStyles('Menu', props); + const { classNames, cleanedProps } = useStyles(MenuDefinition, props); const { className, ...rest } = cleanedProps; return ( diff --git a/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx b/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx index c0e1aca932..af68ec9415 100644 --- a/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx +++ b/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { MenuTrigger, SubmenuTrigger, @@ -26,7 +26,7 @@ import { Button } from '../..'; import { useState, useEffect } from 'react'; import { MemoryRouter } from 'react-router-dom'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/MenuAutocomplete', component: MenuTrigger, decorators: [ @@ -36,10 +36,7 @@ const meta = { ), ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const options = [ { label: 'Apple', value: 'apple' }, @@ -53,7 +50,7 @@ const options = [ { label: 'Honeydew', value: 'honeydew' }, ]; -export const Default: Story = { +export const Default = meta.story({ args: { children: null, }, @@ -72,11 +69,11 @@ export const Default: Story = { ), -}; +}); -export const PreviewAutocompleteMenu: Story = { +export const PreviewAutocompleteMenu = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => ( @@ -93,11 +90,11 @@ export const PreviewAutocompleteMenu: Story = { ), -}; +}); -export const Virtualized: Story = { +export const Virtualized = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -132,11 +129,11 @@ export const Virtualized: Story = { ); }, -}; +}); -export const VirtualizedMaxHeight: Story = { +export const VirtualizedMaxHeight = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -172,11 +169,11 @@ export const VirtualizedMaxHeight: Story = { ); }, -}; +}); -export const Submenu: Story = { +export const Submenu = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => ( @@ -197,4 +194,4 @@ export const Submenu: Story = { ), -}; +}); diff --git a/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx b/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx index bf66f5f8fd..e1c22fbd9e 100644 --- a/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx +++ b/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { MenuTrigger, MenuAutocompleteListbox, @@ -28,7 +28,7 @@ import { useEffect, useState } from 'react'; import { Selection } from 'react-aria-components'; import { MemoryRouter } from 'react-router-dom'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/MenuAutocompleteListBox', component: MenuTrigger, decorators: [ @@ -38,10 +38,7 @@ const meta = { ), ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const options = [ { label: 'Apple', value: 'apple' }, @@ -55,7 +52,7 @@ const options = [ { label: 'Honeydew', value: 'honeydew' }, ]; -export const Default: Story = { +export const Default = meta.story({ args: { children: null, }, @@ -83,11 +80,11 @@ export const Default: Story = { ); }, -}; +}); -export const PreviewListbox: Story = { +export const PreviewListbox = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [selected, setSelected] = useState( @@ -113,11 +110,11 @@ export const PreviewListbox: Story = { ); }, -}; +}); -export const PreviewListboxMultiple: Story = { +export const PreviewListboxMultiple = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [selected, setSelected] = useState( @@ -144,11 +141,11 @@ export const PreviewListboxMultiple: Story = { ); }, -}; +}); -export const Submenu: Story = { +export const Submenu = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [selected, setSelected] = useState( @@ -182,11 +179,11 @@ export const Submenu: Story = { ); }, -}; +}); -export const Virtualized: Story = { +export const Virtualized = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -221,11 +218,11 @@ export const Virtualized: Story = { ); }, -}; +}); -export const VirtualizedMaxHeight: Story = { +export const VirtualizedMaxHeight = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -261,4 +258,4 @@ export const VirtualizedMaxHeight: Story = { ); }, -}; +}); diff --git a/packages/ui/src/components/Menu/MenuListBox.stories.tsx b/packages/ui/src/components/Menu/MenuListBox.stories.tsx index 72284fb6b9..2297598d0c 100644 --- a/packages/ui/src/components/Menu/MenuListBox.stories.tsx +++ b/packages/ui/src/components/Menu/MenuListBox.stories.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { MenuTrigger, MenuListBox, MenuListBoxItem } from './index'; import { Button, Flex, Text } from '../..'; import { useEffect, useState } from 'react'; import { Selection } from 'react-aria-components'; import { MemoryRouter } from 'react-router-dom'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/MenuListBox', component: MenuTrigger, decorators: [ @@ -31,12 +31,9 @@ const meta = { ), ], -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { children: null, }, @@ -50,11 +47,11 @@ export const Default: Story = { ), -}; +}); -export const Controlled: Story = { +export const Controlled = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [selected, setSelected] = useState(new Set(['paul'])); @@ -86,11 +83,11 @@ export const Controlled: Story = { ); }, -}; +}); -export const Virtualized: Story = { +export const Virtualized = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -121,11 +118,11 @@ export const Virtualized: Story = { ); }, -}; +}); -export const VirtualizedMaxHeight: Story = { +export const VirtualizedMaxHeight = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: () => { const [pokemon, setPokemon] = useState< @@ -156,4 +153,4 @@ export const VirtualizedMaxHeight: Story = { ); }, -}; +}); diff --git a/packages/ui/src/components/Menu/definition.ts b/packages/ui/src/components/Menu/definition.ts new file mode 100644 index 0000000000..e4ba0d83ab --- /dev/null +++ b/packages/ui/src/components/Menu/definition.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Menu + * @public + */ +export const MenuDefinition = { + classNames: { + root: 'bui-Menu', + popover: 'bui-MenuPopover', + content: 'bui-MenuContent', + section: 'bui-MenuSection', + sectionHeader: 'bui-MenuSectionHeader', + item: 'bui-MenuItem', + itemListBox: 'bui-MenuItemListBox', + itemListBoxCheck: 'bui-MenuItemListBoxCheck', + itemWrapper: 'bui-MenuItemWrapper', + itemContent: 'bui-MenuItemContent', + itemArrow: 'bui-MenuItemArrow', + separator: 'bui-MenuSeparator', + searchField: 'bui-MenuSearchField', + searchFieldInput: 'bui-MenuSearchFieldInput', + searchFieldClear: 'bui-MenuSearchFieldClear', + emptyState: 'bui-MenuEmptyState', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Menu/index.ts b/packages/ui/src/components/Menu/index.ts index 00237712df..f42de8a7eb 100644 --- a/packages/ui/src/components/Menu/index.ts +++ b/packages/ui/src/components/Menu/index.ts @@ -26,6 +26,7 @@ export { MenuSection, MenuSeparator, } from './Menu'; +export { MenuDefinition } from './definition'; export type { MenuTriggerProps, SubmenuTriggerProps, diff --git a/packages/ui/src/components/PasswordField/PasswordField.module.css b/packages/ui/src/components/PasswordField/PasswordField.module.css index bf78ab477f..adad950816 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.module.css +++ b/packages/ui/src/components/PasswordField/PasswordField.module.css @@ -23,52 +23,136 @@ font-family: var(--bui-font-regular); width: 100%; flex-shrink: 0; - /* Reserve space for browser/password manager icon (e.g. 1Password) */ - --bui-passwordmanager-icon-width: var(--bui-space-1); + + &[data-size='small'] { + --password-field-item-height: 2rem; + } + + &[data-size='medium'] { + --password-field-item-height: 2.5rem; + } } - .bui-InputVisibility { - position: absolute; - right: var(--bui-passwordmanager-icon-width); - top: 0; - bottom: 0; + .bui-PasswordFieldInputWrapper { + display: flex; + align-items: center; + border-radius: var(--bui-radius-2); + border: 1px solid var(--bui-border); + background-color: var(--bui-bg-surface-1); + transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; + + &[data-size='small'] { + height: 2rem; + } + + &[data-size='medium'] { + height: 2.5rem; + } + + &:focus-within { + border-color: var(--bui-border-pressed); + outline-width: 0px; + } + + &:hover { + border-color: var(--bui-border-hover); + } + + &:has([data-invalid]) { + border-color: var(--bui-fg-danger); + } + + &:has([data-disabled]) { + opacity: 0.5; + cursor: not-allowed; + border: 1px solid var(--bui-border-disabled); + } + } + + .bui-PasswordFieldIcon { + flex: 0 0 auto; + display: grid; + place-content: center; + color: var(--bui-fg-primary); + pointer-events: none; + width: var(--password-field-item-height); + height: var(--password-field-item-height); + + & svg { + .bui-PasswordField[data-size='small'] & { + width: 1rem; + height: 1rem; + } + + .bui-PasswordField[data-size='medium'] & { + width: 1.25rem; + height: 1.25rem; + } + } + } + + .bui-PasswordFieldInput { + flex: 1; + display: flex; + align-items: center; + padding: 0; + border: none; + background-color: transparent; + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + font-weight: var(--bui-font-weight-regular); + color: var(--bui-fg-primary); + width: 100%; + height: 100%; + outline: none; + + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + } + + &::placeholder { + color: var(--bui-fg-secondary); + } + + &[data-disabled] { + cursor: not-allowed; + } + + &:first-child { + .bui-PasswordField[data-size='small'] & { + padding-inline: var(--bui-space-3) 0; + } + + .bui-PasswordField[data-size='medium'] & { + padding-inline: var(--bui-space-4) 0; + } + } + } + + .bui-PasswordFieldVisibility { + flex: 0 0 auto; + display: grid; + place-content: center; background-color: transparent; cursor: pointer; border: none; padding: 0; margin: 0; - display: flex; - align-items: center; - justify-content: center; color: var(--bui-fg-primary); + width: var(--password-field-item-height); + height: var(--password-field-item-height); - /* Size: small */ - &[data-size='small'] { - width: 2rem; - height: 2rem; - } - &[data-size='small'] svg { - width: 1rem; - height: 1rem; - } + & svg { + .bui-PasswordField[data-size='small'] & { + width: 1rem; + height: 1rem; + } - /* Size: medium */ - &[data-size='medium'] { - width: 2.5rem; - height: 2.5rem; + .bui-PasswordField[data-size='medium'] & { + width: 1.25rem; + height: 1.25rem; + } } - &[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; - } - } - - /* Ensure input has enough right padding for our toggle + PM icon */ - .bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input { - padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); - } - - .bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input { - padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); } } diff --git a/packages/ui/src/components/PasswordField/PasswordField.stories.tsx b/packages/ui/src/components/PasswordField/PasswordField.stories.tsx index 1867e5b7bf..8025ce7217 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.stories.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { PasswordField } from './PasswordField'; import { Form } from 'react-aria-components'; import { Flex } from '../Flex'; import { FieldLabel } from '../FieldLabel'; import { RiSparklingLine } from '@remixicon/react'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/PasswordField', component: PasswordField, argTypes: { @@ -32,12 +31,9 @@ const meta = { control: 'object', }, }, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { name: 'secret', placeholder: 'Enter a secret', @@ -45,11 +41,11 @@ export const Default: Story = { maxWidth: '300px', }, }, -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( @@ -57,79 +53,77 @@ export const Sizes: Story = { } /> ), -}; +}); -export const DefaultValue: Story = { +export const DefaultValue = meta.story({ args: { - ...Default.args, + ...Default.input.args, defaultValue: 'https://example.com', }, -}; +}); -export const WithLabel: Story = { +export const WithLabel = meta.story({ args: { - ...Default.args, + ...Default.input.args, label: 'Label', }, -}; +}); -export const WithDescription: Story = { +export const WithDescription = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, description: 'Description', }, -}; +}); -export const Required: Story = { +export const Required = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, isRequired: true, }, -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ args: { - ...Default.args, + ...Default.input.args, isDisabled: true, }, -}; +}); -export const WithIcon: Story = { +export const WithIcon = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( } /> ), -}; +}); -export const DisabledWithIcon: Story = { +export const DisabledWithIcon = WithIcon.extend({ args: { - ...WithIcon.args, isDisabled: true, }, - render: WithIcon.render, -}; +}); -export const ShowError: Story = { +export const ShowError = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, }, render: args => (
    ), -}; +}); -export const Validation: Story = { +export const Validation = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, validate: value => (value === 'admin' ? 'Nice try!' : null), }, -}; +}); -export const CustomField: Story = { +export const CustomField = meta.story({ render: () => ( <> ), -}; +}); diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index d7cea3a6bd..c2cfdd1081 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -26,9 +26,9 @@ import { FieldError } from '../FieldError'; import type { PasswordFieldProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { PasswordFieldDefinition } from './definition'; import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; import stylesPasswordField from './PasswordField.module.css'; -import stylesTextField from '../TextField/TextField.module.css'; /** @public */ export const PasswordField = forwardRef( @@ -47,13 +47,11 @@ export const PasswordField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames: textFieldClassNames } = useStyles('TextField'); - const { classNames: classNamesPasswordField, dataAttributes, cleanedProps, - } = useStyles('PasswordField', { + } = useStyles(PasswordFieldDefinition, { size: 'small', ...props, }); @@ -96,16 +94,16 @@ export const PasswordField = forwardRef( />
    {icon && ( )} -
    - setIsVisible(v => !v)} - className={clsx( - classNamesPasswordField.inputVisibility, - stylesPasswordField[classNamesPasswordField.inputVisibility], - )} - > - {isVisible ? : } - -
    + setIsVisible(v => !v)} + className={clsx( + classNamesPasswordField.inputVisibility, + stylesPasswordField[classNamesPasswordField.inputVisibility], + )} + > + {isVisible ? : } +
    diff --git a/packages/ui/src/components/PasswordField/definition.ts b/packages/ui/src/components/PasswordField/definition.ts new file mode 100644 index 0000000000..11272494b4 --- /dev/null +++ b/packages/ui/src/components/PasswordField/definition.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for PasswordField + * @public + */ +export const PasswordFieldDefinition = { + classNames: { + root: 'bui-PasswordField', + inputWrapper: 'bui-PasswordFieldInputWrapper', + input: 'bui-PasswordFieldInput', + inputIcon: 'bui-PasswordFieldIcon', + inputVisibility: 'bui-PasswordFieldVisibility', + }, + dataAttributes: { + size: ['small', 'medium'] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/PasswordField/index.ts b/packages/ui/src/components/PasswordField/index.ts index df7874e1c2..705bf68c04 100644 --- a/packages/ui/src/components/PasswordField/index.ts +++ b/packages/ui/src/components/PasswordField/index.ts @@ -15,4 +15,5 @@ */ export * from './PasswordField'; +export { PasswordFieldDefinition } from './definition'; export * from './types'; diff --git a/packages/ui/src/components/Popover/Popover.module.css b/packages/ui/src/components/Popover/Popover.module.css index 873dc33e81..fe757c8dd4 100644 --- a/packages/ui/src/components/Popover/Popover.module.css +++ b/packages/ui/src/components/Popover/Popover.module.css @@ -18,12 +18,124 @@ @layer components { .bui-Popover { - margin-right: 12px; - overflow: scroll; - background-color: var(--bui-bg-surface-1); - border: 1px solid var(--bui-border); + box-shadow: var(--bui-shadow); border-radius: var(--bui-radius-3); - padding-block: var(--bui-space-1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + background: var(--bui-bg-surface-1); + border: 1px solid var(--bui-gray-3); + forced-color-adjust: none; + outline: none; + max-height: inherit; + display: flex; + flex-direction: column; + /* fixes FF gap */ + transform: translate3d(0, 0, 0); + transition: transform 200ms, opacity 200ms; + + &[data-entering], + &[data-exiting] { + transform: var(--origin); + opacity: 0; + } + + &[data-placement='top'] { + --origin: translateY(4px); + + &:has(.bui-PopoverArrow) { + margin-bottom: var(--bui-space-3); + } + } + + &[data-placement='right'] { + --origin: translateX(-4px); + + &:has(.bui-PopoverArrow) { + margin-left: var(--bui-space-3); + } + } + + &[data-placement='bottom'] { + --origin: translateY(-4px); + + &:has(.bui-PopoverArrow) { + margin-top: var(--bui-space-3); + } + } + + &[data-placement='left'] { + --origin: translateX(4px); + + &:has(.bui-PopoverArrow) { + margin-right: var(--bui-space-3); + } + } + } + + .bui-PopoverContent { + overflow-x: hidden; + overflow-y: auto; + padding: var(--bui-space-4); + flex: 1 1 auto; + min-height: 0; + } + + .bui-PopoverArrow { + & svg { + display: block; + + /* The popover is rendered overlaying the main + popover element by 1px. This causes the borders + to overlap, which causes minor visual artifacts + with transparent border colors. To mitigate this, + we split the stroke and fill across separate + elements in order to guarantee that the stroke is + always overlaying a consistent color. */ + path:nth-child(1) { + fill: var(--bui-bg-surface-1); + } + + path:nth-child(2) { + fill: var(--bui-gray-3); + } + + /* The arrow svg overlaps the popover by 2px, so we + need to adjust the margins accordingly. */ + --popover-arrow-overlap: -2px; + } + + &[data-placement='top'] svg { + margin-top: var(--popover-arrow-overlap); + } + + &[data-placement='bottom'] svg { + margin-bottom: var(--popover-arrow-overlap); + transform: rotate(180deg); + } + + &[data-placement='right'] svg { + margin-right: var(--popover-arrow-overlap); + transform: rotate(90deg); + } + + &[data-placement='left'] svg { + margin-left: var(--popover-arrow-overlap); + transform: rotate(-90deg); + } + } + + [data-theme='dark'] { + .bui-Popover { + background: var(--bui-bg-surface-2); + border: 1px solid var(--bui-gray-4); + } + + .bui-PopoverArrow { + svg path:nth-child(1) { + fill: var(--bui-bg-surface-2); + } + + svg path:nth-child(2) { + fill: var(--bui-gray-4); + } + } } } diff --git a/packages/ui/src/components/Popover/Popover.stories.tsx b/packages/ui/src/components/Popover/Popover.stories.tsx new file mode 100644 index 0000000000..ea92bf9d5c --- /dev/null +++ b/packages/ui/src/components/Popover/Popover.stories.tsx @@ -0,0 +1,278 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import preview from '../../../../../.storybook/preview'; +import { Popover } from './Popover'; +import { Button } from '../Button/Button'; +import { DialogTrigger } from '../Dialog/Dialog'; +import { Text } from '../Text/Text'; +import { Flex } from '../Flex/Flex'; + +const meta = preview.meta({ + title: 'Backstage UI/Popover', + component: Popover, + parameters: { layout: 'centered' }, + argTypes: { + isOpen: { + control: { type: 'boolean' }, + }, + hideArrow: { + control: { type: 'boolean' }, + }, + placement: { + control: { type: 'select' }, + options: [ + 'top', + 'top start', + 'top end', + 'bottom', + 'bottom start', + 'bottom end', + 'left', + 'left start', + 'left end', + 'right', + 'right start', + 'right end', + ], + }, + offset: { + control: { type: 'number' }, + }, + }, + render: ({ children, isOpen, hideArrow, placement, offset }) => ( + + + + {children ?? This is a popover} + + + ), +}); + +export const Default = meta.story({ + args: { + children: This is a popover, + }, +}); + +export const IsOpen = Default.extend({ + args: { + isOpen: true, + }, +}); + +export const HideArrow = Default.extend({ + args: { + isOpen: true, + hideArrow: true, + }, +}); + +export const TopPlacement = Default.extend({ + args: { + isOpen: true, + placement: 'top', + }, +}); + +export const RightPlacement = Default.extend({ + args: { + isOpen: true, + placement: 'right', + }, +}); + +export const BottomPlacement = Default.extend({ + args: { + isOpen: true, + placement: 'bottom', + }, +}); + +export const LeftPlacement = Default.extend({ + args: { + isOpen: true, + placement: 'left', + }, +}); + +export const AllPlacements = Default.extend({ + parameters: { + controls: { + exclude: ['placement'], + }, + }, + args: { + isOpen: true, + }, + render: ({ isOpen, hideArrow }) => { + return ( + + + + Top placement + + + Right placement + + + Bottom placement + + + Left placement + + + ); + }, +}); + +export const AllPlacementsNoArrow = Default.extend({ + parameters: { + controls: { + exclude: ['placement', 'hideArrow'], + }, + }, + args: { + isOpen: true, + hideArrow: true, + }, + render: ({ isOpen }) => { + return ( + + + + Top placement + + + Right placement + + + Bottom placement + + + Left placement + + + ); + }, +}); + +export const WithRichContent = Default.extend({ + args: { + isOpen: true, + }, + render: ({ isOpen, hideArrow, placement }) => ( + + + + + Popover Title + + This is a popover with rich content. It can contain multiple + elements and formatted text. + + + + + + + + + ), +}); + +export const CustomOffset = Default.extend({ + args: { + isOpen: true, + offset: 20, + placement: 'bottom', + }, +}); + +export const NonModal = Default.extend({ + args: { + isOpen: true, + }, + render: ({ isOpen, hideArrow, placement }) => ( + + + + + This is a non-modal popover. You can interact with other elements on + the page while it's open. + + + + ), +}); + +export const WithLongContent = Default.extend({ + args: { + isOpen: true, + }, + render: ({ isOpen, hideArrow, placement }) => ( + + + + + Long Content Example + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. + + + Sed ut perspiciatis unde omnis iste natus error sit voluptatem + accusantium doloremque laudantium, totam rem aperiam, eaque ipsa + quae ab illo inventore veritatis et quasi architecto beatae vitae + dicta sunt explicabo. + + + Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut + fugit, sed quia consequuntur magni dolores eos qui ratione + voluptatem sequi nesciunt. + + + Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, + consectetur, adipisci velit, sed quia non numquam eius modi tempora + incidunt ut labore et dolore magnam aliquam quaerat voluptatem. + + + + + ), +}); diff --git a/packages/ui/src/components/Popover/Popover.tsx b/packages/ui/src/components/Popover/Popover.tsx new file mode 100644 index 0000000000..1e38e98375 --- /dev/null +++ b/packages/ui/src/components/Popover/Popover.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef } from 'react'; +import { OverlayArrow, Popover as AriaPopover } from 'react-aria-components'; +import clsx from 'clsx'; +import { PopoverProps } from './types'; +import { useStyles } from '../../hooks/useStyles'; +import { PopoverDefinition } from './definition'; +import styles from './Popover.module.css'; + +/** + * A popover component built on React Aria Components that displays floating + * content anchored to a trigger element. + * + * @remarks + * The Popover component supports multiple placements (top, right, bottom, left), + * automatic viewport-constrained scrolling, and conditional arrow rendering. It + * automatically handles positioning, collision detection, and ARIA attributes for + * accessibility. Content is automatically padded and scrollable when it exceeds + * available space. + * + * @example + * Basic usage with DialogTrigger: + * ```tsx + * + * + * + * Popover content + * + * + * ``` + * + * @example + * With custom placement and no arrow: + * ```tsx + * + * + * + * Content without arrow + * + * + * ``` + * + * @public + */ +export const Popover = forwardRef( + (props, ref) => { + const { classNames, cleanedProps } = useStyles(PopoverDefinition, props); + const { className, children, hideArrow, ...rest } = cleanedProps; + + return ( + + {({ trigger }) => ( + <> + {!hideArrow && + trigger !== 'MenuTrigger' && + trigger !== 'SubmenuTrigger' && ( + + + + + + + )} +
    + {children} +
    + + )} +
    + ); + }, +); + +Popover.displayName = 'Popover'; diff --git a/packages/ui/src/components/Popover/definition.ts b/packages/ui/src/components/Popover/definition.ts new file mode 100644 index 0000000000..1361d4bbcc --- /dev/null +++ b/packages/ui/src/components/Popover/definition.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Popover + * @public + */ +export const PopoverDefinition = { + classNames: { + root: 'bui-Popover', + arrow: 'bui-PopoverArrow', + content: 'bui-PopoverContent', + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Popover/index.ts b/packages/ui/src/components/Popover/index.ts new file mode 100644 index 0000000000..e97be21931 --- /dev/null +++ b/packages/ui/src/components/Popover/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { Popover } from './Popover'; +export type { PopoverProps } from './types'; +export { PopoverDefinition } from './definition'; diff --git a/packages/ui/src/components/Popover/types.ts b/packages/ui/src/components/Popover/types.ts new file mode 100644 index 0000000000..fd04eef321 --- /dev/null +++ b/packages/ui/src/components/Popover/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PopoverProps as AriaPopoverProps } from 'react-aria-components'; + +/** + * Properties for {@link Popover} + * + * @public + */ +export interface PopoverProps extends Omit { + /** + * The content to display inside the popover. + * Content is automatically wrapped with padding and scroll behavior. + */ + children: React.ReactNode; + + /** + * Whether to hide the arrow pointing to the trigger element. + * Arrow is also automatically hidden for MenuTrigger and SubmenuTrigger contexts. + * + * @defaultValue false + */ + hideArrow?: boolean; +} diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.module.css b/packages/ui/src/components/RadioGroup/RadioGroup.module.css index acc6d677e4..a18f2d385d 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.module.css +++ b/packages/ui/src/components/RadioGroup/RadioGroup.module.css @@ -54,6 +54,8 @@ background: var(--bui-gray-1); border-radius: var(--bui-radius-full); transition: all 200ms; + flex-shrink: 0; + flex-grow: 0; } &[data-pressed]:before { diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx index df7a826c50..4f3465dade 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx +++ b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,19 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { Meta, StoryObj } from '@storybook/react-vite'; +import preview from '../../../../../.storybook/preview'; import { RadioGroup, Radio } from './RadioGroup'; -const meta = { +const meta = preview.meta({ title: 'Backstage UI/RadioGroup', component: RadioGroup, -} satisfies Meta; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { label: 'What is your favorite pokemon?', }, @@ -36,11 +32,11 @@ export const Default: Story = { Squirtle ), -}; +}); -export const Horizontal: Story = { +export const Horizontal = meta.story({ args: { - ...Default.args, + ...Default.input.args, orientation: 'horizontal', }, render: args => ( @@ -50,11 +46,11 @@ export const Horizontal: Story = { Squirtle ), -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ args: { - ...Default.args, + ...Default.input.args, isDisabled: true, }, render: args => ( @@ -64,11 +60,11 @@ export const Disabled: Story = { Squirtle ), -}; +}); -export const DisabledSingle: Story = { +export const DisabledSingle = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( @@ -79,11 +75,11 @@ export const DisabledSingle: Story = { Squirtle ), -}; +}); -export const DisabledAndSelected: Story = { +export const DisabledAndSelected = meta.story({ args: { - ...Default.args, + ...Default.input.args, value: 'charmander', }, render: args => ( @@ -95,11 +91,11 @@ export const DisabledAndSelected: Story = { Squirtle ), -}; +}); -export const Invalid: Story = { +export const Invalid = meta.story({ args: { - ...Default.args, + ...Default.input.args, name: 'pokemon', isInvalid: true, }, @@ -112,11 +108,11 @@ export const Invalid: Story = { Squirtle ), -}; +}); -export const Validation: Story = { +export const Validation = meta.story({ args: { - ...Default.args, + ...Default.input.args, name: 'pokemon', defaultValue: 'charmander', validationBehavior: 'aria', @@ -129,11 +125,11 @@ export const Validation: Story = { Squirtle ), -}; +}); -export const ReadOnly: Story = { +export const ReadOnly = meta.story({ args: { - ...Default.args, + ...Default.input.args, isReadOnly: true, defaultValue: 'charmander', }, @@ -144,4 +140,4 @@ export const ReadOnly: Story = { Squirtle ), -}; +}); diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.tsx index 355e181f37..de6cc3793b 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.tsx +++ b/packages/ui/src/components/RadioGroup/RadioGroup.tsx @@ -23,6 +23,7 @@ import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; import { FieldError } from '../FieldError'; import { useStyles } from '../../hooks/useStyles'; +import { RadioGroupDefinition } from './definition'; import styles from './RadioGroup.module.css'; import type { RadioGroupProps, RadioProps } from './types'; @@ -30,7 +31,7 @@ import type { RadioGroupProps, RadioProps } from './types'; /** @public */ export const RadioGroup = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles('RadioGroup', props); + const { classNames, cleanedProps } = useStyles(RadioGroupDefinition, props); const { className, label, @@ -83,7 +84,7 @@ RadioGroup.displayName = 'RadioGroup'; export const Radio = forwardRef((props, ref) => { const { className, ...rest } = props; - const { classNames } = useStyles('RadioGroup'); + const { classNames } = useStyles(RadioGroupDefinition); return ( ; +}); -export default meta; -type Story = StoryObj; - -export const Default: Story = { +export const Default = meta.story({ args: { name: 'url', style: { @@ -52,11 +50,11 @@ export const Default: Story = { }, 'aria-label': 'Search', }, -}; +}); -export const Sizes: Story = { +export const Sizes = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( @@ -64,46 +62,46 @@ export const Sizes: Story = { ), -}; +}); -export const DefaultValue: Story = { +export const DefaultValue = meta.story({ args: { - ...Default.args, + ...Default.input.args, defaultValue: 'https://example.com', }, -}; +}); -export const WithLabel: Story = { +export const WithLabel = meta.story({ args: { - ...Default.args, + ...Default.input.args, label: 'Label', }, -}; +}); -export const WithDescription: Story = { +export const WithDescription = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, description: 'Description', }, -}; +}); -export const Required: Story = { +export const Required = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, isRequired: true, }, -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ args: { - ...Default.args, + ...Default.input.args, isDisabled: true, }, -}; +}); -export const WithIcon: Story = { +export const WithIcon = meta.story({ args: { - ...Default.args, + ...Default.input.args, }, render: args => ( } /> ), -}; +}); -export const DisabledWithIcon: Story = { +export const DisabledWithIcon = meta.story({ args: { - ...WithIcon.args, + ...WithIcon.input.args, isDisabled: true, }, -}; +}); -export const ShowError: Story = { +export const ShowError = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, }, render: args => (
    ), -}; +}); -export const Validation: Story = { +export const Validation = meta.story({ args: { - ...WithLabel.args, + ...WithLabel.input.args, validate: value => (value === 'admin' ? 'Nice try!' : null), }, -}; +}); -export const CustomField: Story = { +export const CustomField = meta.story({ render: () => ( <> ), -}; +}); -export const StartCollapsed: Story = { +export const StartCollapsed = meta.story({ args: { - ...Default.args, + ...Default.input.args, startCollapsed: true, }, render: args => ( - + + + + + - ), -}; +}); -export const InHeader: Story = { +export const StartCollapsedWithValue = meta.story({ + args: { + ...StartCollapsed.input.args, + defaultValue: 'https://example.com', + }, + + render: args => , +}); + +export const InHeader = meta.story({ decorators: [ Story => ( @@ -204,11 +214,11 @@ export const InHeader: Story = { /> ), -}; +}); -export const StartCollapsedInHeader: Story = { +export const StartCollapsedInHeader = meta.story({ args: { - ...StartCollapsed.args, + ...StartCollapsed.input.args, }, decorators: [ Story => ( @@ -241,11 +251,11 @@ export const StartCollapsedInHeader: Story = { /> ), -}; +}); -export const StartCollapsedWithButtons: Story = { +export const StartCollapsedWithButtons = meta.story({ args: { - ...StartCollapsed.args, + ...StartCollapsed.input.args, }, render: args => ( @@ -271,11 +281,11 @@ export const StartCollapsedWithButtons: Story = { ), -}; +}); -export const StartCollapsedWithOnChange: Story = { +export const StartCollapsedWithOnChange = meta.story({ args: { - ...StartCollapsed.args, + ...StartCollapsed.input.args, }, render: args => { const handleChange = (value: string) => { @@ -292,4 +302,42 @@ export const StartCollapsedWithOnChange: Story = { ); }, -}; +}); + +export const StartCollapsedControlledEmpty = meta.story({ + args: { + ...StartCollapsed.input.args, + }, + render: function Render(args) { + const [value, setValue] = useState(''); + + return ( + + + + ); + }, +}); + +export const StartCollapsedControlledWithValue = meta.story({ + args: { + ...StartCollapsed.input.args, + }, + render: function Render(args) { + const [value, setValue] = useState('Component'); + + return ( + + + + ); + }, +}); diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index 820ceb42b9..eea749e46f 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { forwardRef, useEffect, useState } from 'react'; +import { forwardRef, useEffect, useState, useRef } from 'react'; import { Input, SearchField as AriaSearchField, @@ -25,8 +25,8 @@ import { FieldLabel } from '../FieldLabel'; import { FieldError } from '../FieldError'; import { RiSearch2Line, RiCloseCircleLine } from '@remixicon/react'; import { useStyles } from '../../hooks/useStyles'; -import stylesSearchField from './SearchField.module.css'; -import stylesTextField from '../TextField/TextField.module.css'; +import { SearchFieldDefinition } from './definition'; +import styles from './SearchField.module.css'; import type { SearchFieldProps } from './types'; @@ -39,9 +39,6 @@ export const SearchField = forwardRef( 'aria-labelledby': ariaLabelledBy, } = props; - const [isCollapsed, setIsCollapsed] = useState(false); - const [shouldCollapse, setShouldCollapse] = useState(true); - useEffect(() => { if (!label && !ariaLabel && !ariaLabelledBy) { console.warn( @@ -50,19 +47,15 @@ export const SearchField = forwardRef( } }, [label, ariaLabel, ariaLabelledBy]); - const { classNames: textFieldClassNames } = useStyles('TextField'); - - const { - classNames: searchFieldClassNames, - dataAttributes, - style, - cleanedProps, - } = useStyles('SearchField', { - size: 'small', - placeholder: 'Search', - startCollapsed: false, - ...props, - }); + const { classNames, dataAttributes, style, cleanedProps } = useStyles( + SearchFieldDefinition, + { + size: 'small', + placeholder: 'Search', + startCollapsed: false, + ...props, + }, + ); const { className, @@ -75,47 +68,39 @@ export const SearchField = forwardRef( ...rest } = cleanedProps; + const [isFocused, setIsFocused] = useState(false); + const inputRef = useRef(null); + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null); - const handleClick = (isFocused: boolean) => { + const handleFocusChange = (isFocused: boolean) => { props.onFocusChange?.(isFocused); - if (shouldCollapse) { - if (isFocused) { - setIsCollapsed(true); - } else { - setIsCollapsed(false); - } - } + setIsFocused(isFocused); }; - const handleChange = (value: string) => { - props.onChange?.(value); - if (value.length > 0) { - setShouldCollapse(false); - } else { - setShouldCollapse(true); - } + const handleContainerClick = () => { + inputRef.current?.focus(); }; + const hasInputRef = !!inputRef.current; + const hasValue = !!inputRef.current?.value; + + const isCollapsed = hasInputRef + ? startCollapsed && !hasValue && !isFocused + : startCollapsed && !rest.value && !rest.defaultValue && !isFocused; + return ( ( />
    {icon !== false && ( )} + + + + ); +} diff --git a/packages/ui/src/components/Select/SelectListBox.tsx b/packages/ui/src/components/Select/SelectListBox.tsx new file mode 100644 index 0000000000..3b14292729 --- /dev/null +++ b/packages/ui/src/components/Select/SelectListBox.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ListBox, ListBoxItem, Text } from 'react-aria-components'; +import { RiCheckLine } from '@remixicon/react'; +import clsx from 'clsx'; +import { useStyles } from '../../hooks/useStyles'; +import { SelectDefinition } from './definition'; +import styles from './Select.module.css'; +import type { Option } from './types'; + +interface SelectListBoxProps { + options?: Array
    - - Name - Owner - Type - Lifecycle - - - {data1.map(item => ( - - } - description={item.description} - /> - - - - - ))} - -
    - ); - }, -}; - -export const WithPaginationUncontrolled: Story = { - render: () => { - const { data, paginationProps } = useTable({ data: data1 }); - - return ( - <> - - - Name - Owner - Type - Lifecycle - - - {data?.map(item => ( - - } - description={item.description} - /> - - - - - ))} - -
    - - - ); - }, -}; - -export const WithPaginationControlled: Story = { - render: () => { - const [offset, setOffset] = useState(0); - const [pageSize, setPageSize] = useState(5); - - const { data, paginationProps } = useTable({ - data: data4, - pagination: { - offset, - pageSize, - onOffsetChange: setOffset, - onPageSizeChange: setPageSize, - onNextPage: () => console.log('Next page analytics'), - onPreviousPage: () => console.log('Previous page analytics'), - }, - }); - - return ( - <> - - - Band name - Genre - Year formed - Albums - - - {data?.map(item => ( - - - - - - - ))} - -
    - -
    - Current state: offset={offset}, pageSize={pageSize} -
    - - ); - }, -}; - -export const Sorting: Story = { - render: () => { - return ( - - - - Name - - Owner - Type - Lifecycle - - - {data1.map(item => ( - - } - description={item.description} - /> - - - - - ))} - -
    - ); - }, -}; - -export const TableRockBand: Story = { - render: () => { - const { data, paginationProps } = useTable({ - data: data4, - pagination: { - defaultPageSize: 5, - }, - }); - - return ( - <> - - - Band name - Genre - Year formed - Albums - - - {data?.map(item => ( - - - - - - - ))} - -
    - - - ); - }, -}; - -export const RowClick: Story = { - render: () => { - const { data, paginationProps } = useTable({ - data: data4, - pagination: { - defaultPageSize: 5, - }, - }); - - return ( - <> - - - Band name - Genre - Year formed - Albums - - - {data?.map(item => ( - alert('Row clicked')}> - - - - - - ))} - -
    - - - ); - }, -}; - -export const RowLink: Story = { - render: () => { - const { data, paginationProps } = useTable({ - data: data4, - pagination: { - defaultPageSize: 5, - }, - }); - - return ( - <> - - - Band name - Genre - Year formed - Albums - - - {data?.map(item => ( - - - - - - - ))} - -
    - - - ); - }, -}; - -export const CellText: Story = { - render: () => { - return ( - - - Name - - - {data2.map(item => ( - - - - ))} - -
    - ); - }, -}; - -export const CellProfile: Story = { - render: () => { - return ( - - - Name - - - {data3.map(item => ( - - - - ))} - -
    - ); - }, -}; diff --git a/packages/ui/src/components/Table/components/Cell.tsx b/packages/ui/src/components/Table/components/Cell.tsx index ad0a92cb94..809b224a07 100644 --- a/packages/ui/src/components/Table/components/Cell.tsx +++ b/packages/ui/src/components/Table/components/Cell.tsx @@ -15,62 +15,26 @@ */ import clsx from 'clsx'; -import { Text } from '../../Text'; -import { Link } from '../../Link'; import { Cell as ReactAriaCell } from 'react-aria-components'; import type { CellProps } from '../types'; import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; import styles from '../Table.module.css'; /** @public */ const Cell = (props: CellProps) => { - const { classNames, cleanedProps } = useStyles<'Table', CellProps>('Table', { - color: 'primary', + const { classNames, cleanedProps } = useStyles(TableDefinition, { + color: 'primary' as const, ...props, }); - const { className, title, description, color, leadingIcon, href, ...rest } = - cleanedProps; + const { className, children, ...rest } = cleanedProps; return ( -
    - {leadingIcon && ( -
    - {leadingIcon} -
    - )} -
    - {href ? ( - - {title} - - ) : ( - - {title} - - )} - {description && ( - - {description} - - )} -
    -
    + {children}
    ); }; diff --git a/packages/ui/src/components/Table/components/CellProfile.tsx b/packages/ui/src/components/Table/components/CellProfile.tsx index 89ca54056a..4abef56e01 100644 --- a/packages/ui/src/components/Table/components/CellProfile.tsx +++ b/packages/ui/src/components/Table/components/CellProfile.tsx @@ -18,20 +18,18 @@ import clsx from 'clsx'; import { CellProfileProps } from '../types'; import { Text } from '../../Text/Text'; import { Link } from '../../Link/Link'; -import { Avatar } from '@base-ui-components/react/avatar'; +import { Avatar } from '../../Avatar'; import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; import { Cell as ReactAriaCell } from 'react-aria-components'; import styles from '../Table.module.css'; /** @public */ export const CellProfile = (props: CellProfileProps) => { - const { classNames, cleanedProps } = useStyles<'Table', CellProfileProps>( - 'Table', - { - color: 'primary', - ...props, - }, - ); + const { classNames, cleanedProps } = useStyles(TableDefinition, { + color: 'primary' as const, + ...props, + }); const { className, src, name, href, description, color, ...rest } = cleanedProps; @@ -46,39 +44,9 @@ export const CellProfile = (props: CellProfileProps) => { styles[classNames.cellContentWrapper], )} > -
    - {src && ( - - - - {(name || '') - .split(' ') - .map(word => word[0]) - .join('') - .toLocaleUpperCase('en-US') - .slice(0, 1)} - - - )} -
    + {src && name && ( + + )}
    { + const { classNames, cleanedProps } = useStyles(TableDefinition, { + color: 'primary' as const, + ...props, + }); + const { className, title, description, color, leadingIcon, href, ...rest } = + cleanedProps; + + return ( + +
    + {leadingIcon && ( +
    + {leadingIcon} +
    + )} +
    + {href ? ( + + {title} + + ) : ( + + {title} + + )} + {description && ( + + {description} + + )} +
    +
    +
    + ); +}; + +CellText.displayName = 'CellText'; + +export { CellText }; diff --git a/packages/ui/src/components/Table/components/Column.tsx b/packages/ui/src/components/Table/components/Column.tsx index 6ebd263d3f..473f95a40e 100644 --- a/packages/ui/src/components/Table/components/Column.tsx +++ b/packages/ui/src/components/Table/components/Column.tsx @@ -16,25 +16,23 @@ import { Column as ReactAriaColumn } from 'react-aria-components'; import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; import styles from '../Table.module.css'; import clsx from 'clsx'; import { ColumnProps } from '../types'; -import { RiArrowUpLine, RiArrowDownLine } from '@remixicon/react'; +import { RiArrowUpLine } from '@remixicon/react'; /** @public */ export const Column = (props: ColumnProps) => { - const { classNames, cleanedProps } = useStyles<'Table', ColumnProps>( - 'Table', - props, - ); - const { children, ...rest } = cleanedProps; + const { classNames, cleanedProps } = useStyles(TableDefinition, props); + const { className, children, ...rest } = cleanedProps; return ( - {({ allowsSorting, sortDirection }) => ( + {({ allowsSorting }) => (
    { styles[classNames.headSortButton], )} > - {sortDirection === 'ascending' ? ( - - ) : ( - - )} + )}
    diff --git a/packages/ui/src/components/Table/components/Row.tsx b/packages/ui/src/components/Table/components/Row.tsx index 8b51e4024f..6f7a4ce108 100644 --- a/packages/ui/src/components/Table/components/Row.tsx +++ b/packages/ui/src/components/Table/components/Row.tsx @@ -18,45 +18,55 @@ import { Row as ReactAriaRow, RowProps, useTableOptions, - Cell, + Cell as ReactAriaCell, Collection, - Checkbox, RouterProvider, } from 'react-aria-components'; +import { Checkbox } from '../../Checkbox'; import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; import { useNavigate } from 'react-router-dom'; import { useHref } from 'react-router-dom'; import { isExternalLink } from '../../../utils/isExternalLink'; import styles from '../Table.module.css'; import clsx from 'clsx'; +import { Flex } from '../../Flex'; /** @public */ export function Row(props: RowProps) { - const { classNames, cleanedProps } = useStyles<'Table', RowProps>( - 'Table', - props, - ); + const { classNames, cleanedProps } = useStyles(TableDefinition, props); const { id, columns, children, href, ...rest } = cleanedProps; const navigate = useNavigate(); const isExternal = isExternalLink(href); - let { selectionBehavior } = useTableOptions(); + let { selectionBehavior, selectionMode } = useTableOptions(); const content = ( <> - {selectionBehavior === 'toggle' && ( - - - + {selectionBehavior === 'toggle' && selectionMode === 'multiple' && ( + + + + <> + + + )} {children} ); - if (isExternal) { + if (!href || isExternal) { return ( @@ -69,6 +79,7 @@ export function Row(props: RowProps) { ( + rowConfig: RowConfig | RowRenderFn | undefined, +): rowConfig is RowRenderFn { + return typeof rowConfig === 'function'; +} + +function useDisabledRows({ + data, + rowConfig, +}: Pick, 'data' | 'rowConfig'>): Set | undefined { + return useMemo(() => { + if (!data || typeof rowConfig === 'function' || !rowConfig?.getIsDisabled) { + return; + } + + return data.reduce>((set, item) => { + const isDisabled = rowConfig.getIsDisabled?.(item); + if (isDisabled) { + set.add(String(item.id)); + } + return set; + }, new Set()); + }, [data, rowConfig]); +} + +function useLiveRegionLabel( + pagination: TablePaginationType, + isStale: boolean, + hasData: boolean, +): string { + if (!hasData || pagination.type === 'none') { + return ''; + } + + const { pageSize, offset, totalCount, getLabel } = pagination; + + if (isStale) { + return 'Loading table data.'; + } + + let liveRegionLabel = 'Table page loaded. '; + + if (getLabel) { + liveRegionLabel += getLabel({ pageSize, offset, totalCount }); + } else if (offset !== undefined) { + const fromCount = offset + 1; + const toCount = Math.min(offset + pageSize, totalCount ?? 0); + liveRegionLabel += `Showing ${fromCount} to ${toCount} of ${totalCount}`; + } + return liveRegionLabel; +} /** @public */ -export const Table = (props: TableProps) => { - const { classNames, cleanedProps } = useStyles<'Table', TableProps>( - 'Table', - props, +export function Table({ + columnConfig, + data, + loading = false, + isStale = false, + error, + pagination, + sort, + rowConfig, + selection, + emptyState, + className, + style, +}: TableProps) { + const liveRegionId = useId(); + + const visibleColumns = useMemo( + () => columnConfig.filter(col => !col.isHidden), + [columnConfig], + ); + const disabledRows = useDisabledRows({ data, rowConfig }); + + const { + mode: selectionMode, + selected: selectedKeys, + behavior: selectionBehavior, + onSelectionChange, + } = selection || {}; + + if (loading && !data) { + return ( +
    + Loading... +
    + ); + } + + if (error) { + return ( +
    + Error: {error.message} +
    + ); + } + + const liveRegionLabel = useLiveRegionLabel( + pagination, + isStale, + data !== undefined, ); return ( - +
    + + {liveRegionLabel} + + + + + + {column => + column.header ? ( + <>{column.header()} + ) : ( + + {column.label} + + ) + } + + {emptyState} : undefined + } + > + {item => { + const itemIndex = data?.indexOf(item) ?? -1; + + if (isRowRenderFn(rowConfig)) { + return rowConfig({ + item, + index: itemIndex, + }); + } + + return ( + rowConfig?.onClick?.(item) + : undefined + } + > + {column => ( + {column.cell(item)} + )} + + ); + }} + + + + {pagination.type === 'page' && ( + + )} +
    ); -}; +} diff --git a/packages/ui/src/components/Table/components/TableBody.tsx b/packages/ui/src/components/Table/components/TableBody.tsx index 7c858c4f32..960d37bf84 100644 --- a/packages/ui/src/components/Table/components/TableBody.tsx +++ b/packages/ui/src/components/Table/components/TableBody.tsx @@ -19,15 +19,13 @@ import { type TableBodyProps, } from 'react-aria-components'; import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; import styles from '../Table.module.css'; import clsx from 'clsx'; /** @public */ export const TableBody = (props: TableBodyProps) => { - const { classNames, cleanedProps } = useStyles<'Table', TableBodyProps>( - 'Table', - props, - ); + const { classNames, cleanedProps } = useStyles(TableDefinition, props); return ( (props: TableHeaderProps) => { - let { selectionBehavior, selectionMode, allowsDragging } = useTableOptions(); + let { selectionBehavior, selectionMode } = useTableOptions(); - const { classNames, cleanedProps } = useStyles<'Table', TableHeaderProps>( - 'Table', - props, - ); + const { classNames, cleanedProps } = useStyles(TableDefinition, props); const { columns, children, ...rest } = cleanedProps; return ( @@ -40,11 +40,21 @@ export const TableHeader = (props: TableHeaderProps) => { className={clsx(classNames.header, styles[classNames.header])} {...rest} > - {/* Add extra columns for drag and drop and selection. */} - {allowsDragging && } - {selectionBehavior === 'toggle' && ( - - {selectionMode === 'multiple' && } + {selectionBehavior === 'toggle' && selectionMode === 'multiple' && ( + + + + <> + + )} {children} diff --git a/packages/ui/src/components/Table/components/TableRoot.tsx b/packages/ui/src/components/Table/components/TableRoot.tsx new file mode 100644 index 0000000000..95e80dcf51 --- /dev/null +++ b/packages/ui/src/components/Table/components/TableRoot.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useStyles } from '../../../hooks/useStyles'; +import { TableDefinition } from '../definition'; +import { Table as ReactAriaTable } from 'react-aria-components'; +import styles from '../Table.module.css'; +import clsx from 'clsx'; +import { TableRootProps } from '../types'; + +/** @public */ +export const TableRoot = (props: TableRootProps) => { + const { classNames, dataAttributes, cleanedProps } = useStyles( + TableDefinition, + props, + ); + + return ( + + ); +}; diff --git a/packages/ui/src/components/Table/definition.ts b/packages/ui/src/components/Table/definition.ts new file mode 100644 index 0000000000..ca4fca999a --- /dev/null +++ b/packages/ui/src/components/Table/definition.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ComponentDefinition } from '../../types'; + +/** + * Component definition for Table + * @public + */ +export const TableDefinition = { + classNames: { + table: 'bui-Table', + header: 'bui-TableHeader', + body: 'bui-TableBody', + row: 'bui-TableRow', + head: 'bui-TableHead', + headContent: 'bui-TableHeadContent', + headSortButton: 'bui-TableHeadSortButton', + caption: 'bui-TableCaption', + cell: 'bui-TableCell', + cellContentWrapper: 'bui-TableCellContentWrapper', + cellContent: 'bui-TableCellContent', + cellIcon: 'bui-TableCellIcon', + cellProfileAvatar: 'bui-TableCellProfileAvatar', + cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage', + cellProfileAvatarFallback: 'bui-TableCellProfileAvatarFallback', + cellProfileName: 'bui-TableCellProfileName', + cellProfileLink: 'bui-TableCellProfileLink', + headSelection: 'bui-TableHeadSelection', + cellSelection: 'bui-TableCellSelection', + }, + dataAttributes: { + stale: [true, false] as const, + }, +} as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Table/hooks/getEffectivePageSize.ts b/packages/ui/src/components/Table/hooks/getEffectivePageSize.ts new file mode 100644 index 0000000000..41b246da9f --- /dev/null +++ b/packages/ui/src/components/Table/hooks/getEffectivePageSize.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { PageSizeOption } from '../../TablePagination/types'; +import type { PaginationOptions } from './types'; + +const DEFAULT_PAGE_SIZE = 20; + +function getOptionValue(option: number | PageSizeOption): number { + return typeof option === 'number' ? option : option.value; +} + +/** @internal */ +export function getEffectivePageSize( + paginationOptions: PaginationOptions, +): number { + const { pageSize, pageSizeOptions } = paginationOptions; + + if (!pageSizeOptions) { + return pageSize ?? DEFAULT_PAGE_SIZE; + } + + const firstValue = getOptionValue(pageSizeOptions[0]); + + if (pageSize === undefined) { + return firstValue; + } + + const isValid = pageSizeOptions.some(opt => getOptionValue(opt) === pageSize); + + if (isValid) { + return pageSize; + } + + console.warn( + `useTable: pageSize ${pageSize} is not in pageSizeOptions, using ${firstValue} instead`, + ); + return firstValue; +} diff --git a/packages/ui/src/components/Table/hooks/types.ts b/packages/ui/src/components/Table/hooks/types.ts index 8c93d678fc..f22efd4b53 100644 --- a/packages/ui/src/components/Table/hooks/types.ts +++ b/packages/ui/src/components/Table/hooks/types.ts @@ -15,73 +15,153 @@ */ import type { TablePaginationProps } from '../../TablePagination/types'; +import type { SortDescriptor, TableItem, TableProps } from '../types'; /** @public */ -export interface UseTablePaginationConfig { - /** Total number of rows in the dataset - only needed when data is not provided at the top level */ - rowCount?: number; - - // Controlled pagination with offset/pageSize (Backstage style) - /** Current offset. When provided, pagination is controlled */ - offset?: number; - /** Current page size. When provided, pagination is controlled */ - pageSize?: number; - /** Callback when offset changes */ - onOffsetChange?: (offset: number) => void; - /** Callback when page size changes */ - onPageSizeChange?: (pageSize: number) => void; - - // Uncontrolled pagination defaults - /** Default page size for uncontrolled mode */ - defaultPageSize?: number; - /** Default offset for uncontrolled mode */ - defaultOffset?: number; - - // Analytics callbacks - /** Callback when next page is clicked */ - onNextPage?: () => void; - /** Callback when previous page is clicked */ - onPreviousPage?: () => void; - - // UI options - /** Whether to show page size options */ - showPageSizeOptions?: boolean; +export interface FilterState { + value: TFilter | undefined; + onChange: (value: TFilter) => void; } /** @public */ -export interface UseTablePagination { - /** Props to pass to TablePagination component */ - paginationProps: TablePaginationProps; - /** Current offset */ +export interface SearchState { + value: string; + onChange: (value: string) => void; +} + +/** @public */ +export interface QueryOptions { + initialSort?: SortDescriptor; + sort?: SortDescriptor | null; + onSortChange?: (sort: SortDescriptor) => void; + + initialFilter?: TFilter; + filter?: TFilter; + onFilterChange?: (filter: TFilter) => void; + + initialSearch?: string; + search?: string; + onSearchChange?: (search: string) => void; +} + +/** @public */ +export interface PaginationOptions + extends Partial< + Pick< + TablePaginationProps, + | 'pageSize' + | 'pageSizeOptions' + | 'onPageSizeChange' + | 'onNextPage' + | 'onPreviousPage' + | 'showPageSizeOptions' + | 'getLabel' + > + > { + initialOffset?: number; +} + +/** @public */ +export interface OffsetParams { offset: number; - /** Current page size */ pageSize: number; - /** Sliced data for current page - only available when data is provided to useTable */ - data?: T[]; - /** Go to next page */ - nextPage: () => void; - /** Go to previous page */ - previousPage: () => void; - /** Set specific offset */ - setOffset: (offset: number) => void; - /** Set page size */ - setPageSize: (pageSize: number) => void; + sort: SortDescriptor | null; + filter: TFilter | undefined; + search: string; + signal: AbortSignal; } /** @public */ -export interface UseTableConfig { - /** Full dataset - when provided, rowCount is calculated automatically and sliced data is returned */ - data?: T[]; - /** Pagination configuration */ - pagination?: UseTablePaginationConfig; +export interface CursorParams { + cursor: string | undefined; + pageSize: number; + sort: SortDescriptor | null; + filter: TFilter | undefined; + search: string; + signal: AbortSignal; } /** @public */ -export interface UseTableResult { - /** Sliced data for current page */ - data?: T[]; - /** Props to pass to TablePagination component */ - paginationProps: TablePaginationProps; - /** Pagination utilities */ - pagination: UseTablePagination; +export interface OffsetResponse { + data: T[]; + totalCount: number; +} + +/** @public */ +export interface CursorResponse { + data: T[]; + nextCursor?: string; + prevCursor?: string; + totalCount?: number; +} + +/** @public */ +export interface UseTableCompleteOptions + extends QueryOptions { + mode: 'complete'; + getData: () => T[] | Promise; + paginationOptions?: PaginationOptions; + sortFn?: (data: T[], sort: SortDescriptor) => T[]; + filterFn?: (data: T[], filter: TFilter) => T[]; + searchFn?: (data: T[], search: string) => T[]; +} + +/** @public */ +export interface UseTableOffsetOptions + extends QueryOptions { + mode: 'offset'; + getData: (params: OffsetParams) => Promise>; + paginationOptions?: PaginationOptions; +} + +/** @public */ +export interface UseTableCursorOptions + extends QueryOptions { + mode: 'cursor'; + getData: (params: CursorParams) => Promise>; + paginationOptions?: Omit; +} + +/** @public */ +export type UseTableOptions = + | UseTableCompleteOptions + | UseTableOffsetOptions + | UseTableCursorOptions; + +/** @public */ +export interface UseTableResult { + tableProps: Omit< + TableProps, + 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState' + >; + reload: () => void; + filter: FilterState; + search: SearchState; +} + +/** @internal */ +export interface PaginationResult { + data: T[] | undefined; + loading: boolean; + error: Error | undefined; + totalCount: number | undefined; + offset?: number; + pageSize: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + onNextPage: () => void; + onPreviousPage: () => void; + onPageSizeChange: (size: number) => void; +} + +/** @internal */ +export interface QueryState { + sort: SortDescriptor | null; + setSort: (sort: SortDescriptor) => void; + + filter: TFilter | undefined; + setFilter: (filter: TFilter) => void; + + search: string; + setSearch: (search: string) => void; } diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts new file mode 100644 index 0000000000..70db68dae1 --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; +import type { TableItem } from '../types'; +import type { + PaginationResult, + QueryState, + UseTableCompleteOptions, +} from './types'; +import { useStableCallback } from './useStableCallback'; +import { getEffectivePageSize } from './getEffectivePageSize'; + +/** @internal */ +export function useCompletePagination( + options: UseTableCompleteOptions, + query: QueryState, +): PaginationResult & { reload: () => void } { + const { + getData: getDataProp, + paginationOptions = {}, + sortFn, + filterFn, + searchFn, + } = options; + const { initialOffset = 0 } = paginationOptions; + const defaultPageSize = getEffectivePageSize(paginationOptions); + + const getData = useStableCallback(getDataProp); + const { sort, filter, search } = query; + + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(undefined); + const [loadCount, setLoadCount] = useState(0); + + const [offset, setOffset] = useState(initialOffset); + const [pageSize, setPageSize] = useState(defaultPageSize); + + // Load data on mount and when loadCount changes (reload trigger) + useEffect(() => { + let cancelled = false; + setIsLoading(true); + setError(undefined); + + (async () => { + try { + const result = getData(); + const data = result instanceof Promise ? await result : result; + if (!cancelled) { + setItems(data); + setIsLoading(false); + } + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err : new Error(String(err))); + setIsLoading(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [getData, loadCount]); + + // Reset offset when query changes (query object is memoized) + const prevQueryRef = useRef(query); + useEffect(() => { + if (prevQueryRef.current !== query) { + prevQueryRef.current = query; + setOffset(0); + } + }, [query]); + + // Process data client-side (filter, search, sort) + const processedData = useMemo(() => { + let result = [...items]; + if (filter !== undefined && filterFn) { + result = filterFn(result, filter); + } + if (search && searchFn) { + result = searchFn(result, search); + } + if (sort && sortFn) { + result = sortFn(result, sort); + } + return result; + }, [items, sort, filter, search, filterFn, searchFn, sortFn]); + + const totalCount = processedData.length; + + // Paginate the processed data + const paginatedData = useMemo( + () => processedData.slice(offset, offset + pageSize), + [processedData, offset, pageSize], + ); + + const hasNextPage = offset + pageSize < totalCount; + const hasPreviousPage = offset > 0; + + const onNextPage = useCallback(() => { + if (offset + pageSize < totalCount) { + setOffset(offset + pageSize); + } + }, [offset, pageSize, totalCount]); + + const onPreviousPage = useCallback(() => { + if (offset > 0) { + setOffset(Math.max(0, offset - pageSize)); + } + }, [offset, pageSize]); + + const onPageSizeChange = useCallback((newSize: number) => { + setPageSize(newSize); + setOffset(0); + }, []); + + const reload = useCallback(() => { + setOffset(0); + setLoadCount(c => c + 1); + }, []); + + return { + data: paginatedData, + loading: isLoading, + error, + totalCount, + offset, + pageSize, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + onPageSizeChange, + reload, + }; +} diff --git a/packages/ui/src/components/Table/hooks/useCursorPagination.ts b/packages/ui/src/components/Table/hooks/useCursorPagination.ts new file mode 100644 index 0000000000..5da56b3ce4 --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useCursorPagination.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useCallback } from 'react'; +import type { TableItem } from '../types'; +import type { + UseTableCursorOptions, + CursorParams, + QueryState, + PaginationResult, +} from './types'; +import { usePageCache } from './usePageCache'; +import { useStableCallback } from './useStableCallback'; +import { useDebouncedReload } from './useDebouncedReload'; +import { getEffectivePageSize } from './getEffectivePageSize'; + +export function useCursorPagination( + options: UseTableCursorOptions, + query: QueryState, +): PaginationResult & { reload: () => void } { + const { getData: getDataProp, paginationOptions = {} } = options; + const defaultPageSize = getEffectivePageSize(paginationOptions); + + const getData = useStableCallback(getDataProp); + const { sort, filter, search } = query; + + const [pageSize, setPageSize] = useState(defaultPageSize); + + const wrappedGetData = useCallback( + async ({ + cursor, + signal, + }: { + cursor: string | undefined; + signal: AbortSignal; + }) => { + const params: CursorParams = { + cursor, + pageSize, + sort, + filter, + search, + signal, + }; + + const response = await getData(params); + + return { + data: response.data, + prevCursor: response.prevCursor, + nextCursor: response.nextCursor, + totalCount: response.totalCount, + }; + }, + [getData, pageSize, sort, filter, search], + ); + + const cache = usePageCache({ getData: wrappedGetData }); + + useDebouncedReload(query, pageSize, cache.reload); + + const onPageSizeChange = useCallback( + (newSize: number) => setPageSize(newSize), + [], + ); + + return { + data: cache.data, + loading: cache.loading, + error: cache.error, + totalCount: cache.totalCount, + offset: undefined, + pageSize, + hasNextPage: cache.hasNextPage, + hasPreviousPage: cache.hasPreviousPage, + onNextPage: cache.onNextPage, + onPreviousPage: cache.onPreviousPage, + onPageSizeChange, + reload: cache.reload, + }; +} diff --git a/packages/ui/src/components/Table/hooks/useDebouncedReload.ts b/packages/ui/src/components/Table/hooks/useDebouncedReload.ts new file mode 100644 index 0000000000..d90afa8861 --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useDebouncedReload.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, useRef } from 'react'; +import type { QueryState } from './types'; + +/** + * Triggers a debounced reload when query or pageSize changes. + * Debouncing reduces backend load during rapid changes (e.g., typing in search). + */ +/** @internal */ +export function useDebouncedReload( + query: QueryState, + pageSize: number, + reload: () => void, + delay: number = 200, +): void { + const prevDepsRef = useRef({ query, pageSize }); + + useEffect(() => { + const prev = prevDepsRef.current; + if (prev.query !== query || prev.pageSize !== pageSize) { + prevDepsRef.current = { query, pageSize }; + const timer = setTimeout(reload, delay); + return () => clearTimeout(timer); + } + return undefined; + }, [query, pageSize, reload, delay]); +} diff --git a/packages/ui/src/components/Table/hooks/useOffsetPagination.ts b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts new file mode 100644 index 0000000000..b34dbe8a6c --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useCallback } from 'react'; +import type { TableItem } from '../types'; +import type { + UseTableOffsetOptions, + OffsetParams, + QueryState, + PaginationResult, +} from './types'; +import { usePageCache } from './usePageCache'; +import { useStableCallback } from './useStableCallback'; +import { useDebouncedReload } from './useDebouncedReload'; +import { getEffectivePageSize } from './getEffectivePageSize'; + +export function useOffsetPagination( + options: UseTableOffsetOptions, + query: QueryState, +): PaginationResult & { reload: () => void } { + const { getData: getDataProp, paginationOptions = {} } = options; + const { initialOffset = 0 } = paginationOptions; + const defaultPageSize = getEffectivePageSize(paginationOptions); + + const getData = useStableCallback(getDataProp); + const { sort, filter, search } = query; + + const [pageSize, setPageSize] = useState(defaultPageSize); + + const wrappedGetData = useCallback( + async ({ + cursor, + signal, + }: { + cursor: number | undefined; + signal: AbortSignal; + }) => { + const currentOffset = cursor ?? 0; + + const params: OffsetParams = { + offset: currentOffset, + pageSize, + sort, + filter, + search, + signal, + }; + + const response = await getData(params); + + const prevCursor = + currentOffset > 0 ? Math.max(0, currentOffset - pageSize) : undefined; + const nextCursor = + currentOffset + pageSize < response.totalCount + ? currentOffset + pageSize + : undefined; + + return { + data: response.data, + prevCursor, + nextCursor, + totalCount: response.totalCount, + }; + }, + [getData, pageSize, sort, filter, search], + ); + + const cache = usePageCache({ + getData: wrappedGetData, + initialCurrentCursor: initialOffset > 0 ? initialOffset : undefined, + }); + + useDebouncedReload(query, pageSize, cache.reload); + + const onPageSizeChange = useCallback( + (newSize: number) => setPageSize(newSize), + [], + ); + + return { + data: cache.data, + loading: cache.loading, + error: cache.error, + totalCount: cache.totalCount, + offset: cache.currentCursor ?? 0, + pageSize, + hasNextPage: cache.hasNextPage, + hasPreviousPage: cache.hasPreviousPage, + onNextPage: cache.onNextPage, + onPreviousPage: cache.onPreviousPage, + onPageSizeChange, + reload: cache.reload, + }; +} diff --git a/packages/ui/src/components/Table/hooks/usePageCache.ts b/packages/ui/src/components/Table/hooks/usePageCache.ts new file mode 100644 index 0000000000..9bf864749b --- /dev/null +++ b/packages/ui/src/components/Table/hooks/usePageCache.ts @@ -0,0 +1,280 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, useCallback, useRef, useEffect } from 'react'; + +const FIRST_PAGE_CURSOR = Symbol('firstPage'); + +type CursorType = string | number; + +type InternalCursor = + | TCursor + | typeof FIRST_PAGE_CURSOR; + +interface PageEntry { + data: T[] | undefined; + nextCursor: InternalCursor | undefined; + prevCursor: InternalCursor | undefined; +} + +interface GetDataResult { + data: T[]; + nextCursor?: TCursor; + prevCursor?: TCursor; + totalCount?: number; +} + +/** @internal */ +export interface UsePageCacheOptions { + getData: (params: { + cursor: TCursor | undefined; + signal: AbortSignal; + }) => Promise>; + initialCurrentCursor?: TCursor; +} + +/** @internal */ +export interface UsePageCacheResult { + loading: boolean; + error: Error | undefined; + data: T[] | undefined; + totalCount: number | undefined; + currentCursor: TCursor | undefined; + hasPreviousPage: boolean; + onPreviousPage: () => void; + hasNextPage: boolean; + onNextPage: () => void; + reload: (options?: { keepCurrentCursor?: boolean }) => void; +} + +type Direction = 'mount' | 'reset' | 'refresh' | 'next' | 'prev'; + +class PageCacheStore { + private cache = new Map, PageEntry>(); + + get(cursor: InternalCursor): PageEntry | undefined { + return this.cache.get(cursor); + } + + getOrCreate(cursor: InternalCursor): PageEntry { + const existing = this.cache.get(cursor); + if (existing) { + return existing; + } + const entry: PageEntry = { + data: undefined, + nextCursor: undefined, + prevCursor: undefined, + }; + this.cache.set(cursor, entry); + return entry; + } + + clear() { + this.cache.clear(); + } + + getTargetCursor( + direction: Direction, + currentCursor: InternalCursor, + initialCurrentCursor: TCursor | undefined, + ): InternalCursor | undefined { + if (direction === 'mount') { + return toInternalCursor(initialCurrentCursor); + } + if (direction === 'reset') { + return FIRST_PAGE_CURSOR; + } + if (direction === 'refresh') { + return currentCursor; + } + const currentEntry = this.cache.get(currentCursor); + + if (!currentEntry) { + return; + } + + return direction === 'next' + ? currentEntry.nextCursor + : currentEntry.prevCursor; + } + + linkEntryToSource( + entry: PageEntry, + direction: Direction, + currentCursor: InternalCursor, + ) { + if (direction === 'next') { + entry.prevCursor = currentCursor; + } else if (direction === 'prev') { + entry.nextCursor = currentCursor; + } + } +} + +function toInternalCursor( + cursor: TCursor | undefined, +): InternalCursor { + return cursor === undefined ? FIRST_PAGE_CURSOR : cursor; +} + +function toExternalCursor( + cursor: InternalCursor, +): TCursor | undefined { + return cursor === FIRST_PAGE_CURSOR ? undefined : cursor; +} + +/** @internal */ +export function usePageCache( + options: UsePageCacheOptions, +): UsePageCacheResult { + const { getData, initialCurrentCursor } = options; + + const [currentCursor, setCurrentCursor] = useState>( + () => toInternalCursor(initialCurrentCursor), + ); + + const cacheStore = useRef(new PageCacheStore()).current; + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const [totalCount, setTotalCount] = useState(undefined); + + const abortControllerRef = useRef(null); + + const currentPage = cacheStore.get(currentCursor); + const data = currentPage?.data; + const hasNextPage = currentPage?.nextCursor !== undefined; + const hasPreviousPage = currentPage?.prevCursor !== undefined; + + const goToPage = useCallback( + async (direction: Direction) => { + const targetCursor = cacheStore.getTargetCursor( + direction, + currentCursor, + initialCurrentCursor, + ); + + if (!targetCursor) { + return; + } + + const existingEntry = cacheStore.get(targetCursor); + if (existingEntry?.data !== undefined) { + setCurrentCursor(targetCursor); + return; + } + + const entry = cacheStore.getOrCreate(targetCursor); + cacheStore.linkEntryToSource(entry, direction, currentCursor); + setCurrentCursor(targetCursor); + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setLoading(true); + setError(undefined); + + try { + const result = await getData({ + cursor: toExternalCursor(targetCursor), + signal: abortController.signal, + }); + + if (abortController.signal.aborted) { + return; + } + + entry.data = result.data; + + if (entry.nextCursor === undefined && result.nextCursor !== undefined) { + entry.nextCursor = result.nextCursor; + } + if (entry.prevCursor === undefined && result.prevCursor !== undefined) { + entry.prevCursor = result.prevCursor; + } + + if (result.totalCount !== undefined) { + setTotalCount(result.totalCount); + } + + setLoading(false); + } catch (err) { + if (abortController.signal.aborted) { + return; + } + + setError(err instanceof Error ? err : new Error(String(err))); + setLoading(false); + } + }, + [getData, initialCurrentCursor, currentCursor, cacheStore], + ); + + useEffect(() => { + goToPage('mount'); + + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + const onNextPage = useCallback(() => { + if (loading) return; + const page = cacheStore.get(currentCursor); + if (!page?.nextCursor) return; + goToPage('next'); + }, [loading, currentCursor, goToPage, cacheStore]); + + const onPreviousPage = useCallback(() => { + if (loading) return; + const page = cacheStore.get(currentCursor); + if (!page?.prevCursor) return; + goToPage('prev'); + }, [loading, currentCursor, goToPage, cacheStore]); + + const reload = useCallback( + (reloadOptions?: { keepCurrentCursor?: boolean }) => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + cacheStore.clear(); + + goToPage(reloadOptions?.keepCurrentCursor ? 'refresh' : 'reset'); + }, + [goToPage, cacheStore], + ); + + return { + loading, + error, + data, + totalCount, + currentCursor: toExternalCursor(currentCursor), + hasPreviousPage, + onPreviousPage, + hasNextPage, + onNextPage, + reload, + }; +} diff --git a/packages/ui/src/components/Table/hooks/useQueryState.ts b/packages/ui/src/components/Table/hooks/useQueryState.ts new file mode 100644 index 0000000000..308d9fd385 --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useQueryState.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useMemo, useState, useCallback } from 'react'; +import type { QueryOptions, QueryState } from './types'; + +function useControlledStateHelper( + initialValue: TInitial, + controlledValue: TControlled | undefined, + onChange: ((value: T) => void) | undefined, +) { + const [internalValue, setInternalValue] = useState(initialValue); + + const value = controlledValue !== undefined ? controlledValue : internalValue; + + const setValue = useCallback( + (newValue: T) => { + if (controlledValue === undefined) { + setInternalValue(newValue as unknown as TInitial); + } + if (onChange) { + onChange(newValue); + } + }, + [controlledValue, onChange], + ); + + return [value, setValue] as const; +} + +/** @internal */ +export function useQueryState( + options: QueryOptions, +): QueryState { + const [sort, setSort] = useControlledStateHelper( + options.initialSort ?? null, + options.sort, + options.onSortChange, + ); + const [filter, setFilter] = useControlledStateHelper( + options.initialFilter, + options.filter, + options.onFilterChange, + ); + const [search, setSearch] = useControlledStateHelper( + options.initialSearch ?? '', + options.search, + options.onSearchChange, + ); + + return useMemo( + () => ({ sort, setSort, filter, setFilter, search, setSearch }), + [sort, setSort, filter, setFilter, search, setSearch], + ); +} diff --git a/packages/ui/src/components/Table/hooks/useStableCallback.ts b/packages/ui/src/components/Table/hooks/useStableCallback.ts new file mode 100644 index 0000000000..1764e07dd0 --- /dev/null +++ b/packages/ui/src/components/Table/hooks/useStableCallback.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useRef, useCallback } from 'react'; + +/** + * Returns a stable callback reference that always calls the latest version + * of the provided function. Useful for callbacks passed as props that may + * change on every render but shouldn't trigger effect re-runs. + * + * @internal + */ +export function useStableCallback any>(fn: T): T { + const ref = useRef(fn); + ref.current = fn; + + return useCallback((...args: Parameters) => ref.current(...args), []) as T; +} diff --git a/packages/ui/src/components/Table/hooks/useTable.ts b/packages/ui/src/components/Table/hooks/useTable.ts index 2759fcde41..b8f5b8bd13 100644 --- a/packages/ui/src/components/Table/hooks/useTable.ts +++ b/packages/ui/src/components/Table/hooks/useTable.ts @@ -13,154 +13,147 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { useState, useMemo, useCallback } from 'react'; -import type { TablePaginationProps } from '../../TablePagination/types'; +import { useMemo, useRef } from 'react'; +import type { SortState, TableItem, TableProps } from '../types'; import type { - UseTableConfig, + PaginationOptions, + PaginationResult, + UseTableOptions, UseTableResult, - UseTablePagination, } from './types'; +import { useQueryState } from './useQueryState'; +import { useCompletePagination } from './useCompletePagination'; +import { useCursorPagination } from './useCursorPagination'; +import { useOffsetPagination } from './useOffsetPagination'; -/** - * Hook for managing table state including pagination and future features like sorting. - * Supports both controlled and uncontrolled modes using offset/pageSize pattern (Backstage style). - * - * @public - */ -export function useTable( - config: UseTableConfig = {}, -): UseTableResult { - const { data, pagination: paginationConfig = {} } = config; - +function useTableProps( + paginationResult: PaginationResult, + sortState: SortState, + paginationOptions: PaginationOptions = {}, +): Omit< + TableProps, + 'columnConfig' | 'rowConfig' | 'selection' | 'emptyState' +> { const { - rowCount: providedRowCount, - offset: controlledOffset, - pageSize: controlledPageSize, - onOffsetChange, - onPageSizeChange, - defaultPageSize = 10, - defaultOffset = 0, - onNextPage, - onPreviousPage, showPageSizeOptions = true, - } = paginationConfig; + pageSizeOptions, + onPageSizeChange: onPageSizeChangeCallback, + onNextPage: onNextPageCallback, + onPreviousPage: onPreviousPageCallback, + getLabel, + } = paginationOptions; - // Determine if we're in controlled mode - const isControlled = - controlledOffset !== undefined || controlledPageSize !== undefined; + const previousDataRef = useRef(paginationResult.data); + if (paginationResult.data) { + previousDataRef.current = paginationResult.data; + } - // Calculate row count from data or use provided value - const rowCount = data?.length ?? providedRowCount ?? 0; + const displayData = paginationResult.data ?? previousDataRef.current; + const isStale = paginationResult.loading && displayData !== undefined; - // Internal state for uncontrolled mode - const [internalOffset, setInternalOffset] = useState(defaultOffset); - const [internalPageSize, setInternalPageSize] = useState(defaultPageSize); - - // Calculate current values - const currentOffset = controlledOffset ?? internalOffset; - const currentPageSize = controlledPageSize ?? internalPageSize; - - // Calculate sliced data if data array is provided - const currentData = useMemo(() => { - if (!data) return undefined; - return data.slice(currentOffset, currentOffset + currentPageSize); - }, [data, currentOffset, currentPageSize]); - - // Update functions - const setOffset = useCallback( - (newOffset: number) => { - if (isControlled) { - onOffsetChange?.(newOffset); - } else { - setInternalOffset(newOffset); - } - }, - [isControlled, onOffsetChange], - ); - - const setPageSize = useCallback( - (newPageSize: number) => { - // When changing page size, reset to first page to avoid showing empty results - const newOffset = 0; - - if (isControlled) { - onPageSizeChange?.(newPageSize); - onOffsetChange?.(newOffset); - } else { - setInternalPageSize(newPageSize); - setInternalOffset(newOffset); - } - }, - [isControlled, onPageSizeChange, onOffsetChange], - ); - - const nextPage = useCallback(() => { - const nextOffset = currentOffset + currentPageSize; - if (nextOffset < rowCount) { - onNextPage?.(); - setOffset(nextOffset); - } - }, [currentOffset, currentPageSize, rowCount, onNextPage, setOffset]); - - const previousPage = useCallback(() => { - if (currentOffset > 0) { - onPreviousPage?.(); - const prevOffset = Math.max(0, currentOffset - currentPageSize); - setOffset(prevOffset); - } - }, [currentOffset, currentPageSize, onPreviousPage, setOffset]); - - // Pagination props for TablePagination component - const paginationProps: TablePaginationProps = useMemo( + const pagination = useMemo( () => ({ - offset: currentOffset, - pageSize: currentPageSize, - rowCount, - setOffset, - setPageSize, - onNextPage, - onPreviousPage, + type: 'page' as const, + pageSize: paginationResult.pageSize, + pageSizeOptions, + offset: paginationResult.offset, + totalCount: paginationResult.totalCount, + hasNextPage: paginationResult.hasNextPage, + hasPreviousPage: paginationResult.hasPreviousPage, + onNextPage: () => { + paginationResult.onNextPage(); + onNextPageCallback?.(); + }, + onPreviousPage: () => { + paginationResult.onPreviousPage(); + onPreviousPageCallback?.(); + }, + onPageSizeChange: (size: number) => { + paginationResult.onPageSizeChange(size); + onPageSizeChangeCallback?.(size); + }, showPageSizeOptions, + getLabel, }), [ - currentOffset, - currentPageSize, - rowCount, - setOffset, - setPageSize, - onNextPage, - onPreviousPage, - showPageSizeOptions, + paginationResult.pageSize, + pageSizeOptions, + paginationResult.offset, + paginationResult.totalCount, + paginationResult.hasNextPage, + paginationResult.hasPreviousPage, + paginationResult.onNextPage, + paginationResult.onPreviousPage, + paginationResult.onPageSizeChange, + onNextPageCallback, + onPreviousPageCallback, + onPageSizeChangeCallback, ], ); - const pagination: UseTablePagination = useMemo( + return useMemo( () => ({ - paginationProps, - offset: currentOffset, - pageSize: currentPageSize, - data: currentData, - nextPage, - previousPage, - setOffset, - setPageSize, + data: displayData, + loading: paginationResult.loading, + isStale, + error: paginationResult.error, + pagination, + sort: sortState, }), [ - paginationProps, - currentOffset, - currentPageSize, - currentData, - nextPage, - previousPage, - setOffset, - setPageSize, + displayData, + paginationResult.loading, + isStale, + paginationResult.error, + pagination, + showPageSizeOptions, + getLabel, + sortState, ], ); +} + +/** @public */ +export function useTable( + options: UseTableOptions, +): UseTableResult { + const query = useQueryState(options); + + const initialModeRef = useRef(options.mode); + if (initialModeRef.current !== options.mode) { + throw new Error( + `useTable mode cannot change from '${initialModeRef.current}' to '${options.mode}'. ` + + `The mode must remain stable for the lifetime of the component.`, + ); + } + + let pagination: PaginationResult & { reload: () => void }; + + if (options.mode === 'complete') { + pagination = useCompletePagination(options, query); + } else if (options.mode === 'offset') { + pagination = useOffsetPagination(options, query); + } else if (options.mode === 'cursor') { + pagination = useCursorPagination(options, query); + } else { + throw new Error('Invalid mode'); + } + + const sortState: SortState = useMemo( + () => ({ descriptor: query.sort, onSortChange: query.setSort }), + [query.sort, query.setSort], + ); + + const tableProps = useTableProps( + pagination, + sortState, + options.paginationOptions ?? {}, + ); return { - data: currentData, - paginationProps, - pagination, + tableProps, + reload: pagination.reload, + filter: { value: query.filter, onChange: query.setFilter }, + search: { value: query.search, onChange: query.setSearch }, }; } diff --git a/packages/ui/src/components/Table/index.ts b/packages/ui/src/components/Table/index.ts index fb4ec6147d..0d3d5ff8eb 100644 --- a/packages/ui/src/components/Table/index.ts +++ b/packages/ui/src/components/Table/index.ts @@ -15,18 +15,48 @@ */ export { Table } from './components/Table'; +export { TableRoot } from './components/TableRoot'; export { TableHeader } from './components/TableHeader'; export { TableBody } from './components/TableBody'; export { Column } from './components/Column'; export { Row } from './components/Row'; export { Cell } from './components/Cell'; +export { CellText } from './components/CellText'; export { CellProfile } from './components/CellProfile'; export { useTable } from './hooks/useTable'; -export type { CellProps, CellProfileProps, ColumnProps } from './types'; export type { - UseTableConfig, + CellProps, + CellTextProps, + CellProfileProps, + ColumnProps, + TableProps, + TableRootProps, + TableItem, + ColumnConfig, + RowConfig, + RowRenderFn, + TableSelection, + SortState, + SortDescriptor, + NoPagination, + PagePagination, + TablePaginationType, +} from './types'; +export type { + UseTableOptions, UseTableResult, - UseTablePagination, - UseTablePaginationConfig, + UseTableCompleteOptions, + UseTableOffsetOptions, + UseTableCursorOptions, + OffsetParams, + OffsetResponse, + CursorParams, + CursorResponse, + FilterState, + SearchState, + QueryOptions, + PaginationOptions, } from './hooks/types'; + +export { TableDefinition } from './definition'; diff --git a/packages/ui/src/components/Table/mocked-data2.ts b/packages/ui/src/components/Table/mocked-data2.ts deleted file mode 100644 index 30ca36a689..0000000000 --- a/packages/ui/src/components/Table/mocked-data2.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createElement } from 'react'; -import { RiCactusLine } from '@remixicon/react'; - -export interface DataProps { - name: string; - description?: string; - icon?: React.ReactNode; -} - -export const data: DataProps[] = [ - { - name: 'Cell with title only', - }, - { - name: 'Cell with title and description', - description: - 'A comprehensive service handling user authentication and role-based access control across all applications.', - }, - { - name: 'Cell with title and icon', - icon: createElement(RiCactusLine), - }, - { - name: 'Cell with title, description and icon', - - description: - 'A comprehensive service handling user authentication and role-based access control across all applications.', - icon: createElement(RiCactusLine), - }, -]; diff --git a/packages/ui/src/components/Table/stories/Table.dev.stories.tsx b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx new file mode 100644 index 0000000000..503665ffed --- /dev/null +++ b/packages/ui/src/components/Table/stories/Table.dev.stories.tsx @@ -0,0 +1,967 @@ +/* eslint-disable no-restricted-syntax */ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, Fragment } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { + Table, + TableRoot, + TableHeader, + TableBody, + Column, + Row, + CellText, + CellProfile, + useTable, + type ColumnConfig, +} from '..'; +import { Button } from '../../Button'; +import { Select } from '../../Select'; +import { Flex } from '../../Flex'; +import { data as data1 } from './mocked-data1'; +import { data as data4 } from './mocked-data4'; +import { selectionData, selectionColumns, tableStoriesMeta } from './utils'; +import { SearchField } from '../../SearchField'; + +const meta = { + title: 'Backstage UI/Table/dev', + ...tableStoriesMeta, +} satisfies Meta; + +export default meta; +type Story = StoryObj; +type Data1Item = (typeof data1)[0]; +type Data4Item = (typeof data4)[0]; + +export const BasicLocalData: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + defaultWidth: '4fr', + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + defaultWidth: '1fr', + cell: item => , + }, + { + id: 'type', + label: 'Type', + defaultWidth: '1fr', + cell: item => , + }, + { + id: 'lifecycle', + label: 'Lifecycle', + defaultWidth: '1fr', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + }); + + return ; + }, +}; + +export const Sorting: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + isSortable: true, + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + isSortable: true, + }, + { + id: 'type', + label: 'Type', + cell: item => , + isSortable: true, + }, + { + id: 'lifecycle', + label: 'Lifecycle', + cell: item => , + isSortable: true, + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + initialSort: { column: 'name', direction: 'ascending' }, + sortFn: (items, { column, direction }) => { + return [...items].sort((a, b) => { + let aVal: string; + let bVal: string; + if (column === 'name') { + aVal = a.name; + bVal = b.name; + } else if (column === 'owner') { + aVal = a.owner.name; + bVal = b.owner.name; + } else if (column === 'type') { + aVal = a.type; + bVal = b.type; + } else { + aVal = a.lifecycle; + bVal = b.lifecycle; + } + const cmp = aVal.localeCompare(bVal); + return direction === 'descending' ? -cmp : cmp; + }); + }, + }); + + return
    ; + }, +}; + +export const Search: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + isSortable: true, + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps, search } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + searchFn: (items, query) => { + const lowerQuery = query.toLowerCase(); + return items.filter( + item => + item.name.toLowerCase().includes(lowerQuery) || + item.owner.name.toLowerCase().includes(lowerQuery) || + item.type.toLowerCase().includes(lowerQuery), + ); + }, + }); + + return ( +
    + +
    No results found + ) : ( +
    No data available
    + ) + } + {...tableProps} + /> + + ); + }, +}; + +export const Selection: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    + ); + }, +}; + +export const RowLinks: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Band name', + isRowHeader: true, + cell: item => , + }, + { + id: 'genre', + label: 'Genre', + cell: item => , + }, + { + id: 'yearFormed', + label: 'Year formed', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data4, + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    `/bands/${item.id}` }} + /> + ); + }, +}; + +export const Reload: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps, reload } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    + +
    + + ); + }, +}; + +export const ServerSidePaginationOffset: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'offset', + getData: async ({ offset, pageSize }) => { + await new Promise(resolve => setTimeout(resolve, 500)); + return { + data: data1.slice(offset, offset + pageSize), + totalCount: data1.length, + }; + }, + paginationOptions: { pageSize: 5 }, + }); + + return
    ; + }, +}; + +export const ServerSidePaginationCursor: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Band name', + isRowHeader: true, + cell: item => , + }, + { + id: 'genre', + label: 'Genre', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'cursor', + getData: async ({ cursor, pageSize }) => { + await new Promise(resolve => setTimeout(resolve, 500)); + const startIndex = cursor ? parseInt(cursor, 10) : 0; + const nextIndex = startIndex + pageSize; + return { + data: data4.slice(startIndex, nextIndex), + totalCount: data4.length, + nextCursor: nextIndex < data4.length ? String(nextIndex) : undefined, + prevCursor: + startIndex > 0 + ? String(Math.max(0, startIndex - pageSize)) + : undefined, + }; + }, + paginationOptions: { pageSize: 5 }, + }); + + return
    ; + }, +}; + +export const CustomRowRender: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + { + id: 'lifecycle', + label: 'Lifecycle', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    ( + + {column => ( + + {column.id === 'name' ? ( + + ) : ( + column.cell(item) + )} + + )} + + )} + /> + ); + }, +}; + +export const AtomicComponents: Story = { + render: () => { + const displayData = data1.slice(0, 5); + + return ( + + + Name + Owner + Type + + + {displayData.map(item => ( + + + + + + ))} + + + ); + }, +}; + +export const RowClick: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Band name', + isRowHeader: true, + cell: item => ( + + ), + }, + { + id: 'genre', + label: 'Genre', + cell: item => , + }, + { + id: 'yearFormed', + label: 'Year formed', + cell: item => , + }, + { + id: 'albums', + label: 'Albums', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data4, + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    alert(`Clicked: ${item.name}`) }} + /> + ); + }, +}; + +export const SelectionSingleToggle: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    + ); + }, +}; + +export const SelectionMultiToggle: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    + ); + }, +}; + +export const SelectionWithRowClick: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    alert(`Clicked: ${item.name}`) }} + /> + ); + }, +}; + +export const SelectionWithRowLinks: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    `/items/${item.id}` }} + /> + ); + }, +}; + +export const SelectionWithPagination: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { pageSize: 5 }, + }); + + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + return ( +
    + ); + }, +}; + +export const SelectionSingleReplace: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    + ); + }, +}; + +export const SelectionMultiReplace: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    + ); + }, +}; + +export const SelectionReplaceWithRowClick: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    alert(`Opening ${item.name}`) }} + /> + ); + }, +}; + +export const SelectionReplaceWithRowLinks: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    `/items/${item.id}` }} + /> + ); + }, +}; + +// Type filter interface for ComprehensiveServerSide story +interface TypeFilter { + type: string | null; +} + +/** + * Comprehensive example showcasing a common complex use case: + * - Server-side offset pagination + * - Search/filtering + * - Sorting + * - Multi-selection + * - Type filter dropdown + */ +export const ComprehensiveServerSide: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const typeOptions = [ + { value: '', label: 'All types' }, + { value: 'service', label: 'Service' }, + { value: 'website', label: 'Website' }, + { value: 'library', label: 'Library' }, + { value: 'documentation', label: 'Documentation' }, + { value: 'other', label: 'Other' }, + ]; + + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + isSortable: true, + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + isSortable: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + isSortable: true, + cell: item => , + }, + { + id: 'lifecycle', + label: 'Lifecycle', + isSortable: true, + cell: item => , + }, + ]; + + const { tableProps, search, filter } = useTable({ + mode: 'offset', + initialSort: { column: 'name', direction: 'ascending' }, + getData: async ({ + offset, + pageSize, + sort, + filter: typeFilter, + search: searchQuery, + }) => { + // Simulate server-side filtering, sorting, and pagination + // with slower and slower responses + const page = Math.floor(offset / pageSize) + 1; + await new Promise(resolve => setTimeout(resolve, 300 * page)); + + let filtered = [...data1]; + + // Apply search filter + if (searchQuery) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter( + item => + item.name.toLowerCase().includes(query) || + item.owner.name.toLowerCase().includes(query) || + item.description?.toLowerCase().includes(query), + ); + } + + // Apply type filter + if (typeFilter?.type) { + filtered = filtered.filter(item => item.type === typeFilter.type); + } + + // Apply sorting + if (sort) { + filtered.sort((a, b) => { + let aVal: string; + let bVal: string; + switch (sort.column) { + case 'owner': + aVal = a.owner.name; + bVal = b.owner.name; + break; + case 'type': + aVal = a.type; + bVal = b.type; + break; + case 'lifecycle': + aVal = a.lifecycle; + bVal = b.lifecycle; + break; + default: + aVal = a.name; + bVal = b.name; + } + const cmp = aVal.localeCompare(bVal); + return sort.direction === 'descending' ? -cmp : cmp; + }); + } + + return { + data: filtered.slice(offset, offset + pageSize), + totalCount: filtered.length, + }; + }, + paginationOptions: { pageSize: 10 }, + }); + + return ( + + + +
    No results match your filters + ) : ( +
    No data available
    + ) + } + /> + + ); + }, +}; diff --git a/packages/ui/src/components/Table/stories/Table.docs.stories.tsx b/packages/ui/src/components/Table/stories/Table.docs.stories.tsx new file mode 100644 index 0000000000..aff656b083 --- /dev/null +++ b/packages/ui/src/components/Table/stories/Table.docs.stories.tsx @@ -0,0 +1,202 @@ +/* eslint-disable no-restricted-syntax */ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Table, CellText, CellProfile, useTable, type ColumnConfig } from '..'; +import { Flex } from '../../Flex'; +import { Text } from '../../Text'; +import { RadioGroup, Radio } from '../../RadioGroup'; +import { data as data4 } from './mocked-data4'; +import { selectionData, selectionColumns, tableStoriesMeta } from './utils'; + +const meta = { + title: 'Backstage UI/Table/docs', + ...tableStoriesMeta, +} satisfies Meta; + +export default meta; +type Story = StoryObj; +type Data4Item = (typeof data4)[0]; + +export const TableRockBand: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Band name', + isRowHeader: true, + defaultWidth: '4fr', + cell: item => ( + + ), + }, + { + id: 'genre', + label: 'Genre', + defaultWidth: '4fr', + cell: item => , + }, + { + id: 'yearFormed', + label: 'Year formed', + defaultWidth: '1fr', + cell: item => , + }, + { + id: 'albums', + label: 'Albums', + defaultWidth: '1fr', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data4, + paginationOptions: { pageSize: 5 }, + }); + + return
    ; + }, +}; + +export const SelectionToggleWithActions: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    alert(`Clicked: ${item.name}`) }} + /> + ); + }, +}; + +export const SelectionModePlayground: Story = { + render: () => { + const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>( + 'multiple', + ); + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( + +
    +
    + + Selection mode: + + { + setSelectionMode(value as 'single' | 'multiple'); + setSelected(new Set()); + }} + > + single + multiple + +
    + + ); + }, +}; + +export const SelectionBehaviorPlayground: Story = { + render: () => { + const [selectionBehavior, setSelectionBehavior] = useState< + 'toggle' | 'replace' + >('toggle'); + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( + +
    +
    + + Selection behavior: + + { + setSelectionBehavior(value as 'toggle' | 'replace'); + setSelected(new Set()); + }} + > + toggle + replace + +
    + + ); + }, +}; diff --git a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx new file mode 100644 index 0000000000..3ec6af5789 --- /dev/null +++ b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx @@ -0,0 +1,388 @@ +/* eslint-disable no-restricted-syntax */ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Table, CellText, CellProfile, useTable, type ColumnConfig } from '..'; +import { data as data1 } from './mocked-data1'; +import { data as data4 } from './mocked-data4'; +import { selectionData, selectionColumns, tableStoriesMeta } from './utils'; + +const meta = { + title: 'Backstage UI/Table/visual', + ...tableStoriesMeta, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +type Data1Item = (typeof data1)[0]; +type Data4Item = (typeof data4)[0]; +type CellTextVariantsItem = (typeof cellTextVariantsData)[0]; + +export const ProfileCells: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Band name', + isRowHeader: true, + cell: item => ( + + ), + }, + { + id: 'genre', + label: 'Genre', + cell: item => , + }, + { + id: 'yearFormed', + label: 'Year formed', + cell: item => , + }, + { + id: 'albums', + label: 'Albums', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data4, + paginationOptions: { pageSize: 5 }, + }); + + return
    ; + }, +}; + +export const EmptyState: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => [], + paginationOptions: { pageSize: 5 }, + }); + + return ( +
    No data available} + /> + ); + }, +}; + +export const NoPagination: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + return ( +
    + ); + }, +}; + +export const SelectionWithDisabledRows: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(), + ); + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => selectionData, + paginationOptions: { pageSize: 10 }, + }); + + return ( +
    item.id === 2, + }} + /> + ); + }, +}; + +// Data for CellTextVariants story showcasing multiple features +const cellTextVariantsData = [ + { + id: 1, + name: 'Authentication Service', + description: 'Handles user login and session management', + type: 'service', + owner: 'Platform Team', + }, + { + id: 2, + name: 'A very long component name that should be truncated when it exceeds the available column width', + description: + 'This is also a very long description that demonstrates text truncation behavior in the table cells', + type: 'library', + owner: 'Frontend Team', + }, + { + id: 3, + name: 'API Gateway', + description: 'Routes and validates API requests', + type: 'service', + owner: 'Backend Team', + }, +]; + +export const CellTextVariants: Story = { + render: () => { + const [selected, setSelected] = useState | 'all'>( + new Set(['1', '3']), + ); + const [sortDescriptor, setSortDescriptor] = useState<{ + column: string; + direction: 'ascending' | 'descending'; + }>({ column: 'name', direction: 'ascending' }); + + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + isSortable: true, + cell: item => ( + + ), + }, + { + id: 'type', + label: 'Type', + isSortable: true, + cell: item => ( + 📦} + /> + ), + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + ]; + + return ( +
    + setSortDescriptor({ + column: String(descriptor.column), + direction: descriptor.direction, + }), + }} + /> + ); + }, +}; + +export const LoadingState: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + return ( +
    + ); + }, +}; + +export const ErrorState: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + return ( +
    + ); + }, +}; + +export const StaleState: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + return ( +
    + ); + }, +}; + +export const CustomPageSizeOptions: Story = { + render: () => { + const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => data1, + paginationOptions: { + pageSize: 3, + pageSizeOptions: [ + { label: '2 per page', value: 2 }, + { label: '3 per page', value: 3 }, + { label: '5 per page', value: 5 }, + { label: '7 per page', value: 7 }, + ], + onPageSizeChange: size => { + console.log('Page size changed to:', size); + }, + onNextPage: () => { + console.log('Navigated to next page'); + }, + onPreviousPage: () => { + console.log('Navigated to previous page'); + }, + }, + }); + + return
    ; + }, +}; diff --git a/packages/ui/src/components/Table/mocked-data1.ts b/packages/ui/src/components/Table/stories/mocked-data1.ts similarity index 93% rename from packages/ui/src/components/Table/mocked-data1.ts rename to packages/ui/src/components/Table/stories/mocked-data1.ts index ea5a9f2db8..b508bce387 100644 --- a/packages/ui/src/components/Table/mocked-data1.ts +++ b/packages/ui/src/components/Table/stories/mocked-data1.ts @@ -15,6 +15,7 @@ */ export interface DataProps { + id: string; name: string; owner: { name: string; @@ -29,6 +30,7 @@ export interface DataProps { export const data: DataProps[] = [ { + id: 'authentication-and-authorization-service', name: 'authentication-and-authorization-service', owner: { name: 'security-team', @@ -42,6 +44,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-interface-dashboard-and-analytics-platform', name: 'user-interface-dashboard-and-analytics-platform', owner: { name: 'frontend-team', @@ -55,6 +58,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'payment-gateway', name: 'payment-gateway', owner: { name: 'finance-team', @@ -68,6 +72,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'real-time-analytics-processing-and-visualization-engine', name: 'real-time-analytics-processing-and-visualization-engine', owner: { name: 'data-team', @@ -81,6 +86,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'notification-center', name: 'notification-center', owner: { name: 'platform-team', @@ -94,6 +100,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'administrative-control-panel-and-user-management-interface', name: 'administrative-control-panel-and-user-management-interface', owner: { name: 'frontend-team', @@ -107,6 +114,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'search-indexer', name: 'search-indexer', owner: { name: 'search-team', @@ -120,6 +128,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'cross-platform-mobile-application-framework', name: 'cross-platform-mobile-application-framework', owner: { name: 'mobile-team', @@ -133,6 +142,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'database-migration', name: 'database-migration', owner: { name: 'devops-team', @@ -146,6 +156,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'api-gateway', name: 'api-gateway', owner: { name: 'platform-team', @@ -159,6 +170,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'content-management', name: 'content-management', owner: { name: 'content-team', @@ -172,6 +184,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'enterprise-reporting-and-analytics-dashboard', name: 'enterprise-reporting-and-analytics-dashboard', owner: { name: 'analytics-team', @@ -185,6 +198,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'image-processing-and-optimization-service', name: 'image-processing-and-optimization-service', owner: { name: 'media-team', @@ -198,6 +212,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'customer-portal', name: 'customer-portal', owner: { name: 'frontend-team', @@ -211,6 +226,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'log-aggregator', name: 'log-aggregator', owner: { name: 'devops-team', @@ -224,6 +240,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'identity-provider', name: 'identity-provider', owner: { name: 'security-team', @@ -237,6 +254,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'document-storage', name: 'document-storage', owner: { name: 'storage-team', @@ -250,6 +268,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'workflow-engine', name: 'workflow-engine', owner: { name: 'platform-team', @@ -263,6 +282,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'mobile-backend', name: 'mobile-backend', owner: { name: 'mobile-team', @@ -276,6 +296,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'system-monitoring-and-alerting-dashboard', name: 'system-monitoring-and-alerting-dashboard', owner: { name: 'devops-team', @@ -289,6 +310,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'email-service', name: 'email-service', owner: { name: 'communication-team', @@ -302,6 +324,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-pipeline', name: 'data-pipeline', owner: { name: 'data-team', @@ -315,6 +338,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'configuration-manager', name: 'configuration-manager', owner: { name: 'platform-team', @@ -328,6 +352,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'testing-framework', name: 'testing-framework', owner: { name: 'qa-team', @@ -341,6 +366,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'cache-service', name: 'cache-service', owner: { name: 'platform-team', @@ -354,6 +380,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'billing-system', name: 'billing-system', owner: { name: 'finance-team', @@ -367,6 +394,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'comprehensive-product-documentation-and-api-reference', name: 'comprehensive-product-documentation-and-api-reference', owner: { name: 'docs-team', @@ -380,6 +408,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'queue-manager', name: 'queue-manager', owner: { name: 'platform-team', @@ -393,6 +422,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'security-scanner', name: 'security-scanner', owner: { name: 'security-team', @@ -406,6 +436,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'user-profile', name: 'user-profile', owner: { name: 'frontend-team', @@ -419,6 +450,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-warehouse', name: 'data-warehouse', owner: { name: 'data-team', @@ -432,6 +464,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'deployment-automation', name: 'deployment-automation', owner: { name: 'devops-team', @@ -445,6 +478,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'chat-service', name: 'chat-service', owner: { name: 'communication-team', @@ -458,6 +492,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'analytics-dashboard', name: 'analytics-dashboard', owner: { name: 'analytics-team', @@ -471,6 +506,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'file-uploader', name: 'file-uploader', owner: { name: 'storage-team', @@ -484,6 +520,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'search-service', name: 'search-service', owner: { name: 'search-team', @@ -497,6 +534,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'mobile-sdk', name: 'mobile-sdk', owner: { name: 'mobile-team', @@ -510,6 +548,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'performance-monitor', name: 'performance-monitor', owner: { name: 'devops-team', @@ -523,6 +562,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'content-delivery', name: 'content-delivery', owner: { name: 'media-team', @@ -536,6 +576,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-authentication', name: 'user-authentication', owner: { name: 'security-team', @@ -549,6 +590,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-export', name: 'data-export', owner: { name: 'data-team', @@ -562,6 +604,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'admin-api', name: 'admin-api', owner: { name: 'platform-team', @@ -575,6 +618,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'testing-dashboard', name: 'testing-dashboard', owner: { name: 'qa-team', @@ -587,6 +631,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'message-broker', name: 'message-broker', owner: { name: 'platform-team', @@ -600,6 +645,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'payment-processor', name: 'payment-processor', owner: { name: 'finance-team', @@ -613,6 +659,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'document-viewer', name: 'document-viewer', owner: { name: 'frontend-team', @@ -625,6 +672,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'load-balancer', name: 'load-balancer', owner: { name: 'devops-team', @@ -638,6 +686,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'security-audit', name: 'security-audit', owner: { name: 'security-team', @@ -651,6 +700,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-settings', name: 'user-settings', owner: { name: 'frontend-team', @@ -664,6 +714,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-import', name: 'data-import', owner: { name: 'data-team', @@ -677,6 +728,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'infrastructure-monitor', name: 'infrastructure-monitor', owner: { name: 'devops-team', @@ -690,6 +742,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'notification-manager', name: 'notification-manager', owner: { name: 'communication-team', @@ -703,6 +756,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'analytics-processor', name: 'analytics-processor', owner: { name: 'analytics-team', @@ -716,6 +770,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'file-manager', name: 'file-manager', owner: { name: 'storage-team', @@ -728,6 +783,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'search-index', name: 'search-index', owner: { name: 'search-team', @@ -740,6 +796,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'mobile-authentication', name: 'mobile-authentication', owner: { name: 'mobile-team', @@ -753,6 +810,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'system-monitor', name: 'system-monitor', owner: { name: 'devops-team', @@ -766,6 +824,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'media-processor', name: 'media-processor', owner: { name: 'media-team', @@ -778,6 +837,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-management', name: 'user-management', owner: { name: 'security-team', @@ -790,6 +850,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-transformer', name: 'data-transformer', owner: { name: 'data-team', @@ -803,6 +864,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'admin-dashboard', name: 'admin-dashboard', owner: { name: 'platform-team', @@ -816,6 +878,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'test-automation', name: 'test-automation', owner: { name: 'qa-team', @@ -828,6 +891,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'event-bus', name: 'event-bus', owner: { name: 'platform-team', @@ -840,6 +904,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'invoice-generator', name: 'invoice-generator', owner: { name: 'finance-team', @@ -852,6 +917,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'document-editor', name: 'document-editor', owner: { name: 'frontend-team', @@ -864,6 +930,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'service-discovery', name: 'service-discovery', owner: { name: 'devops-team', @@ -876,6 +943,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'security-monitor', name: 'security-monitor', owner: { name: 'security-team', @@ -888,6 +956,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-preferences', name: 'user-preferences', owner: { name: 'frontend-team', @@ -900,6 +969,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-validator', name: 'data-validator', owner: { name: 'data-team', @@ -912,6 +982,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'infrastructure-automation', name: 'infrastructure-automation', owner: { name: 'devops-team', @@ -925,6 +996,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'notification-dispatcher', name: 'notification-dispatcher', owner: { name: 'communication-team', @@ -938,6 +1010,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'analytics-collector', name: 'analytics-collector', owner: { name: 'analytics-team', @@ -950,6 +1023,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'file-processor', name: 'file-processor', owner: { name: 'storage-team', @@ -962,6 +1036,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'search-analyzer', name: 'search-analyzer', owner: { name: 'search-team', @@ -974,6 +1049,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'mobile-notifications', name: 'mobile-notifications', owner: { name: 'mobile-team', @@ -986,6 +1062,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'system-alerts', name: 'system-alerts', owner: { name: 'devops-team', @@ -998,6 +1075,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'media-encoder', name: 'media-encoder', owner: { name: 'media-team', @@ -1010,6 +1088,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-authorization', name: 'user-authorization', owner: { name: 'security-team', @@ -1022,6 +1101,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-aggregator', name: 'data-aggregator', owner: { name: 'data-team', @@ -1034,6 +1114,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'admin-authentication', name: 'admin-authentication', owner: { name: 'platform-team', @@ -1046,6 +1127,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'test-coverage', name: 'test-coverage', owner: { name: 'qa-team', @@ -1058,6 +1140,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'event-processor', name: 'event-processor', owner: { name: 'platform-team', @@ -1070,6 +1153,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'payment-validator', name: 'payment-validator', owner: { name: 'finance-team', @@ -1082,6 +1166,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'document-converter', name: 'document-converter', owner: { name: 'frontend-team', @@ -1094,6 +1179,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'service-health', name: 'service-health', owner: { name: 'devops-team', @@ -1106,6 +1192,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'security-logger', name: 'security-logger', owner: { name: 'security-team', @@ -1118,6 +1205,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-analytics', name: 'user-analytics', owner: { name: 'frontend-team', @@ -1131,6 +1219,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'data-cleaner', name: 'data-cleaner', owner: { name: 'data-team', @@ -1143,6 +1232,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'infrastructure-deployer', name: 'infrastructure-deployer', owner: { name: 'devops-team', @@ -1155,6 +1245,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'notification-queue', name: 'notification-queue', owner: { name: 'communication-team', @@ -1167,6 +1258,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'analytics-exporter', name: 'analytics-exporter', owner: { name: 'analytics-team', @@ -1179,6 +1271,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'file-validator', name: 'file-validator', owner: { name: 'storage-team', @@ -1191,6 +1284,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'search-optimizer', name: 'search-optimizer', owner: { name: 'search-team', @@ -1203,6 +1297,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'mobile-analytics', name: 'mobile-analytics', owner: { name: 'mobile-team', @@ -1215,6 +1310,7 @@ export const data: DataProps[] = [ lifecycle: 'experimental', }, { + id: 'system-logger', name: 'system-logger', owner: { name: 'devops-team', @@ -1227,6 +1323,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'media-validator', name: 'media-validator', owner: { name: 'media-team', @@ -1239,6 +1336,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'user-audit', name: 'user-audit', owner: { name: 'security-team', @@ -1251,6 +1349,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'data-normalizer', name: 'data-normalizer', owner: { name: 'data-team', @@ -1263,6 +1362,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'admin-authorization', name: 'admin-authorization', owner: { name: 'platform-team', @@ -1275,6 +1375,7 @@ export const data: DataProps[] = [ lifecycle: 'production', }, { + id: 'test-reporting', name: 'test-reporting', owner: { name: 'qa-team', diff --git a/packages/ui/src/components/Table/stories/mocked-data2.ts b/packages/ui/src/components/Table/stories/mocked-data2.ts new file mode 100644 index 0000000000..0aff7bcd98 --- /dev/null +++ b/packages/ui/src/components/Table/stories/mocked-data2.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createElement } from 'react'; +import { RiCactusLine } from '@remixicon/react'; + +export interface DataProps { + name: string; + description?: string; + icon?: React.ReactNode; + href?: string; +} + +export const data: DataProps[] = [ + { + name: 'Cell with title only', + }, + { + name: 'Cell with title and description', + description: + 'A comprehensive service handling user authentication and role-based access control across all applications.', + }, + { + name: 'Cell with title and icon', + icon: createElement(RiCactusLine), + }, + { + name: 'Cell with title, description and icon', + + description: + 'A comprehensive service handling user authentication and role-based access control across all applications.', + icon: createElement(RiCactusLine), + }, + { + icon: createElement(RiCactusLine), + name: 'Cell with extremely long title that demonstrates text wrapping behavior and how the component handles content that extends well beyond typical cell widths in table layouts', + description: + 'This is a comprehensive and detailed description that contains a substantial amount of text to demonstrate how the CellText component handles lengthy content. It includes multiple sentences and covers various aspects of the service or component being described. The text should wrap appropriately within the cell boundaries while maintaining readability and proper spacing. This example helps ensure that the table remains visually consistent and user-friendly even when dealing with extensive content that might otherwise break the layout or cause accessibility issues. The component should gracefully handle such scenarios by providing appropriate text wrapping, maintaining proper line heights, and ensuring that all content remains accessible and readable regardless of its length.', + }, + { + name: 'Cell with title and URL', + href: '/catalog/default/service/auth-service', + }, + { + name: 'Cell with title, description and URL', + description: + 'A comprehensive service handling user authentication and role-based access control across all applications.', + href: '/catalog/default/service/auth-service', + }, + { + name: 'Cell with title, icon and URL', + icon: createElement(RiCactusLine), + href: '/catalog/default/service/auth-service', + }, + { + name: 'Cell with title, description, icon and URL', + description: + 'A comprehensive service handling user authentication and role-based access control across all applications.', + icon: createElement(RiCactusLine), + href: '/catalog/default/service/auth-service', + }, + { + icon: createElement(RiCactusLine), + name: 'Cell with extremely long title that demonstrates text wrapping behavior and how the component handles content that extends well beyond typical cell widths in table layouts', + description: + 'This is a comprehensive and detailed description that contains a substantial amount of text to demonstrate how the CellText component handles lengthy content. It includes multiple sentences and covers various aspects of the service or component being described. The text should wrap appropriately within the cell boundaries while maintaining readability and proper spacing. This example helps ensure that the table remains visually consistent and user-friendly even when dealing with extensive content that might otherwise break the layout or cause accessibility issues. The component should gracefully handle such scenarios by providing appropriate text wrapping, maintaining proper line heights, and ensuring that all content remains accessible and readable regardless of its length.', + href: '/catalog/default/service/auth-service', + }, + { + name: 'External link example', + description: 'This is an external link that opens in a new tab', + href: 'https://backstage.io', + }, +]; diff --git a/packages/ui/src/components/Table/mocked-data3.ts b/packages/ui/src/components/Table/stories/mocked-data3.ts similarity index 100% rename from packages/ui/src/components/Table/mocked-data3.ts rename to packages/ui/src/components/Table/stories/mocked-data3.ts diff --git a/packages/ui/src/components/Table/mocked-data4.ts b/packages/ui/src/components/Table/stories/mocked-data4.ts similarity index 99% rename from packages/ui/src/components/Table/mocked-data4.ts rename to packages/ui/src/components/Table/stories/mocked-data4.ts index b86d4c903f..5cedd2cbae 100644 --- a/packages/ui/src/components/Table/mocked-data4.ts +++ b/packages/ui/src/components/Table/stories/mocked-data4.ts @@ -15,6 +15,7 @@ */ export interface RockBandDataProps { + id: string; name: string; image: string; description: string; @@ -31,6 +32,7 @@ export interface RockBandDataProps { export const data: RockBandDataProps[] = [ { + id: 'The Beatles', name: 'The Beatles', image: 'https://upload.wikimedia.org/wikipedia/en/thumb/4/42/Beatles_-_Abbey_Road.jpg/250px-Beatles_-_Abbey_Road.jpg', @@ -57,6 +59,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.thebeatles.com', }, { + id: 'Led Zeppelin', name: 'Led Zeppelin', image: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUSExMWFRUXGBcYFxgYGBcXGBgXGBUYFhgYFhgYHSggGBolHRcXITEhJSkrLi4uGB8zODMtNygtLysBCgoKBQUFDgUFDisZExkrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrK//AABEIALwBDAMBIgACEQEDEQH/xAAcAAABBQEBAQAAAAAAAAAAAAAGAQIEBQcAAwj/xABCEAACAQIEBAQDBQYFBAEFAQABAhEAAwQSITEFBkFREyJhcTKBkQdCobHwFCNScsHRM2KC4fEkg5KiQxdTc6OyFf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDZzS0gpaBK6urqDq6urqDq6krqBa6krqDjSVxrqDprqSuoFmuFJSigWaWkFdQdS0grqBa411JQcaQmuNV/FMVlUgHUg/Kg8+Kcat2d5J7Dt3J6CgXiX2nFLoVbQKGAGLFNZgk5htU7ia3C9uyozggEkgEmQRm1I2jodMwq24fyxYyL41pHYDqJGpkmDufU0HYTnFAiteAAZgilD4gk6DNG1FStQVxjhdq0gVhlsh1dGk/unVsyySdLZOnpMbHS14HxwXGyaHyzI1HyNARUtMFOoOpaSloENIwp1IaBa6urqDjSV1dQdSV1NJoFmummzXTQOmups0s0C00murzuXANaB80mas/5u+0uxhly2Ct+6egMqv8AOR19KAb/ANquPZgQ1tB/CEBB9y0mg37NSg1mvLP2rWLxS3iVNm4YGfQ2idt5lJ9RA71ottwdZoJE11eeanTQPpKQGloFrqSaaTQI7UL8UZ7twqNEUEluuYEZQo7DUyav8ZdgGgbGY4XRdt+IbfSREn/bSJ9aCnt8cvJde5cVFbNaQKznKLbZir5gCczHy6CdYANGvBOLXWA8ZbYVhKFPEB9mW4oI+cH0FZ7wJ7jYgC4ZHiIkn7xV7t4R1O2Yeh13FaPjQ0L4YUkEEyxUR1JIU9KCvxXBXvNcLvnVwR+8NwhQWMqltWVAMvWJmJmq7lu0bd5VyeGgQrl3lywOYHqIHyJb3orw+IUyuZSd8oIJH4z86EP2/NiblvT93d9hHhp17h80j1HpIaHhrkivcVU8KxIIg79R2q0BoHUtJS0HUhNdXRQLXV1JQcaSuNJNB01E4jjUs22uOYVRJqSxrNPtI4mbpOHRiAozN2LfdX8Z+QoKHmT7QMRec28OSqzpl/239++3QlmBw3GLdv8AaPGugAzkZ2JIjcqdI9Pwq0+zfhVhE8a5ke6zsqgssrl8p8p3Myd9BEAdT3H4lVGp37KWj1IUGB6nSgqeR+cRjEyuAt5PiXoY0zL/AG6UVrdFfN9jibYTGi6mkPJH+Q7r66Vu3D+IB1V1MhhI9qC7a7WZfbJx69ZtW7Vpiq3S6uwMGAF8oO+snUfwx1oz4zjclm4wmcpC5d8x0UCeskV8+80cZxN5hYxDqxsMy+UAeYHKSdPT060FITVry/y/fxj5LKgxGZmMKs9+p9gDUPhPD2xF1bKQGadTsIE6kdK2HlfhRwFpxoSVYkobgBI+9lZ2Ab1AG3yoM85v5QuYEW2JLo2jMBoH3AjoCJj2o1+yfm27cP7JdYMESbbHeAYyk9YG3tULnzh998O7sXyIM+t5rgYSCcyPOQjUgqemwnTPeFYtrbhlYqRsRvPSg+pUea9laqPgGJL2LRZgWKISRtmKifxq2VqCQDTpryBp4NA4mvN2pSa8bjUFNzFfXIVZsoI1PaszFyzbwl5XuFyZJZN7h+6oIGg9Onzo15o4rZC3EuifKTHcCAfzrL8JxbC2UvhJAurGQnNqCCGXSFKwdTQQsFxh7dxbuY5sNcLNaJMOpyWXZT/EoCAg9NRs1azwfjWExoUAqxWDkY6g9MyHSR3rEOJ4sXGzARmMvMeZtQDp6GfcmtK5G4Vh71pWa3bdlGudA2nY5pmgM+N8wYfBWWZmWVHltrGZmOygD5a9BrWT8Hxd/wAR7l7zLdJuFQ0f4p302AD9egoo+0kWksC2iKh3VUXKJJCzCiOtBXBsViLcMqgquYCQJYspABPWPK3+kUG38HxS5c/whjOu8DQfgKIsPckVlvK9u81xrjHLaMMqGSdAAzDspaSAe4o54NxIMPhI1iOo96C/paYhp1AtLSUtAlJTqaaBDTTSk01jQeOJuQpPpWScxXApu3mbLmMDX7ijOzR10P4VovM3EVs2WdjA0/E/2k/KsB5y4hcuuB/8YUZOxDksD81y/IUG2YLFWLGFth3ChUtlpkwzgHUDqSfxqEDbxGIuqtycqqDkdkZT7qQw9CPX51+C5lwYwqXFJ8RUgbBx8OYHMIIOVTqCDAPrVBzBxixaU4y04OIYgJoiEZRqGVAM0yJMaiKAU5ow+W9clizJedGY7nyZhPcwN+5o/wCQOJZsKoJ1Qlfl0/Csrs4trmdn8zPdLn1LW7gY6fzfhV9yZxXwwyk/rSPzP0oNS5lxwTC3LhjyZW120YHUQfbbrWB8Rv8AiXXeZzOzTrrLEzrrrWkcx4hsVhGtoTPlYgSZykHLHWdNO461l7CgJ/s+41awuJL3pylSJABg7++u3zo1v8Y4hfQ3USzYRklA5ctcRirL5x5dVn/MCdgNayKp1ji99MgW84Fv4BmJC+gU6R6UGhcyc3h8A9l7fhYhv3TWt4UqDnkwYKnqJk1m1kjMJMCRJ3gTrp1pL99nZndizMSWYmSSdyaaKD6Q5XwapbDKB5gDIEAyN46TqT6zRAKoOVOIW79hHtHywAPSAJHrG3uCOlXyig9lp001a4mgVjUTFv5TUkmvC+ukUGRcxcRuG4ywrqJbKfhJGaJH3ogwD1A3ql4lwSzcRbiIUzDMf4VAEkKOu/6g1ofE+XrbM8j4o/D/AJqoxfALp8NSwyJoCB5gNDrB1EjWgyhbAZoE5Zj1rUOTeEPaRWtvBI+8Mw19ARTuD8nAXCWiFJmRM6LB+ep9oo4wmECwBsAP+KAC5l5fvMRca9cueYEjKoVR3CqJb5k1R30gPbRXuWoU6+RkZSTIEHTzEQd962XwxXi/DbRbNkE9436UATyxZNxQrMdRAEnYAkAmNxP5UYcPwnhBQO+skknytqSfap+HwqJqqKvcgAflXnxHEpbALsFB2nqQCx/AGgn4a6c0dI1+pA/XpU8GqPB3wxLDbQT7CfrJP0qzw9/7p+VBLpaQUooENNNKahcQx6WhLddgN/8Aig9cTfVFLMYA/WnrWe89c1ObFy1ZDKWAh1kMokE6g+U6HbvTeZOPNdMAwBMDoNvqaEb1wtMmd567bigGuM8wYm6uW7ddx/mZm2/mPtVH4jvCST2BO3TSdhRRisJsQBG4Y/D6T2Hrt6jerLlTB4ZHS5i8Oly1cOXPJYI3lynytEHMfYQdtSE//wClmI8PTEoJE5HQ6EjUSGP1HahDm/lq5gjbF24rs+YgKDoBEyT7ivodro1Pb8omsa+0HjFrFOCFHh22Kh9nfMN0/wAmgMddD6AAKziisQBoSfckR+U/WiH7PeFjE4ko2qpbZwpJAd5CorEahTmO3b3oev213EgSAJ1O25rT/so4Kf2S9fH+JcaLZ9LJlZ7TczA+goJ3OGFY4VXwwVMmVskBf3Z006AqdwR/FWR47Ctbco4hhBOs7iRr863TjjCEuR+6ulAw/h8WEefQqxP8yetCHE+UlxNws1xrbrC3AFBnIsErJ0mBrrQD/LuHstaS04Du7m6Vg/4eUI6M3RjlFwAa/uukiZXFfs+a0j3hfTw1BMFWzR0Gkyx0FGvC+UbNi2PDktoczHWYIEwBpJmKUXmYxeAW3bcC2Bq125EiFHRJPzE6BaDJOKcKewLef4nWSNfKdDlPrDA/OvPhuDa7cW2ilmYwACB0k6nTp1rQMZyi2JutcuO6eKxcqCDlAGVIkQDGUewjoCLDgfKdvC3ZR2ZyCudoELoXygCJlrag+re1AW8gcMfD4S3auMrMC5OWcozOzZRIG0xtGlFKmqDgjhRk7Ex7EkwPQaiOgirpHoPeaQmmFq82ujvQSAaj3moM419p2DsFkXNeYaeQDLP87aH5UM437XJPkw5y9Qza/Uf2oD+83nPy96W0vUQR+torOcL9pSFpuW2UekGPfXX5UacL4vavIHQyDtoQfnIoLGw2VmUiIiNtQVGv1DD5VNtvVLxe9HhOBtcAY7EK3liBvLFNNuvSrOxcmgmA0tu2SNyD+u4pgbpUix8I+v1oFt2QNdSfUz9BsPkKoua+XmxeSLuQIHhcshmYAAsZkACR/qNEIr0VaCl4RFqbLCHXUn+Kdc49CZPvI6VazpTMfgM4BU5XX4W/NT/lOn0mq4cTCSt3yMIkN6mBB6gnqKC+wt/7p+R71KmqA4odNatcNiQV8xg0DOL4s2rRuCNCszsAWAJPsDWecZ4sWlmJnY+laXiEBBBEgiCKyzifKn/VpZGIdLNzNAgMysAWCI3Y6xmBiPWgE8fjiWCICWPQDMdTGw9qm4fl/FqGuXFyWlU5VkTJZYZguh6mSe2laZwXgdmwMiWwon1M+rMSSx96sOK4fPZuIBup/vQYoWI9+2leZJXJcRRNu5mgroWIUgHTY+F+fep2PtQxgfnXjgbIuP4Wvn3iSVyqWzDuQAw101Oo3AWHH+actj9ntMzZoMEzFsqDldt41mNzMHy7gWKZmMsZP5egHQVPvBDDAtJALkx8XULH3YiJqDiWiggXDr7VvnI2F8HAYdNj4Ydvd5uN+LGsDW0W8o3chV9zoPxNfR6IEXINguUR0gQKCm4tbDW72HJjOjZD2YhmUj1Dbfy164i0M5uxAdDmHYxp+cfKq3nS6bdpbw+4wk+nQ/WfrVt+0BrfplH6ig9cJflOxA27VEt4YZ5gljmAmNM2rR6Qv/se9ehgFSN9BvpBIG20yRSJdVrwUhlORmWZX7+Vtj6CPT50Eu6AuwkxHX8e1QMEQ965AMWYtz0Nxl8R8vsH19Y7VPuXcsNAIGrSYhRqW13jtSYDD5LQB+M5nf8Ancl217AmB6KKBtsw5jsP1+NW9m9pNDpvnNU3D4oxFBa3MVWd/anxe7+5wdtsgvSbjElQVBChWP8ABuW9FHSiC/j4eJqk5j4et+/Zus8KiXBllwWlkIAKCQIDT6DtNAG8d5VWzZzC4ouIMxBYnOvUrplX0HpQjW3eHcZllZQKRaAKxbYiCSFBDfX86yvm/g5w19hkyIxJQTsOqj0BP5UFLNHX2a8UIZrBOnxL/UfrvQGKteWcUbeJtttrB+Yj84oNyxFkXLTITGZSPaRuPak4FeLWkLfEVBb001+h0qFhsf5O57VX8vcSRQ6vcUXrl14QkSqZyEUD2gn1mgOMPqJ7n8NhUpRUSy20bDb5VLWgeKkrUW3UpTQegoV57u22W1YKhrjuGXuioQWYdp0X1k9qJr95UVnc5VUFmJ2AAkk/IVnPCL7Yu/cxTA+YwojRbY0VTrv39Se9AUcNtwoM9KtbdhmEgabVEwNjMwXtq39vc1foABA2oEcUIc3JkC3hvadX+SsCY+Q/GjA1W8Yso1tg4kEHTqfQTQOuoD5h/wAivPEN5GMxode2m9VnKuM/d/szyLlpRAYam1sp7Er8BjsN5BM7iVwKjMSIg77GdAPmYHzoMr42hBPlOpJ9Iih5mIIMAnTcAjfsdJoq4phgAcp8saDeOunYabUL4xhqDp7dNd9qCDxTOrZXQIQF8oED4QQTGkkEEnuTVFibpqfiXWYTMVAGrKFMkCdNes1W4jvQX3I2A8XG4VYkK3it7W5uL/7BB863PF4dyv7sqG7MCQT7g6fjQ1ynyxbwqC/ZJN65Ztgi4QVBgMwQgArJjedhVxhuKJcIBORx8SMYdT2I6jsRoaAb5gxrXMPibN63lZbbbSRKjMNx6Aio3J2PN20M2oWBPcif9q9uc7muIU7tbOU9SCka99QfrVHy3jLduyqsSInodaA3fzPbj4VbMfWFOWfnB+QpLd/96zCJACDtMlj77ih/D4+9fRzZT92cyhsxVmOxKkfDBkT71ZcKRrazeBL6gAGQxmZAn9RQSOLYoJ4aEy967bTX+Gc7COgKIwqxv4omVUZmjpsB3YnT+tZXzNzIRi7RGosXA7dZOgYD5SPnWg47iioMudA5+6WURP3m13oPRrMCZk9fWa8bWKAJBNQcLxBQ+UMGVo2OYZge47iqLmDjARzlNBacUx4FwQfpVNxdP2m9ZSFcKGm2zZVbMVElsrBTEicp3NDmN4z9ajcG4qqXc10ZgdJ1OX101oNV5P4TdwwuBiPCIXJbztc8JhOYB2UEqZ/Cs9+03F+JjIj4EUfUlv60V43m8WcNmQrcJ0GUkiTPxem1ZljMU1241xzLMZP9PlQRJp6XCCCNxB+lNakoNW5Yxy3kzZoYgSp6mIJXvVqeB2bhlhmPykf71mvL3EnSESJkkT+IPejfC4nEnzeGI7qcw3/h+L6A0Fth+H4qwf3Fw5f4XOZfoduuxq2tcw31Ui5hiXAMZSAGMaDXQCfXSqvD8ZdIDqf7+mVgG/Cp1njNljBgbbgr89aCVY5sRR+9VlMAkBLmk6aGIb3U1f3uMWLaq73VCsQqkSxYnYAKCT9NOtVOCxdh9FZT6ZlP9amXcGhGqj5/regHuYuLjG3v2Oy/7pSPFZTOdgZyAj7gMT3PoNb7h/DxaRURdf1rUSzwayrZwvm/i1zD2JP5UQcGuFlJKt/MVKzBjYgdtxoaCZgsPkWOu5Pc1JFNFOFA014XbQJn0j+ule9NNAKcewFwMl+wVW7bn4vhZW0ZHjUqdPYgHpQxzXzOL9r9ju2GtX7jaqScpFuX8SxeAhiHW3uARJMaVomLAkA9dB79qDeab4Ft2bDftFq2Zu29r9ob+JabZl0kQRsdZUgBn+H4xIZWYGNCSMreoddg3quhImBVbj7hzSNuka+tTeN4SyQuLw19bttWUFbhW3fTNMI4YDOp6HrG53qpxOLcg6BgeogkfIRH5UEfHsxbzlZhdEyx8Ag+TSYj51U4oyrH0NWN/DlSpKlAVBOYr3IJ8vwiR8JEiK8+E4fxL9q2R5bl60nuGuqp/A0H0Fh3EJMLKiAYGpAge/SKj8XQBczKDGxYAr7E6ZT06b1D5yN39lveCoa5kbKuXNM6EBCCGOWYWNag8ENq9ZS7YvX7OYee0HNwI+zoVuq+WCCIEdO9BT8YdMQz21bwrqrlyOPKc2iFLm2p+6SD6daDcFgrpmXYDqBbBIiZHmmKvudsO9gm/afK4QL5ZTyscpyptImTBj4TAgznpxVyCPEcf6m/vrQaxwu02HtW5IS2tsl80A52hsxYbQSwiOtQ+K8yeIMti9YCkHPd8VAyjsoIOXfeZ30FZ3juKXLpYFmCEyLZdmUdgMxqCaC8CWXxCIhlA652118wDHMd+uvWtQXjZtOoCqLTkgHKUYPuBkgZs2sHrH1x7hX+JHcR9WFbHcw7C35VAYZIYk9GElvlOg9utBR8538gDoCs9IykEkT5TrHyoMym55p0kLPqWA/rNXnNElGOUAMyLmGn3gIK/d2JionA+GFrQIOp1K+qkH2BgR7xQQOKctvbGYNmMarB/A9a9eXeXS48e8wt2l18w0ZYIMmfIPU1od7l13BZLmXPuGGYHSOhBG3eq+3hLuHBXEAtaMDOsshHZ7cSu280HpwLCgZ7NxRnt/ECBDp9y4NNZGhjqCKEOdb9lCMPaQAhizkAAeYaARvp9KPsfbDWwRJYwEZIzjNoHTTza6lOonfQUNpyTce8t7GMi5iGKWzmBiBlZtInbT11oM+t4VmRnAkIRm7jNMGO2kV4CtVvco2lvM6TbUrlKiChkRAUqc07mZk7a1T8L5HW7b8VmNt80ZEYELBgyTJB30n6bUAzwqwqkG4vlJgHNlAPow2NaVy5j7RK2i03DOVxKh4EkEbZgO41iYr34RwFLByICzvvOuW3J8zNuFkNoIzEr0oT5vt3sFiB4eUWn89s5ElG6qGiQQdQR0PoaDVrFto/xCB7LPygVHx3B7V8BbigrmkLAInKwlgZDfFOs6gHpQZytzsjEJigoOwuiQv/AHF+5/MNN9q0PxBAA+Ua/QDegjjhFtVCqqBRoPux7RpUjAcHWSVZ0jQLmlCO5ToZnYiq/H8zYa02Rjnuf/btjxHB7NlkJ00Jq65cx4voXCZCDlKkox9zkJjrQe2G4Y33yInZZ1Gu50irW2gAAGgFItPFAopRXV00CUhpTSGgrOMt5MobI7SLbdnymD9YrMsFzDcFlTdn9osrCNBPjps1i9HUxpc1WQGncMVc+JduRbt2ncASSpUan+fRumlZRxzCXrJzMGtj/Pbu2lJPa4pNon3oIXFbai9dOHz27JabaSMyAqJUrJGXMWgTERVa7SZZbbR3UK31iKk3MYQAXUr6nzIf9S7H2+lNbFr8TCR7Bh/5D8jBoK+86tAyqkD7uxMnU9zrv6CrTk+yf2/CCZ/fI3/ic5/AGqq54ZmJHbWf6Cin7L8Nnx6tEi1bd56AkC2J9YdvpQH/ANofF2w+Hz22y3AylTvqD1HUdx1E1L5c5js4q012VtOCBfGgAuEAAhj8QIWAd/LHShH7Vmnwl9SxGuwHShfk3jxwuLV2I8N/JcBgLrORj0GVoM9AWoNE554X4tpoOuUjrvuP171ih37enat05lxRQEZz77bjQSABuND3rF+MpF5zr5jOoAJneY/PrQQq6kmumgtOWMH4uLsW+9wE+yec/gpra8VoNfnWP8kqfHLzGVY9yxA+QAzE+3rV9f4ybRdkuTAZo1ALAEgMDowMD110jegg8531d0tjKWUElh1DEZR9BPzFXPAmTJroGk5p0Vog6ERBgn9CgLx8zl2GZmM5RIHsOsDaOwo74Jg3Nk3CwZ2AK2wIVcuqqC2k+u0/WgLeEYksqq7DMqgDXoPKD5T1I9fep37RnMDKwjbQgjudCIrNOXOYbKljiGImR8LEwWkCVGnQfKi/C80cNyx4oUdQVuSffMutBGSw9vH4dLfltXc+mwR1RmJXfLIG1EvEHa3GYMVgyQczLtqVHxjfuaruGY3D3bymxiEbKjnLmMgmF2LCNGI0irl8ShcI28DUnfU9PlQVVu95wzEKg+F2IysIBkdI1ivcWlS6riDbYy8fdfUAj0J0PqR3q1S0sELlB2BjT9fPWqXm3ia4WwxUKb7EIgEAs9xsoJURmiZIO8dJoLZ8WgkrqWbKoG7kDX5AddhQp9o3DmOEe8TNxWtkxstsMVKL6AvmJ3JXoAALngOANm0nimWVAvsBv8ydT/sKkcSxVl7bWXZIug2wJEnMMug3nWgxe20Ixyroh7nXfXT9RU/B8ZusoVrz5AAIZ2VIHSJiPeK8cFoSjbjMp94yn8jVFhrpEake29AcW8cq+W1Zb/8AJkuZT/IAskesijj7NeJE3Gtk6Ms6oysSNRqzsYAJ0rKsClsnMwYt3PmJ95o25RxS271sgtow3002PptIoNoBp4piU+gUUtJSg0CGmk0401qCr4hbknb5iRt1HUUD8awNsEz5AetsujAeuVoiY0MjTUUd4vrQrxskLMaid9ent60Ge4LhNv8AaTh2cHOha2UhVuATqogqriDmUAqdxl2PhxTljww2UoxbLAYm0SCwOWDpOkzp023qHxPHvbZWQDyP4ls75X3bSIIOxH9aJuAYq/cwoOQNdA3V0W6UMshOaQBBgFtTQZ9//k3GIAVC7GFtrcVrjR2RSSev0q+4KcXgfFZMLdD3cmyXCqLmbKolWnUxq0wBrua0Dk7h4F97jtfZ0QALejyZpkrlRVaQsZhOxqy5r4g1i290jyJbcnWMzQFVfQksevT1oMd49x67iro8VAhthhABBBmCGzHp8qo7zRJ7U/CJDQR0b/8AkkfiKseFcJN65Blbaea68E5VB/AnYToN+lAV4zH30wdvDXgQ9u1DkDPlEzbz6GCoyg/6tdKCOJl2MtLHvoflI3/OjrG8Ww902ma0pN1ibjSVJzMQkDqWEnXuO9DGMSzN1AgkO+SGaAF8pOp69qAbrqm4+woVHUyGmfQiOn1qCaA05J4OWttegmTAWYBVeraTvOlJxu0c5WJnYBSPcep9KXifM13DpYsWMqZbQz+QEydANdNhO3Wm8O422MC4e4AL/wD8VwHKHYfdcbAnXUb7dpCtwWHyz+7QGZzvnBXplAVlI1nr32qdhbruHsWXzEgzlRzkzGCVIJIEe5B1BGteJtXTeew4HjDMSJ8rjLJjuf7V78OsXiuUFcPZBIJGjXCNCZ/rQVa8LuI2S5bKMIkaEazGqkjodJ6HtRPwzgKtAIzMw20ED85qwTh+GdALhe6qmQpJVc38RC6t7tNPxGENwHwQtjI0LkAUlhIIjqP7Ggfwjkq0fF8S2D5lC66iFJMHf7w+lJj+Rbk/usSUGkA5jEfMn8qtOW+MeGht33BcMSzBWKZYABLCQug1n366E9vFoVDBlIOxDAg+x0mgBOA8uYlC4vY24iD4QsSx7+eY9ory45ytiPEt3rd5rz22DLbuwAxUz5GELP8AlMe9aEgDCRHsYrksINCqiewAP4b0AVg8ab8HF4V0EwSXvFAe7WvLlH1/rRbwjhlm2ueylqDrKAa+s71YG0GETI9fpt+B+deNjBhI8MkbyCSRPqTrQY3ibYTE31P3b11f/wBjChG2aL+ZGUYzFZdvGef5p8//ALzQgtBcYI7UV8Dchh39PX9fjQfg3os4K5kGO1BvODuZkVu6g/UVIqt4DcnD2j/lH9qsgaBRSikpaBDTGp9Nagrr4kGhXmMgKZOnf301oqudaFebbOa2wzZWYeQn4c28HtO1BjvHD5nAWHBIyjXXpA9f61peD4euRPKSEUIrKzB1CjLKsPPGgMTQbyuhvYsK6k+Cl9ySfgi2UUEAeaLjWyNf99H4YhyggevlH4Mh1B9s1BEuWrtpCA7XbWXVkAGJtiZlQBF0AdIzRPxbVU8x8T8SxZsu63/EFwhlUhbiCCGJ+FXA0K99QANiHHsVUsIEbGTIPp+ulZtxbEtcxgNoQVDXbgB8sqpDXAOkggGN/nQDF+wbT6yTbYBvVdCD/qX+tGdjhrJg7OEtmLmOuN4pGpGGtEgwegIBIn+IivS9wYYpPESBdC5WUiQ6jUabyJ0ZQTqQQdCKvCX72GuECBcS2FMqSApGylgG6yRA1JoPTjHEFxHFMNbRAtuzetLlABkrcUsNNCAFCj2NRuI3VfigDWpBDKV7kq7A9iNqr1z4a6l9RLo4cFpKsZJObUb6zHeop45c/bBjSFNwPnywcm2XLEzGXTegh4xAouJEEXjodwIaNPlUKP11r3xl43He4d3ZmMbDMZgemteMUEniOL8W61yIBPlG+VQIUT10Ak9TJrwXTUaEagjQgjUEHoQfypy+kep/XvTxFBa8R4utxbV4ZkxaMM5gZHAHxzM5iYkerdKkPxOSr5wQybZvhM+ZSJ0GbbuINUfh5tqjssGgNsBx62qw9wDp5QTA3MiDP161Z4XmnDAGbgMzodJG2ummnoeu9Z3YidSB7/8ANXdviVlFg5WP8K2RqfV7jkAeuU+1AXNxFcSviJcsnLmNu2TbzSDoBbd1CtI+Jg/QiJp2FLElmLh2AkwtxWO0NlzJ0qmwWMZbOuDs3s5Z8hZDdRAgljbW2PLFsnTXXUCdUwVzCuQYGHYnLleHtZu2aM6n309qA04fx1hAcT0BXbt7qfeiTCcQVhv9dJ+Y0P4UM4Hl0AA6LOxVio36DUfQ1Kfgt2ZRzPWMqMfU6FWPyFAV2o3HXf1ry4hiVso91jCopdvZVk/lQviMfewuXPDzBMeVgCSNwSJAj3g0Bc0c93sXbNkILNsnzAMXZ4MgFoELIGgGsbxpQUj4gsWdviclj7sSx/E1Cx+E8Mp2e1auA/z21LfR84/00oOmhqbxd1fDYNwIZVvWG/7d3xVP/jiKCPgjRVwYyV66x+VCGFOtFXBr3wqoJYmPruaDdOVz/wBNb9AR9GNW4qk5SYeAFBnIxQ+4AJ/EmrxaBaWkNcKBaa1OppoK27pNDvNGHDWXAGaekwdp8poljpQhxrEMl1rI1QoXg65SI+E9BQBXIKBr+MJWX8NLfbRrozA++QT7Vo2Ew4AAnXaT+TAdaCfsqsgvinMyWtr8hnI+fmNHVyRcIBgEA9N5igGua8ettXMhX0BBMGAJGn3hrv6Gqj7N+EB0v4t1kXSbSTrNtf8AEPsWhf8Atmon2nEhh1IBUE75dNPxP1rR+C4JLWGs20EKttI76rJJ9SSSfegAcLhmtm5a/hMAkTtsY9oPzoc4+5GJDMCZAE+cht9s066jqRptNaLxrDqLuYDVlE/JoH4flQVzi2V7QG2RjHSQU1oInHnlIkEgCR2nXTvpQdiySS3ei61YBw9tjqXzM099dvpQhNB4FKTLXvFNig8gKeq16RXCgl2oUZj9P1+tar7gJJNXPD7Cu1tG1GW4x1OpRCwB9NOlRsSgg+8foUFYVpakuIpjLrQIl1pBk6TGvff61Iu4u41vwyZXNm1EmYjft/Ydq8Ipy0Htw3iN6ySbN17ZJk5WgE92U6N8waKcDz7jFGVstyNSYFt49CAVX/woSRAQTXuEgAjTWg1XDXbWOti9bus8KFZHyhrZBJ1CiCddxoRFZbxnBCzfu29wrmPY+YD5Ax8qNPsiuE4m8k+VrBYjpmW7bUH6O31ql+0jCqnELuWfOttz7lADHp5Z+ZoBWKsywOByx5kxRM9ku2BofdrOnsag5KmYCwGTEzP7uyLiwYGYYmxa1HXy3X+tBBsrrRDwXFZGU9R/Yg1Q2jrU/COZFBvn2cD/AKJZ3LuT7sZ/rRUKEfs1cnCx2On0ouoOFOpBXUH/2Q==', @@ -74,6 +77,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.ledzeppelin.com', }, { + id: 'Pink Floyd', name: 'Pink Floyd', image: 'https://upload.wikimedia.org/wikipedia/en/d/d6/Pink_Floyd_-_all_members.jpg', @@ -97,6 +101,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.pinkfloyd.com', }, { + id: 'The Rolling Stones', name: 'The Rolling Stones', image: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTExIWFhUXGBgXGBcYFx0YHhoYHRgXFxgdFxcdHSggGBolHhcWIjEhJSkrLi4uGB8zODMtNygtLisBCgoKDg0OGhAQGy0lICUtLS0tLS0tLS0tNS0tLy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAL8BCAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAEBQIDBgEAB//EAEMQAAIBAgQDBgMGBAUCBQUAAAECEQADBBIhMQVBUQYTImFxgTKRoSNCscHR8BRSYuEHcoKS8SRTFTNDstIWc5Oiw//EABoBAAIDAQEAAAAAAAAAAAAAAAIDAAEEBQb/xAAvEQACAgIBAwIEBQQDAAAAAAAAAQIRAyESBDFBIlETMmFxBRSBkbHB0eHwQmKh/9oADAMBAAIRAxEAPwDN8F7GqozYskSoZVTlqPj+e1aO5igAEUqAsADKBpoBVV6+S0TJKk/Dpz2PXSgsRYklo1yxofXlXNnmcjZGHuF2nm6AygTppGs/jXLzBEvWQd7V0AHmCjfnSjE44LoWhgQQJj9ipcZxYa2l5GHeIYj+ZGEEHqKLEnotrwaS9jCpytBDKCp1112nrpUQ5zaBgOupjSkOG4qLyZHEaGD57j0qdvieZe6RjnOk7/8AJ0rTGDboF6Qn7QIBeeDOv150lY1o73DV1LP1mevnSvH4S2oOViSACJ59Yrb2VGenYLhsQVMjpFWWVkgdaEtbxTq1w0qM+YGIO1VzS7lqEpK0LcTaIOoijuHYoW7YZVAuK4KtGscweoq3FOtx1zSBzqvH4busoncaj3qOW0icNN+Bl20wqzh8Qog4i0HYaRmAUGBy3rR9m8f3tgLmhlABB8tOlYVLgIhszQIHkJnToN6YcGx4s3QwBA2InlRNWqE1oY9ucH4bd0AfDlYjryn61h7tfUOK2hdtXEEHMDl33BkV8zxFhhqVIExtzpaeqGRKVorD0KtE2KKJbGto6VReFW2DpVd6m+AUX4a73im0xHVSeumlLbikEg7ipFoM0Vi1FxO8HxD4h+dKemGLLgqmKvaqiKplFls0ysNIpYlH4VqOILF2PXelZFPMetJnQztS59wolRrhq4WDUhYFLsYkyqydaINdW1pIGg5+sxr7H5VwtQtjYQ3TOg1yog612qcku4+OGb+Vn1dDopMRrrp5Ug47x/u0HdDUyuYkQD5Dek/F+0DGVUnQmByH6n6Uhe4WJLamsmLpm3chE5JKhlxC0fC5JbOAZP1prxvD23weHvI3iSLdwE6zGkD96GgsPdzYcSPgPTkfyqItd8MqkCNdRG1bIypbM8LnryDYMkxqd6Y4S/kYMpggyD6VT/Cd1IZgdjpRGDVOk+9PxtPaLmmtMbcSxFu6BdLAXGJDrEDbQge31oNbqRBAOnQV5sIh1Jj0il2ItsDCmRNXNJ6JCTiWXrxYgC2qwdxzkVYhuvCDUnz3q/hQP3kVtjqaeYVkS6GFhUIDAEsSM0aT06+1LSSLc23RS/C7YAFxwptmGZfEpOUtAI32I051dj+zs2+9tXluECWA1gbiDOp5nSB1Ok5Tity8JlwZMkAjnIB3mCBty08q7wniF22YVo011OvrVcnZNdg5EM76iaji38Rjy/Ciu5B8QgBoMA7TIMTrQmJSDWiIuWkDXcfdH/qN86XX8Qx3Ynnvzom/QN2hkSJ5WphwvCPduLbQSzEAClqV9Q4Hg7WAyXLsm+yjL90IzbAjeADqQPKOqpT4oYo2Hp2NsYayWuMLrD4yzFUXSSEC+InbUz6Cdc3xfhVgoLli6Mx1a0fuzsAenrzonG9qc7MFhVk+OCSeh1JgkzzPLUxSW3h2N/MskMCzdCJhiRsBJny0pKyzvYzhES3gQSDuKlg8TlbXY6H0q/iFhpDkEBxmUnmJKz8waDNoVptNC62WY+wEbQyp1BoQqavZ6iTVE4kVSi8OtULV9s0SKo5jBzpZcOtMsQJpXfSgmMiRJFWWSJ2oYOf+akt2OVIlFtG7DnxRabQVecxHImflIH4mg7lWrczMq7SQJOwkxJ+c0x4rwwW2IXvsqgS3dhuUkhgwAXlrEQd6uEGkL6jPGeXkuwnAjU16r8VhQsyWzCNGABMx0c8jXqtr3FfFfgrNdAp3iuy+IU+Be8UiQysNR6EivWOzGIPxKEG8swH4Gg+Pjq+SM/Bg3B7oDFJgOCp6a7H51PDXjZYwQTqNRPlRtjs/4ot4iy1wa5Ad/Q0v4th2S54lgnWOh2NSM4ylQCUoStE8Ri2uEFgNBGmlWWEoG29GWLla8aS0iTbbthF2QKEa8w50Y9zSg7rijkgERt49h5/uaNGOzq4EhspYD0gnX0B+VLHIo3AYG45DIhiQAzeFZOkZjoSZ+ESTOgNAsbk6SDslhcA7IhMHMYRZ1MgmSJ2gR70fc4Y4t3IScoJMfdgyd9fhnT5itFjOErZZEdQQoDITrlBGYToJIEbxFOuNXUFm/csqi95bZs4XKWYwozSIaWI16b9azyUlLiaYwi1Z85tAwJ/etWttV13COhAdSsiRI3B5qdmHmNK8V0rdFUjG9sV36CuIelNb4oC6elJmMigdE9Px/wCa2uL4r3uGs96+YnRm3KEGJb1kGB1rN4vg92zBv/ZEx4WnPHUoASo9YpzwO9YCXVyFityzLsAQbcqXhdQpBB6/CNSJBVODaDjNJ0aHCdn07nPeDu7eLwyW8oXWZ31FRxvAFto7v3oQoXXPACnVBmI2Mx4RJ0GlObeMCAg6soP+316ab+RpN2u44HtW7AaWMOw8Pw65JymCCTI/y+dIXqeja4JaMrj7i5VRSSqAhTqNSQSYPL5a0rd6IvN9NBQbGtCVIV1PBZKx9lr7/X/fBwmpKarJrqmrEF6VcgqhDRVsUSKqz13alN5efXb0p06yPWluMXWpIIXGo1NqiaSQ5FdR9QST0mdYrwUmvMuk1CNEWGpivV5n6aV6oFQTYxlxNEuOo6BiB8gYrl3Eu/xu7f5mJ/E1TR/C+EXLpEDKv8zTHt1qS4x2xKth/Z3xvbRbYzK2c3JM5eYI2pn23uKxBG+/tEGiLeJs4NMttgWM5jzJ18tKzOJ4q7OXLQxEDXYVlheTLz7JDJqo0C2zRVg1EYm42uaZ3NXWieZroRYlotY6ULcBphIih2WTqYHM9ABJPsJNG7ekSMS/A/YqHyg3HErInKs/d6O0aHcCI3Na7sqq3Li3GLMxkh7hDFVEZ4ERbX7sKCWZgNjFYji2Jm6xGgBIA/lVdIHlAplwbFOshWIJIVY5Rvz5ZgR5zXchwx43Apxsf9pOI5MVcuSVW4FClgcgdfjDKCzK3jU+UwRKmVXEePX8SoQ5AoXN4J8XiCSZAJ1YQIFHYrEEL3EHuTowYCGAzLM7r4iTMg6b6zSvCOllhIJ7t7ZCwG+zXvM4M6Ez3fkZO0VyJRcZPL+x0/yuTFhk5rSSr9WbfsdxX+Cs/wAPjjbe1cV2t2rkO1pwCQGQghUePYxpLGF/aq7gQB3CEXCTBQnJEKQWV5OoOhWAd4qj/EazatFQloLcyoCytoIUBbYta5IUA6nYjfllBi2uubjtmYkSfRQo0Hko+VM/CoLqMHxJ+TnqPGVojiDNG9nQLbPimH/kwUkTN1pyH/TBb1y0HdplxDCm3grQaFDsbkE+JyYAKrvkCwCx0zCBOsKjG3sVOWgG/wAUuYhwbzG42sFtY/cUBfwgMgTBO06eU1LhyeNj0Ee53/L50aLf79qU9go1/AMTgjZQ38UGuIB4bhyFSBqsEw8HSdZrHYuwLmKu3bbZlYk5uk6ZYO4EASNOm1LL25PUk/WnXBhAEckWfrM/Ks6xqLtGueZzSTAMRbOpIjU8+UkaeVCNVrYrOTz8/wB/6vpVT00XF2VmvVIWya9EUNjVjlV1ovw5otaX2m1p/wBn+FNiby2gYG7NEwo3PTmPnV2asLXFwfZgy0BilrdrZwVgtbyC9Hha5ccLrromoWfTXz5Uk43wMLb7+202y5XKRDLzGsnMOU6fpfKzGlsxjprVgsACalid/Kp3fhH75f3rPkbTSOjgwQ+HKcvC0V3BoPQH8aouLIorEb+w/OqbR3/ysPmpqsbtWL6yPDI4+yBGrlTG3nA19thXqYZr0aD/AMZRJ7rD216EiTpVGN41iH+JoBAMKI05UoNw1diCwySDqgIkcpMR5b0CwxW6FczjsTrua+5dk7mGs2baKMv2eaVUksQpLsSoObY6n9K+Fo06RvX1zhnGLy2U7xhba7DAagSdCFhWEmTKbieVH2G44pq7M326wFkXzdtEBWVGIUQDcLXATHIlVDbanN1rOExp+5pv2o4ybx7yQS7vB/pXwj6s8eUCkCGihb2NzTxxx8EvV5f9AwGajeEo58go9WP/AMQ1dtmq8WYQeZJ/AD5eL51t6aPLIvoZI6QPfxcXg6/dZXHrIfbpPKn3DnyvaKgkC8yqJjVRcKknqSfoKzmFXPdQciyz0ALAT6ain3B0DHDo40d7jFddf/MCg+R19prU53yZUPmVjfjNi+hzEBckEHMRo0tBOUCQdI31jWBVWCD4lyzKbrF8hMBfs1A+NgugIbVmGgUU+7V4q2uGcZSAzhI2kBcylIMFZiZ6AaVT/h3dJu20Ughg6lSYbKc0uvUrIEdLk/d0wdTPjBtHU6jM3iS/t9/5Be0VlsRjLeHZcjMWLgNm1VCJk8zlby0nnWOwtwwpHRZ+k0/7SY//AKvE3gTKq6CdCGKm3I10IZyfYUiUj7uo29q6nRRccaX/AFX7vZzZPbDrFnO6p/MQNNdzBgCmfaQozlst57oHiZyiqqjRVtWlJZEAEAN4uoBmruxrqMUpIJhLhGUEtmCMRlA1J00iD5jcbPtl/h5duYf+IDs95VBe2TnOUfEEfcsBy5xv15raSMeRtM+UYIwp89fnqPpFGYlgqMeiyPXYfWKL4FgA16GEgSxHLoPaSKH7V5VJAjV4AGmm506SBVSVQsqM05UZ43SBrH78qc8Fb7F25nNH/tH50jxbbCKZ4K6ww+UCTmI9pJNINDZTgkGQGNf3v5n8q61FWcA6KqlWzMfDzzbDwxuZIEfnVtrgmIuXGtLabMrFXnQKQdQxOgPlv0mqbSVsfgVuNeQGyYk0Xwbgd7EsBbU5Jhrh0VRz1OjH+ka1sMF2Vw9i33mJdWjfN4bYPSN31+fSl/Ge3MfZ4RcsQBcYRH/27ewHmem1ZPiubaxr9fB1OqnFqMU9Jf8ArG9jsph7S+IZiIksdyAy6HQiQ0kTEqDyqLcVw9q4LFtMxbQhdQCQqw25Ywig7/CJ2rA3r97EXBndrj6wWO3XfRR8hTHh2FVMtw3/ABgyq21zfDqCzkgKNOQOlFHppS+eTZil1EIPtRteGfZwVVZ0Bk8idYP719SaV9s7xOVSArHUhWzAgSFJ01Op18/lRwnjiG2JuKG5gnJB1JgkRloPGu+Jz4hYKLA1ZQQOXhJBJMToDvT4RfK2DKcVHRmr6eIVC/uP3z/SjLieIaVVxTA3LeVmWAdB8p8XTTl7bg0vJ86OpFpdMn7tfyDYjcegoXPXmbz9/wB7CoVIR4qjB1WZZcjkiA2Ferw2r1GZl2Clwo5t8qsu2AYAY6CNfn5da4t1SN6IvNPT28hEAbxp0qxdIf8AALtlUVABmjxSACTuSCTr5Aa6Vo8FhA0MWZbKXEZzOg1AkTpmg6/XSK+eWbbNoqk+g/E8q244kxs2rTQcigMBsxA+9G7fvc1LvQp41CXxG3RHjvAbN0zbAtZAU8LZ1JDHxEQILbmOdYkKQSDuDB9RvWvxnEgEheZA/T22pAmEFzEtbL5CzeE5S0sxBA01G++tGlRUcjnJv9ihDpRl2wDh1aBJLaxyGgk8hvRvGeAGy6LaLXg6kyEIMgw3g1gQVO533qg4fLZUZpJcyFZTAhdG10g8v7Gt/QNfE/QfJNKjOJKsx8tD55hz9dacYbiBV7VyDNsxvuCH05QfE306VYOFG6C4OVFILMxAgaDbdtWG3PnQzYIyFyuoBJLMCARAjKCAxnWNIMjbWmZZY4SlFNWSFKSb7DHtLxlb3dqilAsswJBl23Og8hTvs7xdLVpXj7REfI0CCzLcTxGZGXMToDMLqvMHhOVSFgMihmcQBnRQXfOefhBAkwOVKcLdN2zbs2Q/fbZSwKlswK5RplmNSx5Vzs0Fl0/ua3nWV8IKlvv7A2IAZVOYlmuuW13ACEfUsCec/O+5hTAYBvlptyIFNMVwu1bTvs7F0dw6BGKJqTla4wA7wZGmAdfYnlh2W6t62pTIwPe5oWQ7owBchTECVEnQ/Fz6X5+EZNRjf1MUnQHgsRcS4r2yy3FIKldweVfYeznbG42GVkwzPcU90QLiqmZQp3MsPCy7yZr5RgUe+6Wgc9y4fuSgLZi2Yu2wCyT4ObanSvomP7NLawjW7Fy6tyRccq7KGYKFbKAZjKAACSPDsK5OfqseN1N1YMsLyepLsI8bjpxV0d3ZwrAkXG+4RLFRIYhLhBXQblD5gr73Zu3i7qpbvlshY3XyBcoDQFXQatJAmQYLTuKL7JpYTEnvrQcIveOGQPLeNcpmdSSpExrOmmuut27F3D3bah8Ozs1xb9lUgTJUHKBAG2UmehE1WbJKUHHG9lYMUV6uOgGz2dw6J3a2EyRBlQ2b/Mx1J86+dLgblvFXsPYQtl7xQdws+K2WY7QG1k60fwfhf26m9fuOe8e2PtGEXbZkLcaZhkAYaiZjyOk41x2zYBGly4ToshQWmOerxpMSND6HmYcWXpsjXLk3/Jvm4ZsdtUkT4fwO2zW7txyWRgVYHdhlCZyfFPgzDkSxoPtR2l/hW7pUzXSAxZtFGadYBlj5GPWg8D2wVLTlkY3c3hUGEykAROvwlQdROvqRjuK425fuNcuGWbpoAOQUclA/ZMmt/wADlTyeBMJKEVDEU8T4ldvtnuuXPKdgP6VGij0oFevyqbVFqbSWkXdL6kht60wvrCe0fgKXMeXtTDFnw+4H50+Gos52d3OKADTjhVzImh1aZMD0IncHKfeTSnLtp++lOuE2hlZn+DU67GAc3sBE/wDNDGajtl5ccpxqIZwqyEtC/e0n4JE6bgjqZ5aaQdjNIuPcTa8/MIPhXptJMcz/AGqeO4ibrjeIMA8h1PKTp+xS++KzrHvnLv8AwdBZLjxXZA5rhrxqJNWUcr1RZ69UoBSVEmSKua2cobkDE+Zkj8D8qnYwdx2CKjMSYAAmT5fMfOjF4deYLbW2xbMZWD8X3RG2xO/nR0KKbTuLcqSoBIJBiZ/MGfmae8Nuwm0j5dDp+NVjABbSrcDyAQwEmMzGJ5D9aEGJy+HKQRuW0nptP6VdCZXLQysd3dxNlG2Z4bWNgSNj1jnzNazE8IT+ON7CLItkIzOZRHCrDKJLOYMa6Ag76RkcPwMXmX/qbFtiVgfasdRpGVGBPQA/pWt4Bg+4GW5eRiULKwDAMMxGXxAHMMv3gOUTSc03GLce5r6eCtKRscVwwMtm8FTMgacqSzfzAsxMzAOw1iZGlfIOO2Yu3O7T7JcsH4cxMCYUeew6edfTuI9prFvCd2rZnZu7G4gsDJk81UMY8h1rCX5ZlCiJgAbzv5+dN6aWRJtvx9vuVmcU6RruzuGt2cP3VxB3h7tLxMwzP4IgaZJOXXeTsIAy/HrVk3fCSIL2wNxNtipHXUaj1A56WYziuK+MIiozspJ2B5A+LMxkM20grI3qHch7lxngq5FwGMniCiT7meQmTQQg07DnKMo8UD2mJBFu3q66hgNnQiOQClWInprSfC8TXD4m3mIGUsDlGYKGOWSdiAJ1UGRtyq/jvGblh2bDTZzaAosRbA2B2YEzy0IaSTtlWVjJ1LHUkmST1JO9NkZk3Z9e4dxrBtbc3rlp2gHKCLhBAKAgqGJYA5RcMGIB51iePcS7y8dz8KJmLHwgBiQ5EvsonoBzpDwuywJJ5a++9HTz/t9Nh6VMcVFqXsMcnLuaj/Dq8q4szEm04X/NKkx/oFz619WVywBHyr4bwFj/ABNmHCfaL4iYgTrr5iR719m4XxFLltWQhhtKmRI0aD0ma8/+NY5KccnjsbelapxBMHg0GJ7q6p7thKkNlHPQneBty3XU5oGT43irlhbjQGFu81vVmtkMAACwXw3ATPhIBiYI3G944gWwbjwI1SZJZgJhANS0AxHODWE4pcxmLAtraUWnAYsTrGYghjPgbw7AMYYSRMVo6PO4YovhS8yel/lg5I8pP1X9O5hMfxW6S/j0uFGYQIJT4JEbid+fOajcxlvEX85t3mvXGDQjqdZB8EoWAHJTMARm50xxmHw+D8Fy2uIxB1YM027QOy/1tH7HOlO091QRZSzYB/7VsKfnrXQeR5PVCP2bdf5oyUo6k/0J8bwfc3Xtfy5eYMEqrFSRoSpJE84mlF0miQ5YSSSdZJ1J5yTzOtDXq0pPirBvegZmNQza1J6qNCXYTaWWUjXX1+VOMNw67fRzatlwvQgagTAkiT5DWlvBe8NwC0md9SFAn/j1Omtb3h2Im7ctBvHbPiCtoOba7GIjTmI30oZZJRVR7lRwRySTbqjJYPBLcdVkhFA7xx1LGAojeI9IbpFbfBYBMhBFthlKeCYyERDToTvtB1rPdqMSchAYK1xpaB8QgAkECJgLLedJcLdvOcvf3SVAyjOYgHaCY08/Khgnk2MnkjhTvZbxTgZw5kNmWSAdjOpgjYmAdRppy2pPdNN+LY5rrAsIyjKANhrJgcvx6k0puim062UmvAMxqoirWqpqCi3IjdWD7da9UsRbKmCNYB+YBH0NeqwDe8JbCqVLXh4CMoa4RoSQ2QBtNYaN/wAaYDAIMViLasfuOoDEfcBktMzOb6e1vZqxZt3GhF+FYlSTJA5mSOf0q8OqYq82YDwWhr/KEf8AetcaWZuUuLfby/sdJY6Suu/9wDiGCzuqIHNx9PiY6jUliW2G+um+ort3smpH2l4kR4WtiCSZ0GaZkQY684ElxhGtWLZysAsS5mSCxzAEiTAmff8App92bwwuxcM5RooPyPMzJBJPpWzpZTfoV/Vv/e4jOor1MR4Ts41yFSzbtyJkpM+ZLBh00j8zV2J7H4gFYuIx1ISFykCJzAW1EbDnuvtrcVh7puyjBRAhoJy8oC5gDILHUGIHU0WLXhAukXCDMlAPkOXOugsaRjeRnyzjFxmy9+pW6jTbIA7twQEhdIGksCJiI1kwtlRJHxArrJ05llPPzG40r6zxnCpftlLgkH5g8iDyYbzXyDtBh/4dij+JlOkeGRyJnaRBgTuKOqBbsqxllgZdiZ8QLGRyBJM7gQPlVnEcfaFle6vsb4OiKqukRqXLSOe2s+0hDxzEG4QM5ygDMsZfGJB2+LSNTVdhQBsNP0FC5eCkrGWH46yW7lt0W4WXwkwkNJIYhRFzX7p00HSkFm6DvvTS+nhWRzGVhsdfErf1Azr09QaUmyAY13oWWN+HgSAZgnX05xT3FYXAZT3dy+H+6XKFdxuAgJ0nY7xWfwY1AqR2ol2GRVk72HgE50MdDr8orSf4f9oRYdrdwgWm8WY/caQs+hkA+x61kLhq2yn2N1+htp/uLP8A/wAvrSs2GGaDhNaCU3B2j7jxK/buFGNwMNMqkiJH3kHMx+FZrtT2ot2Qbdhs93bqE828+i1l+wBJN204hL9oqhOmnwsU9S6CRoSI1jTOKCJBEEGCPMaGsS/C/leSTkl2X2Gfm9NRVAnEmJOYkkk6k8ydST5zVdtTRF8SSPT8qHUwYro0ZBhhwctVXhV+GOhqGIWi8BJC96ngcKbt1LYMF2Cz011MeQk+1cuCp8PxRtXUuDdGDQefUHpIke9KldOgl3Pq/D8DZwtrwAIvMndjsCx5knQeukV8+4hh2wd1mtz8UHMc28kBiIzAgEz5V9HVbWLw4PxW3HuD+TA/uKXXcIMSf4eT3KZRdcjW4wUBArNOw1Jjp1153T5VHk5d/Jrywuq/QyPDb9xhYuYllSzb+AlQblxQZyW15rpGYgCOZoK7iwb5uqgQFmIUbAGYHTpVfaLG95ibjgeFT3aDYBV8IA8pk+9ApePSK34lXq7GOdNOL7BbtMk7nWhbtWBqquU5lWDPVRq1xUCKAlkLvL0r1ducvSvVRZ9I4TehoQeMqiidiSM2Y9FUMJ/Uiq+K3Ft4y4rZmW4EnXlladDMaqI5gA76g08dweLsZrtlXSwsKrrdMxoBnIadWMCeQUbjXOX+JXbrG47sWA3LMTGukknQFzp/Uazx6bhK78b0aZZuWqNh/GIDc8BU5kj7sAKogrHpzERX0Dsef+ksnmUE+vP618jNxLq+BiWQA7eJrQnNKzq6jXciBE7Ct72F4qP4e2pZgrXntpK/EzMXAkE5NWO/tTOnxcJyfuBmnyijVccxDrYud2ftMukbjUSR0IWTWE7D47FG46/FpmZS5yfENc2pV4mNNdZ202tjEgmJJB8Y0A8M6CefLfWqeI3GttmRSQZJUQJjc+ZitdCEzq3MUTrasqvQ3GJPXUJA/tXz/t5YL3lF1MjFdMjhpiAZLKP6YOn6fR8LjFcSD5+28+lfMe1nE0v4hgpAAcAsdRAEctQBzAG6g8hVS8IiMxxcpP2awM7TJDEyF57ldDGgG/nVNs/Leu8StqhyhpIIaI11AOhBII5htjOlSsXcqloBjkRIOvMHcUsgItw7GfizR0MdPf6VS4+E+X4afpRBtlmJUEzsAJJ9I3OoNVusIP8AM3yhajKDsGdZqeIWPQ6g+X68varuF2SLffEjKGiOe0gxERy60fxPDIbdtldSLubKNAVdSAcyyYB0EzroaB5OM1Frv5+vsbceKLwSyKStP5fNe/7mcuNU8Jjrlok23KkiDGxG8EHQiQDrzAPKqrggkHccqqJphhex92UxDtjEcu5bLcliHuHS04XQAsYbKfKKu7SWl743UIKXibiwZgk+IHQQQZkVb2URrNt8SrWhcIy2kd7amJ8T5bhgrIKiASfF8OjUHj+I3cQ695llcwAUBR4nZzoNPiY/SnV6Ng/8tEreGUqCVExvr+tLuJYfKwIGhH12pyqbD2qnjljwIehj5ifyrMpbNMo+kC4ZcAdcwBEwZ6HStKcJbCv9khIAYSOQMN+M+xrLYO3LAdSB9YrS3L7C6ioJMiZjUE5SJOmoJ1OlTInQfTZVDurAHNv/ALFr/bNUME/7Vv8A2ivXpUkHlVDPSuHsbfzEWNuE8ffDnwomQ/EgBWY0BBkwfOP7E43tlmsMti33RJy6kSMwZiygaciJnQkdazmbXXUdKAShjgg3bRkz5m+xWN4HpFSTeoXSAxI21I9/Pn6+VeQ1oRlkX1FqKwqoT4yQP6QD+JFRvEchp5ijBAXWo5KMt2C85RtvVBoLV0O+FKMVOS0+z9wa5XqZcJwK3XYMSIA29wfy+depU80YumSONtWiWDu6fOrWtlw2VZIjNCCMo1ktGjSQJ3jTWlNtz1q9S/Ikct4pzBSfsOcHYSy6kvcS6JbLkETIyycw0OpkbaRtTjC8RCW5shlNprt5JUBBchWQATp8MZTM1jW7z+Y/7jXZcAnOfTMaFJ33D8dj6ta7WIhDNbYE27awxAGoLl5I+DaNZMHQVb/9UPcjIbdsoGYLdB9J0YAL6718jF1v5j86tVz1PzqTi5O02gFJLwfTOLcTfuvs7tlpCyEBBkkA6i4cv3jMiNuhrInBLqSQCQfCrLAbWNt1/X5p0arre4+fy1o4xpbdlOdvsDYtJunY68gI9v39KNt3MqkNa7wGNSSIg8iOeny9aBsakmvYxvF7D9fzqeCgjC4wWyJtAwP5iJPXQ6eggVDGXbbCVQJEaTMzO3U6fvSgwaItEhSRpqBsPM8/T8KhTbGXCwVBYgHQ6Osj4TGeNtJgGhjYU73AD7n0JgE/KfSieGAHrMHXQaRJlYg+tBsoo/BS7kMYASDmBMDNvv8ALpFD4fDZ7iJPxMFkcgTBOvQa+1XMBRfCbYktzAyjyLTt7Bh71SVui3pWH9ocWrlURQEtjKvkAIAB6aD5VXwi0Ac52B/5oJ9d/ei7mPAtLljMQdOmsEmrzyb7eSsSXkaDCkOTQfaKQqCBqzHzlQNuX3/w6Uk74jUk/sURefMlv0Y+5Yj8FFKhDdjZ5LVFvCVm4nWfw1rR3kncCPUVlbBiTVrXfWjlGwIT4odYjBNdVGWMwlHHmvwn0ykfKqTwJ+b2x6t/alN8+Genn5afKgHcdKU4P3GLKvYf4vg5RGY3bfhVjAaSYBMDqdKz1pajm8ql91vSrjGgZysqcADeTP0rtqqUoixuZPI1aBYbhADM6jLcOvUWnK//ALBaruj9iooPCfb8YqSmjKSK+8YfDInQxOvr1qplNEmq7209KEO3VN6Qy7NLDuP6V/P9a9XezgIusDvkn6j9a9XM6m/iM14vlM1YuUfaelCtRdq/XRsVhyKL2MiK4mh/GgzjfKojFE8qqx8s2Nl7W4MV1a53kivTRJmOaV6CLZr1y9FV54FCX2Jq3IBIMwtzlVd1pJPUk0NbJFWW2mqTIyYoiwdI8/y6/Oq0sFtAQTyExPpPOmNmwq4Yl7b53uwrZdAttSGAmJlrmsbd2vpRIFhnDhlRyInI+5ifCSY/D1IoF1B22onCXyisJEZGBJOokEDKp5zGup9KATEsohTFMe0ik6ONbrQ47hRw1vCq3x3M11x0ByhF9QBr5saV8I729etpmYguM0NrlGrx55QxojieLdnZndmaTowzRrPxNJgSRvVwpbJJ3oX4q5lB1A5Cfr9KFDA7H+1V4u6GM/ONvYVSL0bGKTKVsbF1DjQRiGAG/wC4oC5j3010GgHQSTp7mrOJ3yzfEWACgE+QH0Bml1DYLG2F4mPhfqDmHvuPenC2xGkmsjNaDgZd7ZADHKY0nY68vejjL3Kqwx15a6yPoI+v40I+H8j8qZXcIxtgd22bMTm12gaRGhkbz5VTh+Ggkm65tAbHuw8nplLL+dRsP4ckrpi42Y5VW48Jp1fwtnwzigR4RHdMDAAXSHbWBVvFr2FKFbCQ4y5WyuJMjNO8+U9POqRHFVoy6mrbGrCTAJirLOEc7Lz3On4xpUwvcurvlOVlbJM5oYGDGwMRQpkcHV0E2rYyP1An5EflNV2W8vkK7a7SZHlcLYynQo6s+ZSZILFs0HbSKs4XfS7cCIvduxyqC2YEnZQxAIPITM6a0y0ArI3GjcH5VSXB0E6+lau92Jx7CP4W9/8Aiaq7fYnGpr/C3uf/AKftzFC37B8ZCbs8x78zzRv/AHJXqMHDb1vEhSrq5ttoQJgFBtG23KvVizYnKVo0Y3oxIqxGqqpA1qMiZcgk1bmiqbVyK7cuzUoKy23LGBTG3wx+elL8DizbbMpIPlROK4zdfe45/wBRoklWyWcxS5TlqzC5R8e1LTcJOprly4aolhuOxCT4Nuh/WgQ9Vk14GqKsIV61XY60MXiLeHv3LgBBVGXUyJYKJ0UHxa8idtax4anfZ7Gi3dR+asDzEwZiRqKuPcvlqj6d2l7DrZw9w2cSMhIYrc0OVZMWyCFdyYjQEhSBOtYM8LH85/2f3r7HwHtXZxVt2vYeQBsWDSdAI8AKnz69KwHGeI4dLjNlIUk5RqZHKOnSneAYuN0xZ2f4cq3JZtAGMFd9MsDXeGJ/00o49iwzQp0Gg+ZOp5nX8Ko4rxkufAMg9daSNcPWgc9UE0rtFzETvrUe6HWqFbnXWuUsll6kVXdtgnSqWapLdqUS0Ra0aKwXEblkEJpJk7+nI0OLprszUInW0Gnj97+YfX9aqbi94/eoJhXKspyb7sL/APE7v/cP0/SotxG6fvn6fpQteqWUXnF3P52+dSvXGgAkk+etDTVl061CJkM1dDVGvVCWb7sJxPE4lzh+8zEIWUvcC6AgEZmMHfbfStbjMDetAlzbaNTF4NAG5LAECPWvlnZO3mxKAgEQxIIkHwkbe9P+1lu1bsgpZtqxcDMEAIEM2h9hV/cOMqQTf44TfS6tsHIjJBaJkgztIGm34V6sX/GHrXqlILmf/9k=', @@ -119,6 +124,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.rollingstones.com', }, { + id: 'Queen', name: 'Queen', image: 'https://i.scdn.co/image/af2b8e57f6d7b5d43a616bd1e27ba552cd8bfd42', description: @@ -134,6 +140,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.queenonline.com', }, { + id: 'The Who', name: 'The Who', image: 'https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcQoKRCDsHx1DyrnXBxOgpPUHZZIwebCq91Ge8oQylUJXlWzlOwtzQtepNBzfbT_497Ymm1sU1Oq2OV0qGmxa6igziOxHJX4Eo8oyk088RpF', @@ -155,6 +162,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.thewho.com', }, { + id: 'Black Sabbath', name: 'Black Sabbath', image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qFdD0l8GNdMw30NkViSV7BNfUab-7wZXWs1jC3qYIt6llh-WVpJ9MLmgv4QKo9Gq-FOOYJ3-aOr9jQjDhcToYWj2VO9LbBIh7MO6e34PNw', @@ -171,6 +179,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.blacksabbath.com', }, { + id: 'The Doors', name: 'The Doors', image: 'https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcQVPqssHHK-u-EaYkVo8YuiCBINkJYVsAkqIlhojm2swhdc1Xn9lWkzokObi1_lAcr1CZ_dEbKnBTD9hfkUcPJ9vrwg-ARlgurH2rA5I4iwXQ', @@ -188,6 +197,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.thedoors.com', }, { + id: 'Jimi Hendrix Experience', name: 'Jimi Hendrix Experience', image: 'https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcQkDuNxy0xKisGP4lUk7N0QqrOQpyHNy9U2lpQhKBH5BqnRn6drBm8HYWVwcFfea1brVTxs7jSSVH1hjyrwTlzGRZCgfC91C-YgEStSzuvMwg', @@ -205,6 +215,7 @@ export const data: RockBandDataProps[] = [ website: 'https://www.jimihendrix.com', }, { + id: 'Cream', name: 'Cream', image: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTExMWFhUXGBoYGRgYGB0XGhodGhcXFxcdGRgaHiggGB0lGxgYITEhJSkrLi4uGh8zODMtNygtLisBCgoKBQUFDgUFDisZExkrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrK//AABEIALcBFAMBIgACEQEDEQH/xAAcAAACAgMBAQAAAAAAAAAAAAAFBgQHAAIDAQj/xAA/EAABAgQEAwYDCAIBAgcBAAABAhEAAwQhBRIxQQZRYRMicYGRoQcysRRCUsHR4fDxI2IWU6IkMzRDRHKyF//EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwAlV4xnUSm3m0Da7EUgEG5Hh7mF2uxm7JBvYk6wOTVAuTb3JeAMzq9GUlS7GwEB62uCu7cjkLDzJ3iPPmJFspcX2AjlUZiB3SX0/m8AawbGJKRkVLSQOjnreDdQUyimdKPcUWKf1hew2SmnR2kwZpig6U/h5ExGm4kpZbMEOfSAaqnGJcwORtfn4QCk4gEqBJAJ1HK/9RlM5Uy2J1CxorxjtW4P94A9QesA0UdehUrurS/59YZcIrzkZmPTSKuoxlLeGjw6YPiSUsSG2LwDZRYktKmNwPKGCmr0qhORVJV8uvS39x2TPIuGcHzgHlJj2I1Gt0JPSO5MB7AzFsYlyR3jfYCMxLEMoISxV9PGEWtnqWs951M6l8nfSAnV3FExRZNuQH7XjlT1izeYoWDtqRyd4Bzq2XKLJLqOj8yHBJiCawiWpzc3J8NYA9WY8EghLliz8yenK0c1YqEh1Eg+Jt06mE04mElg5O5015fzSOdXWTVDMyUpGj+zB2/uAdRxRMT8pLdf3gvhfFhXqb8jb05xTypyz8xUU/6gv/POJuH4jKll0yVK2da+WjszeDmAupePlIfUfvE2nxkHlFXorMycyWlga5SWIF9FCJUviGUkOFt0LsfNoCxp2JKN0B9j/UBq6tn5TuL7NAPBsd7RJVpcszN5h4PU9ao6F+adx4c4ABPrZirAMrx1bl16Rzn4mUtmNyQD0P8AcHauUlQKmuDe1x1DQicQzFSyQrKS/e6/hPQ6QB//AJHmsSwFucdp2P8AduoDVyduUVlIxElTA/UfnHWZXldlajQtYwDyeIjnzBbhtIKYdjqlr12ZoqgVRK+6bDfaGnBavIkXuq4L9d4C2sIW6nd7fxoPIhBwfEcqQxDn7x/mkPFGXSLuYCUBGR6IyA+PRiBdhBjC6JayCx56c7RC4OwwzqlKWcC5i78K4fSlLsHf6QCdR4B3MygM2ge+XrGtTgqmD947bCLLGGjlbwj1dAlrjSApDEXSshYN7+HhEOmuoMwP3XZvAvDzxhhySoM1x/PeEcpax2MATRLUpRSUhChe3yny2g3h1aFDKb2b02eA+G1+ZJlqdx3knfqI6005KFlOxVmv1gN59NmXmQSPxAlm8+UF5FQMrZhb8Jf9jEZS0JfQuN9NN4DqqkhVgBfbT94BwpArLmChby9oKS6gljv1DGFg1qclrkjb9YlYbiCkkA94PaAtCgq2QATEPEeIUpdKWJvp0+g6wl1+OFMs5DdlEnYAa/31hGreJZjlKH6nc+PIdIB2xTiJrFQCXc9Tf+NtCzUY7MWciDlBNxqb8+sA5BUogrJMxVgCLIG5bnyiSqYJScsvvLIueT7DrAZWVOU8iLku5c6frHGdPWoJcs4MdZGHZiQW0zKJ2Yf2Yj4nND2By6DZ20YbCA3TMALAFh6nwjAlc0upVh6AX9TtHGikqX31gJCdBt19PrE6oKUnIksEgW5kh/eA0OQCwZPM6n8z9Ik0NOlQKilkjSzdXflA5EkrWl+g6XIAgjjWIFLpT3QLWGwtbrADsVrVqOUem9+mwt5xF7FYQUvYEC+gJd/pEnDmSRMNyo2HU8+Z0iUkBQPJ2bmdz5WgC/DeaUBd0bjn5cxrDpQpCklctRUBs7keI/OEbDVlMzIN7j1D225wYw2pXJmKUjQEFtilQdj4G3lAH5NW5UEuCLF/HrrCLxiDmJmF1E7eFriLBNOiekTZasr+j8i3X6QkcVYbNQFKUXBfKAXfSzne8Ah9oXygteJilrSDd1EX5geEQ1ylJAWNXLHw+t40plrzBg5J994DvTKWo5QCx1tDGiYJZCQrYWO0RKOpQGBUytLAfWNKlAzOltXfk3WAaqLEFuGudRsB67Q+8O8QKAaZc6vpFQ0lQc1j69fHrB2gxYhkJJcfNs+2vKAuVHEErctGRXdNjCcrZQW1L/tHsAk/CSQ9Qo/6xedMkBLRR3whm99Y3tF6UibB4D1IMaVEtTW1ialIjZSAYCr+N6dQUCE23OvWECdSla+4Rn3SSz72P5ReOP4N2oZ2in+KMCMqbADqrD5iUpUzKTqG28IhKrDnzaGCCMWUhGVaiWte9uTmB85KVkqTz/ggJ8mqz2OsbJlqfvDu+/7RGprDkfaJPapBIU/XrAczMIUQLX0P5QbpVZXubByoNZ7nzv7wuqSkHMlyH8Wjeoqj2YB0J7zWzAaOfGAl1tbnJN8pYeQ0/WIktGdQRJSMx3Op5noBHCjSZyr+XIf1DHRolUcszFEKWQyR01PqW9IDmvD+zGVJdTDMd9DYeNzHOXQ9jMJJCrP0FiSSfLXpHKXXqKHUe8pbq8wH9BaOGO4iCpSU/Kf+5mYeFoDtV16CjKPl15FXU/68h1iGkpSDMVfpoE+fP3iIqaAHd1FPu+p5ty0jgqYZhAJZKTpzO56k84Av9oBT2irJDMNi10j19hETDZKlrJPzKc6aPqY3z5hpYBkp2H6mJ0hIQQ5Ys55trARUz8qybMOnLSI2KLClBWymP89faMxioGgIcjTlENNS6HVZiwP1gJBUUpBF7FmO5LP0tEmVUgBIGrW8SdW8oHSKoKdO5unx6RIoLKc/sGgGCnAlhKn75UD4XEF8PqUqm9mVauB4Akp87n0hTm1Lrz3azHYc7xCRiikzlLBuF5h5O/8AOsBZ2F1/YrKVOxJH9/UGOuLze4op7yVbgsfMFwqFWRjaZlwNRpyYXHhHcYiycuo5A3G501gBfEElAloAIHfUSAlKctkhrWOhPnCzWU5llKg/MFm6xNr67/IVEnVgCHy32JjnVTjMsolntz9ICBLSp7wWo0FnUH5dYhrpTLPe0EETNeUCMxI2a0B0ky+85Ntn+jx0lYhkLgMef0jSZWyuzSnIQvUqBP0iGs9HHv5wB6kkKWnNn16tHsc6KvyoACj6CMgJHwhXLliZNmFgC1/aLFmcdSA4Cgw3e0Uhh8qZ9jJQ7FZdojqw6cQe6pwCo2sB1PPpAXPM+JNOC2d35CC+D8ZSpxYKHrHzgmSo6A89ILcPT5iJ0tnZSgm3UtAfRC8cQV5X2eEri6dKmKJUpiNImcRcMLlyFTpSzmSl2PQXilKvFJi1OVEwBqsmIch3jhQlKFH8JgGuoUdY2RUQDWlaD/Nf3iHiOVixci46iB0ueSloiT56nDwE/D68BTH5SbvEmslJKmQ7E2bTz/SBtPhkyYnPLGcD5kpupN906t1EMWEUKyHSgSkjVawSfU/SA1lICVJALZUuS25P9RErJ5nLB2+VI5AQQx0oQCkKuWJV0Y5f50iHgi0JSFki2nkXP5QG2LzwhfZJ0QkAn/fU/wD6gMqcTdt7eH9GOVZXFUxbksSfcvHKXO0G+0BNIJOUcmj2T3bk/rEZM0gO+8b0ySSnxgDmFd8uBYecT1U65s05Q+UeF+UdsEwwsTnygn84cMKn00hDWJ5loCt8cwlaSCxJe5ZmtAyokpTTkk9/MwHT7xizMZxymynMgN/NIXpdbhs0FClFJVa4sPPaAV8IwRc1IXte/hEqTQzJa2mS1EHkHB6iH/hnC5MpJyTgtB2sb8x5Qco6NOZSkkMn6wCDW4SpclMyWkswDMWfTSEOaopUQdQbx9NSJSFIuxilvidgYlT+1lhkqd+TwADCKllDlBmbUM5HL+4U6dZEF6Sozam7f1AR61ehs/Qfy8e0s5h3nJOh5RrOlEnxFvWO0qRoHHnAd5s0lICi42iZhGICWCDe9v0jmqjOU3fe0bYRhqllmJ8BARq6YVqUo78oyjngHvaQWxfCMic0Ly1DSAMrSglwsgGPIHInBt4yAtL4Z4QldAhwLkn3iRjvDc8LzyDlJFwdD4coj/CrFAKVCH6e5EPa57nlAV9K4bqG/wAqpbaMkFzBzD+GJacuZKQ12YawzLCUubPEGmrHU2rmAKYtLenUDplL+kUP/wATCyoJbciPoCvDylDofpFSYROH2lSQ+pgEmvwLs0uZaj1Fx7aQKp6HMe6CIveZhKVaecDZmDyU6pD+EBWNNg5QCoi0AK9LFotPH1y0y2AiqsTX3z4wBDApYzBRmZALsLk+ABt5wUrMTzqUVOUBXdD2ADDQed4V6MnMAPOOsyeQL/zlASa3EjNzHcq9gGHtEIzlANsIm8M0nbLUj73zN9Y1x2hMpYS2odoAdMuXjwRsmUoaiNkJfm8Bsg2hh4cw5SyFAWGkDsMoysgNvFtcLYQkBL2GmkApVtYZQZaSBC7V8RqLhILmL2qOHpE0ATJaVeIhfxbg6UhQmS5CVAfdTY+VoCk66qUq6kt0e+m4/OIqG5RYfEGEyJy85lTUqZiEpZ25hjfqGjrwzwUmbMKjKmBHJbAdG3gF/hGimrmASyT4GLRxaWulpMrFzdSuu94KcPcIy6UhSR3oNcW4aqoop8lFlqlqCfFnHvAUwr4iLR3UhwOcZM4qTXS1yZqAFEd1Q2O0JeK0C5KglTuUhWnP67jyjKCc12uICMzEjcWghIAa+kQAoqUTzL+8EKmeyAlhd3/L6+0B0RNzKH86QVpsORnuXhbppzG4eD+H1jkWvANuG4alSFvytEnA8MXKfKM3QEC3R7RArcQXKQlWUsQ1x6QJHFtQn5CAOTPAPVdgvbSy6QjxIcluloq7GaIyZhSfKCqOJqgllzG8Rb0jTiNKpktMwsoP8w6wC7n6R7GmSMgCfC2OKkJYHQn3ixsK4uzh1RSlPN2EPXA+E/aDZTAG8BaIxRcwMgOG1ELdb8R5NMUyEoUVILLJG738YcaJMillncgX3cxVvFNAiqnrmS0Mo3tv+sA4V/xQp+ytctpCpgONionBSUsoEk+B0hEnYVND9xTDW0PfwwTKlqUJllK3P0gH6XigGsDcTxYMb+cSMbwsqSVSSH1MV5iQmpPeJgOWO1xUVQl1CnVB6rXaF2ZMZUBMw+U2ZXIN6t+TxGqJo19OggpKrAJKgwc2SwZzufIfy8CpySBdiYDpgmJmnnonC+U3HMGyh6RalVw3LxFMuokzBla25G5SRzEUyowX4b4hqKReaSsgH5km6VeI59YBgx7AjImKcqb/AGFz6QDopZVMCQLkwYxvi9VSO8gAnzuwFoH8NozT5Y5qHteAsTAMDSlAIF936w34cgjKwsI8oqU9mGETqSjKS5Pj+cAXpakGJoIMBZCSNb9YJUyoDFUgVqPbWOolpQLMI2XMYPC/i2JjOkFQSh79fOAPy5gVpHdOkBFY5SywM05A6Zh9Iky8epzpNT6wFW8UYHMRNUJkjt0Z1FKg2ZIJJAO8V5xFh5p1BLNnct0BsI+jqkIWokKBsHa/80ijvi4B9uCRomUn3Kj+kAqUaXvyiZNpsyCp77DnE7BsMURobsPUtDQOFVEM9vCAryVTqfQwy8JUSu3Q4cOHhooOEkgZlAmDuCYOEmw3gD/FfDaZ9IlKAAUlJty0MIkvhNMlYK8x6ZXA8TFyyEPLSOgjqqmSRcCAqb/iNNPmZk5iSPkTYPzPIRLxPgXsaOakKe2YdGvFny5aRoAPKIGPqAkTH/CfpAfMpjIxZubbxkAuyjeLBwCpVSALSf8AzEuR4Qgy5Z1a3OH+skhciVMQzBLH+bQHSdxhVKCwmQVDmAbRAwzF8RzPKkqLl+6kGJmHSqoI/wALH94GVUvEUKzCXMSf9d/IQBfEOI8SWCn7EUAjvf4TfqXEA5OMTELBVJUC2gG72LR0VVYspwpNQedjG2E0dYpdpKgdyoX94BmwPjVQIStJCSWL2Ie0dsflCY5S2kBMdw6ZLAz6n2grS0p7EqUdt4BExNTEsekBQrvc4n4grvEdYidhcMYArhaAsh02HUt6aeUQ8TcqOw5RKkVRCcocP7RGqkn2gBZESKGSVKCRqbRqUwzfD6g7WqAIskPAR8UwUyUJUX73tZ4I8ASQupS+wLRY/F+AImSfldhy03tCHwfJMmqUDbx8YC6KFACRE9AG0BsPqwUi8FJU02Zm3/m0B2cuwTyvtHfM2unOPZXgGiQpDwEQoMw2cJ+sRMe4el1EkylOncKGoPPrHTE8YlyRdQHmIHHi2nSM02ehA6m/kNYCq8e+G8ySFzDMC23Yv5wCwCjnz1iQlRSAHcubPtFqYjx/QLHZd9SFd0zAGA6sbn0jlQYhhypwRTTAZiQ4swIGoB3IF2gCWFyBTUwQFE/iUrUneKXxuu+1VsyYbgqYf/VPdHqz+cWj8SMSEikmKGqmSm+6rW8A58opXC5zLBPOAsvh+nKlJUQGSzDqLOYdqOnKr7Qm4FNsG35Q9YWhw0BNVSJCLQJoFoSQtawkBZs+sMiZGYMYqrjnhiaZylJmHIS4F7c4C5qatlEAhaWOlxEiRNBfKoEdC8fOWHS5+dMoqWz5dTF7cF4MimpwlJJKu8okuSTAF52kJ3GdaRTzsp0QfpDlOTYxXfH8zJTTE7qtAUsEmMjuERkAQrqCXLp0y8veK1F/VvpAOTWlA7Mu3KLF4jwQqkpWLKSSfQl4Q8bw4gBY3gLH4QxqQmSHZ0/WDtXxPJ+UMTbyeKFkVakGxIjFYosFwoudYC8qnjCVLcaveJMniWnUAqzNc8niglYksi5JjpLxRaQADblAWZxfj0orAQoEa87wrY1xUVS+zQIWBPJudTGqZTmAlYbh8yepkpKlE7QRpEopzNRUSiZjd0HY84tX4ZYJKRTImBlKU5UdweXSDHE3CFPVsZiWUNFCx/eAoCUXUw3P5xPxaiUQWD5ACttnFreRPlFtYD8L6aWsLUpayDZyw/eGap4XlZ86EgBUvs1pYd4Bwk31IClC/OA+YFGLP+CuGuZ048wgeQc/WBmLfDmb2j04JSqbMQAoMU5FZb9LEv4Ra/BnDwo5CZQ1F1HmTrAFKimduW4bWK641wnslJmoDNYt1i1UyrQF4koBNllJG0BXWEY6zObi0NmHYySRsOTi8VTjFIqSop22idg2NZWCngLnpcQsCD5R4cWKiUuB53hRwbHkqDD00PW0MaKRM4XAt6uPCAhV/C9PUOZ+db6d4oI8MrPC9P4EwlbgTZwUP9ySOjHUw8DD5jd1rDkxH6whcV8JVSiZkqWSrcg38oAPXfClSnNLVoWNkrsfAlP6QoLoavD6hHaSyFJLpI7yTqLEW5xInUtdIUX7VHqH84YpOJLXTkVC3LedruTAAuPscXORTSj91HaEdVWT6JH/AHQrUyrx0xSr7aapexskckgMn2Eb0aQ+kA78IYkQoJMWlhM0BtYqXAZyEkKa4h5w/FXcPaAbarHQh7gAWeF/EMfpSHnTkjoLn0EC5OFJmzM09ZUl7JdvpePKnD8MlFzJY9XUPeAk0/EGFA9o68ydm16w04dx7QlLIm5T+FQI9IR5OO4clX/okE88oMPHDlbSTQCmnQnqUAeloBqoq5E5AUgggxTvxMxN5ikDQFv1i06dcuWshAYHYaRUHH8xE2rMuUB3dW3UdYAJhYlZP8iXL82sw/eMi7eCeHBJo5aJ0qUpd1EsFFlFw5I1vHsBFThHaJWldg5b6veEDiPAhJzIJzC5SdPERatNOJJBhe43w4zJRUgXTfx5wHzzXU2VZfR4jECGDGKcOS0CFoHKAgTCI8SmJhljlG6ZUByRLjqnu3jqmXHtNQTahWSTLUshnYWD2BUo2SOpIgLU+CtcVS50vUJIUOjxZE1BMVRwLKOGzFdqtKysBxKOYDRhmIAPzDp1i2J2KSkmShTpVPAyAAm5G/IdYDemtEHiLFZskyEyhLzTZmR5j5QMpVtuWtBaUZYmdmT32zNzHMDk5gdxVhqZ0q6M+QlWQ6KsXHjuOoEARRKGpYnm0a9neAfANb2lMEEKGRsuZnKDdBLE3A7p6pMMi0QGZbRHqKdwYmoTHikwFWcZ4JmJIDxWFbSZFaNH0NjVGFQgY9w2lb7QFbU09aC6SYb+HeNFSy0z1hcxTClyj0gQqbeAvjDuNJZ19Xhgk8QSVJfMI+cKacQQyiIdsK4LxGakLHdBDgrVlcG4tqIC1Z1VT1CSnuqG/SKW+LEyTJUKeTqrvL/1D2A5Al7dI6Y7VV1AQicCl/lIuFeChY/WEKumzJ8xcxRKlG5JgIcrWCqKcgAwMpqdS1hCElSibAaw4UVDMSOzmoKFgOx3EAIp55SYKUuJKcMq8Qq+jymIiC0BaGAI7axUz9YcKThWQR3zm8YpfCsbmSS6TDJI46nNAWhSYBSJLCUjxa8G0YNJbuhvCKY/5fN1zGC+GcW1MyySYB+rqaYcyKcAr0zEsEPuf2hJ/wD53WypnaDJNcuSlTG5vZTRY3CkkiTmV8yy5P8APODiRAQ6NU1SASgS9spOY2G5FoyJ4EZAKeG1ecJUxAWkEOGfy25x0xWR2iCgFs1oCY5xOino/tKxcMAgG+c6J6X9o84H4iVWSkzJiAklRZtxqk+n0gK242wE083K7hQcH6iEqfKaL8+JeFCdSlYHelHN5fe9r+UUtIwybUTOzkIVMUT90aPzOiR1MAGTLiZTUylqCEJUpR0ADk+QiyuH/hCsgKq5uQf9OWxPms2Hk8WVgnDlNSJaRKSl2dWqi3NRvAVbwx8KZk1l1ilSkf8ATS2c+JuEj1PhB/i6gpqSmTR08sIStQdjctclRJzLLPryiyMsIXxKpWVJnEsgKyENpmSpObUaPvs/OARqanM2clCZikhSkJY2IcgDzPd03i5ZtbJE1Ehjn0T3VMGD2WzAsNHG0VdhakyKwKKgpl5iLF0HL3ksByQphtpFiq4iRL7k0BBKVLlqBdEwAP3Dza7HrrACOJKycVidJkTCZLtODHdlJMpwpSbEP7wz4diCZ0tMxNgpKVB/9khX5t5QCqcRM+np6qVYqUApOZu6p0zGcOopDlmctChxbNrO0lmhkTkokgZTkUm4GV8pHecOCGe8A210gUdSmpDiTMOSYHOVBUfmyizFTEk6X5mG6WoEAguIR8K4iFdTrp5yTT1SkKGRaSl3Hzywsd4dLke8bcB41/8ADVKyTJIZd99Ab3Vm1fqOcA9iNFxpnjhMqdWBtAcK7rCxi6mL7QdqVKOunjCvjgUxvaATuIpgUDziu6uT3rQ14ytTnUwDkyCtWkAb+HuA9vUS83ygufK5j6JQLMNIrr4W0ASlc1uSQfc/l6xYIWWtAV78bMdTKpRTAArn3vfKlJDkdSbDzikAsiWWbvW1v6RYfxwlNUylkuVSyG5ZVbesJNTgcxFMKhQDKUAL3DuzpaAMfDaZSS5kxdRNEs5WSSPVrawa4u4rpJhlIp0qOU3mqDODsBrrCDJw6cqWqamWrs0h1KawGmsNHBnAy65ImdoEIz5TZyANTrYvYCA6VNJnBPOF2ppikxZGKcMLo2Tmzo0CyGPgRzhexSgcPAKiBHeU8brkMYkU0l4DtQ0ylG0WDgeHZAkAX3hdwqUEM9yYeaCYEyzMVokE+0B0oeNewrTImEdgwS/4F7nw5iLKlLBuI+WZ1Ypc1SzqpRV6mLm+H/GKFy5cib3VJAQlWx2D8uUBYjxkax5AfPPG2KSp1AQgkE1JN9CA+VudiDG3wvx9CP8AEtQSQoW5gnukeCrEcjAXjFXaCVKTlupSzlYXygOw08OkTOCeA500pnTGSM3+JKrdo3zKO4QPc+4XvUBCwQohlJLjpu/6wJwSpoZSuwkKloJZkggFXg91GEnG+IDKlfZUKzF3mrQfnUSxSlTfKlgH3ZrR3wOX9hNNOrEjtJ6iAVf+ylhtsouH5CAs949eNEKSwIIY3BdwX0bnrHQCAyAnF1B21NMTuzjy19oO5Y0WgHaAoudMQqnC1ghScwKmICW+ZGYfdUoWewdW7GJ9FP8At1IKW+cf5KZRUxP45b82Nn1tpaJfE9OKWtXLPdlVSQAcrhKg+UhLMSCwOtrb3FTqg9lLWJYSUn5kM0sgAyyAL5goKYfhcXIDBvwcaxK0qBqBSyipKjLSFEH5lAoUCSHLmxIdosTFlTKiSj7JPl9qRmGeyZiWILM5DP8Ad0Ni0cuEMaTMQtBZFR86wLAkgDtUjkbO2hvvHLDa5NTTzZapGdcmapMwIZMwXOWcgczckDcKZ9CHvEc6mT2AqUlKc4SibtLmAuO+C6C6WfTV44cS0cn7TKVLnS0VZy9zNlVOQ+njYsYTKmlVU/8AhlVCxlX3O2KgklTsm93N73D8nu3pwtCpkmZX0k4TacJCJiM01ByF0k9m6tbsp/HWAdKGqExL5SkiyknUFn2sbEaRtNQ8K+MzZlNVy6tz9lWgJWLpY97vKSdCxs4GhGrQ3SVhSQpOhAI216HSAEVyikQkY5XAlosTE5Kciio5QASTyADkxUNLiCKpauzCgzOFi7HQ2MAOr0BR0jlJw5g7Q4UfD6iXYARrWUffypGnpAZwridTTz0UypYVTzAFJUBdJUNSdxmBB5RZVLdA8IrHAElWLyWKyg0aZmUEhKSXBJDsXLXD6iG3hzG/80yjmghUtaky1nRaQXSH/Fl9WgFj444ZmppU8B+zmMfBX7gRWvEvE6KiVLky5SpSEsVjM4UQBbwF2i/eOMGNVRTpKSApQBBOgIUCPpHz9xZwpMo8hUUqSpxmT+IXNjpYwHPEOJVLkimlJySgGN7qALjN+kMXw444lUEqbLnS1rzKC05G1ZiC5HIR0+HvCUtaDU1DFLEoSbC2/XTSAODYBUYlUTOxQhOqlFsktPIWBYnl4wBvij4lTaoIQmUmUhK8x72YqZwASwYXjaXUiagKTofWFHHsJXS1EyRMYqQWcaGzgh9oe6niOlqZcmVTSJiFy5YCiyWYAA6F1X36wC7V0/KOCJagQwgpNN7xIpcgLmAIYNQrIClQQ4xxDsqXsx8y7eW8dqesGUFmAhN4mqzMXrYbQEXh+iE6oRLP3vXy6xcdJwnLk1EifLITLSwWFaWFj0JPPdopPDp65cxMxBZSSFA9RpFkzOPJ66YpMi0wFJmXyuR3sobXo9oCz5+PU6FFKpoBGo5RkUxSz1FO56xkBy4C4cNZMNbNAElDS5aHvMVYd4jRLm51P1ZeOcZ+yp7FNpi0s6bdnLe+U81nlpblGRkAK4WwiWhMysnB5FOSoJFytYygOCdE+6i8NOP05xCTQ5XTLnHOpRCSUvLJ8XYqZt2J0jIyAYcGwpVORJCiuUzpKiMySGGVmuLghuRgvLSrMrMzfdI16gjxjyMgO2WPCiMjICrfjfKKZUiaNQsj2zD6H1gJSkz6chBexVMb7ic2d0hYAV3nOr2trGRkBDwqonWnBf8AmlHMk6Ol150qGjES1H152tHhyQmZUKq5bhFVToPIhUtRSoePfF+kZGQEHE+FpyqCaifPM9ctRmyln5wlL91SjYkpcbXMMHBlaZ9JKWp8zFJe5OUlIJO5IAPnHkZAFK+gROQqXMSFJVqD6g+IN4HYGhaQuRMY9mQEEaqQQMpVyLghukZGQHnFtCZlHUpT8ypKwL2+UmKi+FuEzlVSlobsRaY5vldg3XMPQHnGRkBbWNzkU9PNmAfIhSgG5BxAXAuHZiM6ps0ThMAWhWXIRmBdOVywulrmMjIDhw/gBkVkuZMvM7Jco5VdwBIlGWkDe2cu2vlC/wAdSlSa3tEFjMSJqCCRdLi7cmfxjIyAeeG8cFVTBSwywEpmACzqsCOhPpFMfE/E5cyYinlgnsMwUo/eV0HRtYyMgBcniucJPZZU2RkQoWKbMot94kekS+CuMV4fLnJRKStcwghSiQAwa4HzD0j2MgAWN1kyomKnzGzKN2DD0jng8+ZLmpmSlZVpuOvMEbg8oyMgDCsYE6YSpCZebZD5Qd2G14J0WHFREZGQB+tpChIRuYVsVpCkttGRkAyUvC0nsaGao3mzMq03ZQdxcacofOK/s9PTdgadIlLSoJIA7q2sw1HjGRkAg4T2aUNMzu7hm0YNGRkZAf/Z', diff --git a/packages/ui/src/components/Table/stories/utils.tsx b/packages/ui/src/components/Table/stories/utils.tsx new file mode 100644 index 0000000000..45bc6341ad --- /dev/null +++ b/packages/ui/src/components/Table/stories/utils.tsx @@ -0,0 +1,54 @@ +/* eslint-disable no-restricted-syntax */ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { Meta } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import { CellText, type ColumnConfig } from '..'; + +// Selection demo data +export const selectionData = [ + { id: 1, name: 'Component Library', owner: 'Design System', type: 'library' }, + { id: 2, name: 'API Gateway', owner: 'Platform', type: 'service' }, + { id: 3, name: 'Documentation Site', owner: 'DevEx', type: 'website' }, +]; + +// Selection demo columns +export const selectionColumns: ColumnConfig<(typeof selectionData)[0]>[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => , + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { id: 'type', label: 'Type', cell: item => }, +]; + +// Shared meta config for Table stories +export const tableStoriesMeta = { + decorators: [ + (Story: () => JSX.Element) => ( + + + + ), + ], +} satisfies Partial; diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 293e76fdca..6ba44b6697 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -16,14 +16,34 @@ import { CellProps as ReactAriaCellProps, - ColumnProps as AriaColumnProps, + ColumnProps as ReactAriaColumnProps, + TableProps as ReactAriaTableProps, } from 'react-aria-components'; +import type { ReactNode } from 'react'; +import type { SortDescriptor as ReactStatelySortDescriptor } from 'react-stately'; +import type { ColumnSize, ColumnStaticSize } from '@react-types/table'; +import type { TextColors } from '../../types'; +import { TablePaginationProps } from '../TablePagination'; + +/** + * @public + */ +export type SortDescriptor = ReactStatelySortDescriptor; /** @public */ -export interface CellProps extends ReactAriaCellProps { +export interface SortState { + descriptor: SortDescriptor | null; + onSortChange: (descriptor: SortDescriptor) => void; +} + +/** @public */ +export interface CellProps extends ReactAriaCellProps {} + +/** @public */ +export interface CellTextProps extends ReactAriaCellProps { title: string; description?: string; - color?: 'primary' | 'secondary'; + color?: TextColors; leadingIcon?: React.ReactNode | null; href?: string; } @@ -34,10 +54,85 @@ export interface CellProfileProps extends ReactAriaCellProps { name?: string; href?: string; description?: string; - color?: 'primary' | 'secondary'; + color?: TextColors; } /** @public */ -export interface ColumnProps extends Omit { +export interface ColumnProps extends Omit { children?: React.ReactNode; } + +/** @public */ +export interface TableRootProps extends ReactAriaTableProps { + stale?: boolean; +} + +/** @public */ +export interface TableItem { + id: string | number; +} + +/** @public */ +export interface NoPagination { + type: 'none'; +} + +/** @public */ +export interface PagePagination extends TablePaginationProps { + type: 'page'; +} + +/** @public */ +export type TablePaginationType = NoPagination | PagePagination; + +/** @public */ +export interface ColumnConfig { + id: string; + label: string; + cell: (item: T) => ReactNode; + header?: () => ReactNode; + isSortable?: boolean; + isHidden?: boolean; + width?: ColumnSize | null; + defaultWidth?: ColumnSize | null; + minWidth?: ColumnStaticSize | null; + maxWidth?: ColumnStaticSize | null; + isRowHeader?: boolean; +} + +/** @public */ +export interface RowConfig { + getHref?: (item: T) => string | undefined; + onClick?: (item: T) => void; + getIsDisabled?: (item: T) => boolean; +} + +/** @public */ +export type RowRenderFn = (params: { + item: T; + index: number; +}) => ReactNode; + +/** @public */ +export interface TableSelection { + mode?: ReactAriaTableProps['selectionMode']; + behavior?: ReactAriaTableProps['selectionBehavior']; + selected?: ReactAriaTableProps['selectedKeys']; + onSelectionChange?: ReactAriaTableProps['onSelectionChange']; +} + +/** @public */ +export interface TableProps { + columnConfig: readonly ColumnConfig[]; + data: T[] | undefined; + loading?: boolean; + isStale?: boolean; + error?: Error; + pagination: TablePaginationType; + sort?: SortState; + rowConfig?: RowConfig | RowRenderFn; + selection?: TableSelection; + emptyState?: ReactNode; + className?: string; + style?: React.CSSProperties; +} diff --git a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx index 4550d00abb..dd76f7c55a 100644 --- a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx +++ b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx @@ -14,21 +14,21 @@ * limitations under the License. */ -// TODO: Bring useArgs() back when we update Storybook to 9 -// import { useArgs } from 'storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { TablePagination } from './TablePagination'; +const noop = () => {}; + const meta = { title: 'Backstage UI/TablePagination', component: TablePagination, argTypes: { offset: { control: 'number' }, pageSize: { control: 'radio', options: [5, 10, 20, 30, 40, 50] }, - rowCount: { control: 'number' }, - showPageSizeOptions: { control: 'boolean', defaultValue: true }, - setOffset: { action: 'setOffset' }, - setPageSize: { action: 'setPageSize' }, + totalCount: { control: 'number' }, + hasNextPage: { control: 'boolean' }, + hasPreviousPage: { control: 'boolean' }, + showPageSizeOptions: { control: 'boolean' }, }, } satisfies Meta; @@ -39,21 +39,70 @@ export const Default: Story = { args: { offset: 0, pageSize: 10, - rowCount: 100, - }, - render: args => { - // const [{}, updateArgs] = useArgs(); - - return ( - { - // updateArgs({ offset: value }); - // }} - // setPageSize={value => { - // updateArgs({ pageSize: value }); - // }} - /> - ); + totalCount: 100, + hasNextPage: true, + hasPreviousPage: false, + onNextPage: noop, + onPreviousPage: noop, + onPageSizeChange: noop, + showPageSizeOptions: true, + }, +}; + +export const FirstPage: Story = { + args: { + ...Default.args, + }, +}; + +export const LastPage: Story = { + args: { + ...Default.args, + offset: 90, + hasNextPage: false, + hasPreviousPage: true, + }, +}; + +export const MiddlePage: Story = { + args: { + ...Default.args, + offset: 40, + hasPreviousPage: true, + }, +}; + +export const WithoutPageSizeOptions: Story = { + args: { + ...Default.args, + showPageSizeOptions: false, + }, +}; + +export const CursorPagination: Story = { + args: { + ...Default.args, + offset: undefined, + }, +}; + +export const CustomLabel: Story = { + args: { + ...Default.args, + offset: 20, + hasPreviousPage: true, + getLabel: ({ offset, pageSize, totalCount }) => { + const page = Math.floor((offset ?? 0) / pageSize) + 1; + const totalPages = Math.ceil((totalCount ?? 0) / pageSize); + return `Page ${page} of ${totalPages}`; + }, + }, +}; + +export const EmptyState: Story = { + args: { + ...Default.args, + totalCount: 0, + hasNextPage: false, }, }; diff --git a/packages/ui/src/components/TablePagination/TablePagination.tsx b/packages/ui/src/components/TablePagination/TablePagination.tsx index ebd585a0c3..93ba1e0ef0 100644 --- a/packages/ui/src/components/TablePagination/TablePagination.tsx +++ b/packages/ui/src/components/TablePagination/TablePagination.tsx @@ -15,85 +15,113 @@ */ import clsx from 'clsx'; -import { Text, ButtonIcon, Select } from '../..'; -import type { TablePaginationProps } from './types'; +import { useId } from 'react-aria'; +import { Text } from '../Text'; +import { ButtonIcon } from '../ButtonIcon'; +import { Select } from '../Select'; +import type { TablePaginationProps, PageSizeOption } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { TablePaginationDefinition } from './definition'; import styles from './TablePagination.module.css'; import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'; +import { useMemo } from 'react'; + +const DEFAULT_PAGE_SIZE_OPTIONS: PageSizeOption[] = [ + { label: 'Show 5 results', value: 5 }, + { label: 'Show 10 results', value: 10 }, + { label: 'Show 20 results', value: 20 }, + { label: 'Show 30 results', value: 30 }, + { label: 'Show 40 results', value: 40 }, + { label: 'Show 50 results', value: 50 }, +]; + +function getOptionValue(option: number | PageSizeOption): number { + return typeof option === 'number' ? option : option.value; +} + +function isNumberArray( + options: number[] | PageSizeOption[], +): options is number[] { + return options.length > 0 && typeof options[0] === 'number'; +} + +function normalizePageSizeOptions( + options: number[] | PageSizeOption[], +): PageSizeOption[] { + if (isNumberArray(options)) { + return options.map(value => ({ + label: `Show ${value} results`, + value, + })); + } + return options; +} /** * Pagination controls for Table components with page navigation and size selection. * * @public */ -export function TablePagination(props: TablePaginationProps) { - const { classNames, cleanedProps } = useStyles('TablePagination', { - showPageSizeOptions: true, - ...props, - }); - const { - className, - offset, - pageSize, - rowCount, - onNextPage, - onPreviousPage, - onPageSizeChange, - setOffset, - setPageSize, - showPageSizeOptions, - ...rest - } = cleanedProps; +export function TablePagination({ + pageSize, + pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, + offset, + totalCount, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + onPageSizeChange, + showPageSizeOptions = true, + getLabel, +}: TablePaginationProps) { + const { classNames } = useStyles(TablePaginationDefinition, {}); + const labelId = useId(); + const normalizedOptions = useMemo( + () => normalizePageSizeOptions(pageSizeOptions), + [pageSizeOptions], + ); - const currentOffset = offset ?? 0; - const currentPageSize = pageSize ?? 10; - - const fromCount = currentOffset + 1; - const toCount = Math.min(currentOffset + currentPageSize, rowCount ?? 0); - - const nextPage = () => { - const totalRows = rowCount ?? 0; - const nextOffset = currentOffset + currentPageSize; - - // Check if there are more items to navigate to - if (nextOffset < totalRows) { - onNextPage?.(); // Analytics tracking - setOffset?.(nextOffset); // Navigate to next page + const effectivePageSize = useMemo(() => { + const isValid = pageSizeOptions.some( + opt => getOptionValue(opt) === pageSize, + ); + if (isValid) { + return pageSize; } - }; + const firstValue = getOptionValue(pageSizeOptions[0]); + console.warn( + `TablePagination: pageSize ${pageSize} is not in pageSizeOptions, using ${firstValue} instead`, + ); + return firstValue; + }, [pageSize, pageSizeOptions]); - const previousPage = () => { - // Check if we can go to previous page - if (currentOffset > 0) { - onPreviousPage?.(); // Analytics tracking - const prevOffset = Math.max(0, currentOffset - currentPageSize); - setOffset?.(prevOffset); // Navigate to previous page - } - }; + const hasItems = totalCount !== undefined && totalCount !== 0; + + let label = `${totalCount} items`; + if (getLabel) { + label = getLabel({ pageSize: effectivePageSize, offset, totalCount }); + } else if (offset !== undefined) { + const fromCount = offset + 1; + const toCount = Math.min(offset + effectivePageSize, totalCount ?? 0); + label = `${fromCount} - ${toCount} of ${totalCount}`; + } return ( -
    +
    {showPageSizeOptions && (
    + No scheduled tasks found for {selectedPlugin}. + + } + detailPanel={({ rowData }) => { + return ; + }} + /> + )} + + ); +}; diff --git a/plugins/devtools/src/components/Content/ScheduledTasksContent/fixtures/scheduledTasksErrors.json b/plugins/devtools/src/components/Content/ScheduledTasksContent/fixtures/scheduledTasksErrors.json new file mode 100644 index 0000000000..89f81b740d --- /dev/null +++ b/plugins/devtools/src/components/Content/ScheduledTasksContent/fixtures/scheduledTasksErrors.json @@ -0,0 +1,205 @@ +{ + "tasks": [ + { + "taskId": "cool-provider", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT60M" + }, + "taskState": { + "status": "running", + "startedAt": "2025-10-31T21:02:36.461+00:00", + "timesOutAt": "2025-10-31T22:02:36.461+00:00", + "lastRunEndedAt": "2025-10-31T19:57:15.674+00:00", + "lastRunError": "{\"name\":\"error\",\"message\":\"insert into \\\"refresh_state\\\" (\\\"entity_id\\\", \\\"entity_ref\\\", \\\"errors\\\", \\\"last_discovery_at\\\", \\\"location_key\\\", \\\"next_update_at\\\", \\\"unprocessed_entity\\\", \\\"unprocessed_hash\\\") values ($1, $2, $3, CURRENT_TIMESTAMP, $4, CURRENT_TIMESTAMP, $5, $6), ($7, $8, $9, CURRENT_TIMESTAMP, $10, CURRENT_TIMESTAMP, $11, $12), ($13, $14, $15, CURRENT_TIMESTAMP, $16, CURRENT_TIMESTAMP, $17, $18), ($19, $20, $21, CURRENT_TIMESTAMP, $22, CURRENT_TIMESTAMP, $23, $24), ($25, $26, $27, CURRENT_TIMESTAMP, $28, CURRENT_TIMESTAMP, $29, $30), ($31, $32, $33, CURRENT_TIMESTAMP, $34, CURRENT_TIMESTAMP, $35, $36), ($37, $38, $39, CURRENT_TIMESTAMP, $40, CURRENT_TIMESTAMP, $41, $42), ($43, $44, $45, CURRENT_TIMESTAMP, $46, CURRENT_TIMESTAMP, $47, $48), ($49, $50, $51, CURRENT_TIMESTAMP, $52, CURRENT_TIMESTAMP, $53, $54), ($55, $56, $57, CURRENT_TIMESTAMP, $58, CURRENT_TIMESTAMP, $59, $60), ($61, $62, $63, CURRENT_TIMESTAMP, $64, CURRENT_TIMESTAMP, $65, $66), ($67, $68, $69, CURRENT_TIMESTAMP, $70, CURRENT_TIMESTAMP, $71, $72), ($73, $74, $75, CURRENT_TIMESTAMP, $76, CURRENT_TIMESTAMP, $77, $78), ($79, $80, $81, CURRENT_TIMESTAMP, $82, CURRENT_TIMESTAMP, $83, $84), ($85, $86, $87, CURRENT_TIMESTAMP, $88, CURRENT_TIMESTAMP, $89, $90), ($91, $92, $93, CURRENT_TIMESTAMP, $94, CURRENT_TIMESTAMP, $95, $96), ($97, $98, $99, CURRENT_TIMESTAMP, $100, CURRENT_TIMESTAMP, $101, $102), ($103, $104, $105, CURRENT_TIMESTAMP, $106, CURRENT_TIMESTAMP, $107, $108), ($109, $110, $111, CURRENT_TIMESTAMP, $112, CURRENT_TIMESTAMP, $113, $114), ($115, $116, $117, CURRENT_TIMESTAMP, $118, CURRENT_TIMESTAMP, $119, $120), ($121, $122, $123, CURRENT_TIMESTAMP, $124, CURRENT_TIMESTAMP, $125, $126), ($127, $128, $129, CURRENT_TIMESTAMP, $130, CURRENT_TIMESTAMP, $131, $132), ($133, $134, $135, CURRENT_TIMESTAMP, $136, CURRENT_TIMESTAMP, $137, $138), ($139, $140, $141, CURRENT_TIMESTAMP, $142, CURRENT_TIMESTAMP, $143, $144), ($145, $146, $147, CURRENT_TIMESTAMP, $148, CURRENT_TIMESTAMP, $149, $150), ($151, $152, $153, CURRENT_TIMESTAMP, $154, CURRENT_TIMESTAMP, $155, $156), ($157, $158, $159, CURRENT_TIMESTAMP, $160, CURRENT_TIMESTAMP, $161, $162), ($163, $164, $165, CURRENT_TIMESTAMP, $166, CURRENT_TIMESTAMP, $167, $168), ($169, $170, $171, CURRENT_TIMESTAMP, $172, CURRENT_TIMESTAMP, $173, $174), ($175, $176, $177, CURRENT_TIMESTAMP, $178, CURRENT_TIMESTAMP, $179, $180), ($181, $182, $183, CURRENT_TIMESTAMP, $184, CURRENT_TIMESTAMP, $185, $186), ($187, $188, $189, CURRENT_TIMESTAMP, $190, CURRENT_TIMESTAMP, $191, $192), ($193, $194, $195, CURRENT_TIMESTAMP, $196, CURRENT_TIMESTAMP, $197, $198), ($199, $200, $201, CURRENT_TIMESTAMP, $202, CURRENT_TIMESTAMP, $203, $204), ($205, $206, $207, CURRENT_TIMESTAMP, $208, CURRENT_TIMESTAMP, $209, $210), ($211, $212, $213, CURRENT_TIMESTAMP, $214, CURRENT_TIMESTAMP, $215, $216), ($217, $218, $219, CURRENT_TIMESTAMP, $220, CURRENT_TIMESTAMP, $221, $222), ($223, $224, $225, CURRENT_TIMESTAMP, $226, CURRENT_TIMESTAMP, $227, $228), ($229, $230, $231, CURRENT_TIMESTAMP, $232, CURRENT_TIMESTAMP, $233, $234), ($235, $236, $237, CURRENT_TIMESTAMP, $238, CURRENT_TIMESTAMP, $239, $240), ($241, $242, $243, CURRENT_TIMESTAMP, $244, CURRENT_TIMESTAMP, $245, $246), ($247, $248, $249, CURRENT_TIMESTAMP, $250, CURRENT_TIMESTAMP, $251, $252), ($253, $254, $255, CURRENT_TIMESTAMP, $256, CURRENT_TIMESTAMP, $257, $258), ($259, $260, $261, CURRENT_TIMESTAMP, $262, CURRENT_TIMESTAMP, $263, $264), ($265, $266, $267, CURRENT_TIMESTAMP, $268, CURRENT_TIMESTAMP, $269, $270), ($271, $272, $273, CURRENT_TIMESTAMP, $274, CURRENT_TIMESTAMP, $275, $276), ($277, $278, $279, CURRENT_TIMESTAMP, $280, CURRENT_TIMESTAMP, $281, $282), ($283, $284, $285, CURRENT_TIMESTAMP, $286, CURRENT_TIMESTAMP, $287, $288), ($289, $290, $291, CURRENT_TIMESTAMP, $292, CURRENT_TIMESTAMP, $293, $294), ($295, $296, $297, CURRENT_TIMESTAMP, $298, CURRENT_TIMESTAMP, $299, $300) - value too long for type character varying(255)\",\"length\":99,\"severity\":\"ERROR\",\"code\":\"22001\",\"file\":\"varchar.c\",\"line\":\"637\",\"routine\":\"varchar\"}" + }, + "workerState": { + "status": "running" + } + }, + { + "taskId": "some-provider", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT24H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T21:02:34.850+00:00", + "lastRunEndedAt": "2025-10-31T21:02:35.556+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "three-provider", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT30M", + "timeoutAfterDuration": "PT2M" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-10-31T21:16:04.145+00:00", + "lastRunEndedAt": "2025-10-31T20:46:09.184+00:00", + "lastRunError": "{\"name\":\"TypeError\",\"message\":\"String.prototype.replaceAll called with a non-global RegExp argument\"}" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "a-provider", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT60M", + "timeoutAfterDuration": "PT24H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-10-31T21:16:04.152+00:00", + "lastRunEndedAt": "2025-10-31T20:16:08.713+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider123:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.153+00:00", + "lastRunEndedAt": "2025-10-31T18:18:56.176+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider123:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.156+00:00", + "lastRunEndedAt": "2025-10-31T18:16:04.264+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider234:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.157+00:00", + "lastRunEndedAt": "2025-10-31T18:16:08.564+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider567:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.154+00:00", + "lastRunEndedAt": "2025-10-31T18:16:06.978+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider910:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.160+00:00", + "lastRunEndedAt": "2025-10-31T18:16:08.573+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "github-provider:provider000:refresh", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT24H", + "timeoutAfterDuration": "PT1H" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-11-01T18:16:04.161+00:00", + "lastRunEndedAt": "2025-10-31T18:18:54.339+00:00" + }, + "workerState": { + "status": "idle" + } + }, + { + "taskId": "catalog_orphan_cleanup", + "pluginId": "catalog", + "scope": "global", + "settings": { + "version": 2, + "cadence": "PT30S", + "timeoutAfterDuration": "PT24S" + }, + "taskState": { + "status": "idle", + "startsAt": "2025-10-31T21:06:35.672+00:00", + "lastRunEndedAt": "2025-10-31T21:06:09.106+00:00" + }, + "workerState": { + "status": "idle" + } + } + ] +} diff --git a/plugins/devtools/src/components/Content/ScheduledTasksContent/index.ts b/plugins/devtools/src/components/Content/ScheduledTasksContent/index.ts new file mode 100644 index 0000000000..dff8bcf653 --- /dev/null +++ b/plugins/devtools/src/components/Content/ScheduledTasksContent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { ScheduledTasksContent } from './ScheduledTasksContent'; +export { ScheduledTaskDetailPanel } from './ScheduledTaskDetailedPanel'; diff --git a/plugins/devtools/src/components/Content/index.ts b/plugins/devtools/src/components/Content/index.ts index f5bd64f7c0..964e7116d9 100644 --- a/plugins/devtools/src/components/Content/index.ts +++ b/plugins/devtools/src/components/Content/index.ts @@ -17,3 +17,4 @@ export * from './ConfigContent'; export * from './InfoContent'; export * from './ExternalDependenciesContent'; +export * from './ScheduledTasksContent'; diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx index b4e5d3db5b..efbed90737 100644 --- a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx +++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx @@ -19,13 +19,16 @@ import { devToolsInfoReadPermission, } from '@backstage/plugin-devtools-common'; -import { ConfigContent } from '../Content/ConfigContent'; +import { ConfigContent } from '../Content'; +import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha'; import { DevToolsLayout } from '../DevToolsLayout'; -import { InfoContent } from '../Content/InfoContent'; +import { InfoContent } from '../Content'; import { RequirePermission } from '@backstage/plugin-permission-react'; +import { ScheduledTasksContent } from '../Content/ScheduledTasksContent'; +import { DevToolsPageProps } from '../DevToolsPage'; /** @public */ -export const DefaultDevToolsPage = () => ( +export const DefaultDevToolsPage = ({ contents }: DevToolsPageProps) => ( @@ -37,5 +40,19 @@ export const DefaultDevToolsPage = () => ( + + + + + + {contents?.map((content, index) => ( + + {content.children} + + ))} ); diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx index 07a549f8eb..89305b2c1a 100644 --- a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx +++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx @@ -16,9 +16,26 @@ import { useOutlet } from 'react-router-dom'; import { DefaultDevToolsPage } from '../DefaultDevToolsPage'; +import { ReactElement } from 'react'; -export const DevToolsPage = () => { +/** + @public + */ +export interface DevToolsPageProps { + contents?: DevToolsPageContent[]; +} + +/** + @public + */ +export interface DevToolsPageContent { + title: string; + path: string; + children: ReactElement; +} + +export const DevToolsPage = ({ contents }: DevToolsPageProps) => { const outlet = useOutlet(); - return <>{outlet || }; + return <>{outlet || }; }; diff --git a/plugins/devtools/src/components/DevToolsPage/index.ts b/plugins/devtools/src/components/DevToolsPage/index.ts index 88d8bc06fa..a0a4028d10 100644 --- a/plugins/devtools/src/components/DevToolsPage/index.ts +++ b/plugins/devtools/src/components/DevToolsPage/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { DevToolsPage } from './DevToolsPage'; +export { + DevToolsPage, + type DevToolsPageProps, + type DevToolsPageContent, +} from './DevToolsPage'; diff --git a/plugins/devtools/src/components/index.ts b/plugins/devtools/src/components/index.ts index 9938866bcc..9744a4b801 100644 --- a/plugins/devtools/src/components/index.ts +++ b/plugins/devtools/src/components/index.ts @@ -16,3 +16,7 @@ export * from './Content'; export * from './DevToolsLayout'; +export { + type DevToolsPageProps, + type DevToolsPageContent, +} from './DevToolsPage'; diff --git a/plugins/devtools/src/hooks/index.ts b/plugins/devtools/src/hooks/index.ts index bb2e83d06f..105d03fb68 100644 --- a/plugins/devtools/src/hooks/index.ts +++ b/plugins/devtools/src/hooks/index.ts @@ -17,3 +17,5 @@ export { useConfig } from './useConfig'; export { useExternalDependencies } from './useExternalDependencies'; export { useInfo } from './useInfo'; +export { useScheduledTasks } from './useScheduledTasks'; +export { useTriggerScheduledTask } from './useTriggerScheduledTask'; diff --git a/plugins/devtools/src/hooks/useScheduledTasks.ts b/plugins/devtools/src/hooks/useScheduledTasks.ts new file mode 100644 index 0000000000..2797526dec --- /dev/null +++ b/plugins/devtools/src/hooks/useScheduledTasks.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { devToolsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/esm/useAsync'; + +export const useScheduledTasks = (plugin: string) => { + const api = useApi(devToolsApiRef); + + const { + value, + loading, + error: asyncError, + } = useAsync(async () => { + return api.getScheduledTasksByPlugin(plugin); + }, [api, plugin]); + + if (asyncError) { + return { + scheduledTasks: undefined, + loading: false, + error: asyncError.message, + }; + } + + return { + scheduledTasks: value?.scheduledTasks, + loading, + error: value?.error, + }; +}; diff --git a/plugins/devtools/src/hooks/useTriggerScheduledTask.ts b/plugins/devtools/src/hooks/useTriggerScheduledTask.ts new file mode 100644 index 0000000000..d26b7a8025 --- /dev/null +++ b/plugins/devtools/src/hooks/useTriggerScheduledTask.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useCallback } from 'react'; +import { devToolsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; + +export const useTriggerScheduledTask = () => { + const api = useApi(devToolsApiRef); + const [isTriggering, setIsTriggering] = useState(false); + const [error, setError] = useState(); + + const triggerTask = useCallback( + async (plugin: string, taskId: string) => { + setIsTriggering(true); + setError(undefined); + + try { + await api.triggerScheduledTask(plugin, taskId); + } catch (e) { + setError(e); + } finally { + setIsTriggering(false); + } + }, + [api], + ); + + return { + triggerTask, + isTriggering, + triggerError: error?.message, + }; +}; diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 26f0e0dcab..23eeee8fcd 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.4.18 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.4.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## 0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.4.17-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 19a59890b6..dbcca544dd 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.4.17-next.0", + "version": "0.4.18", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 6bbd19241a..07db604cc3 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## 0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## 0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## 0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.2.26-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index d9f8e03771..e0af5fda5c 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.26-next.0", + "version": "0.2.27", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 31981ea98b..4acd9bf71d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## 0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## 0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## 0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.2.26-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index c7b5fd2dc9..cca7630e0d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.26-next.0", + "version": "0.2.27", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md index 5f282d2ec0..3d602aebb2 100644 --- a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-events-backend-module-bitbucket-server +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.1.7-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index dc37cbe32a..452b3262a0 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", - "version": "0.1.7-next.0", + "version": "0.1.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 4880574ae8..4c0750ad67 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + +## 0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + +## 0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + +## 0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.2.26-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 8622eeafae..3d79d8805c 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.26-next.0", + "version": "0.2.27", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 226ed20db3..5cd02bd357 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-events-backend-module-github +## 0.4.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/integration@1.19.2 + +## 0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.2-next.0 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.19.0 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/integration@1.18.3-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/integration@1.18.3-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.2 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 3960c3b032..f3e86d12bf 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.4.6-next.0", + "version": "0.4.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index 9b34d12a50..b97a60c140 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index abbb94cf28..e0d509fa3b 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.3.7-next.0", + "version": "0.3.8", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-google-pubsub/CHANGELOG.md b/plugins/events-backend-module-google-pubsub/CHANGELOG.md index d42abb8dcd..d07530ac08 100644 --- a/plugins/events-backend-module-google-pubsub/CHANGELOG.md +++ b/plugins/events-backend-module-google-pubsub/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-events-backend-module-google-pubsub +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.1.6-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-google-pubsub/package.json b/plugins/events-backend-module-google-pubsub/package.json index 232a6ffec2..d5a97f3223 100644 --- a/plugins/events-backend-module-google-pubsub/package.json +++ b/plugins/events-backend-module-google-pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-google-pubsub", - "version": "0.1.6-next.0", + "version": "0.1.7", "description": "The google-pubsub backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-module-kafka/CHANGELOG.md b/plugins/events-backend-module-kafka/CHANGELOG.md index f36a0d8643..5e830ad6d2 100644 --- a/plugins/events-backend-module-kafka/CHANGELOG.md +++ b/plugins/events-backend-module-kafka/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-events-backend-module-kafka +## 0.3.0 + +### Minor Changes + +- ef5bbd8: Add support for Kafka offset configuration (`fromBeginning`) and `autoCommit` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + +## 0.3.0-next.0 + +### Minor Changes + +- ef5bbd8: Add support for Kafka offset configuration (`fromBeginning`) and `autoCommit` + +## 0.2.0 + +### Minor Changes + +- 2c74ea9: Added support for multiple named instances in `kafkaConsumingEventPublisher` configuration. The previous single configuration format is still supported for backward compatibility. +- 2c74ea9: Added `KafkaPublishingEventConsumer` to support sending Backstage events to Kafka topics. + + This addition enables Backstage to publish events to external Kafka systems, complementing the existing ability to receive events from Kafka. This allows for better integration with external systems that rely on Kafka for event streaming. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + +## 0.1.5 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-kafka/README.md b/plugins/events-backend-module-kafka/README.md index dbc5bf5607..ebc4b5f292 100644 --- a/plugins/events-backend-module-kafka/README.md +++ b/plugins/events-backend-module-kafka/README.md @@ -2,65 +2,182 @@ Welcome to the `events-backend-module-kafka` backend module! -This package is a module for the `events-backend` backend plugin and extends the events system with an `KafkaConsumingEventPublisher.` +This package is a module for the `events-backend` backend plugin and extends the events system with a `KafkaConsumingEventPublisher` and `KafkaPublishingEventConsumer` -This event publisher will allow you to receive events from an Kafka queue and will publish these to the used `EventsService` implementation. +This module provides two-way integration with Kafka: + +- **KafkaConsumingEventPublisher**: Receives events from Kafka queues and publishes them to the Backstage events system +- **KafkaPublishingEventConsumer**: Consumes events from Backstage and publishes them to Kafka queues ## Configuration -To set up Kafka queues, you need to configure the following values: +To set up Kafka integration, you need to configure one or both of the following components: + +### KafkaConsumingEventPublisher Configuration + +To receive events from Kafka queues and publish them to Backstage: ```yaml events: modules: kafka: kafkaConsumingEventPublisher: - clientId: your-client-id # (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. - brokers: # (Required) List of brokers in the Kafka cluster to connect to. - - broker1 - - broker2 - topics: - - topic: 'backstage.topic' # (Required) Replace with actual topic name as expected by subscribers - kafka: - topics: # (Required) The Kafka topics to subscribe to. - - topic1 - groupId: your-group-id # (Required) The GroupId to be used by the topic consumers. + production: # Instance name, will be included in logs + clientId: your-client-id # (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. + brokers: # (Required) List of brokers in the Kafka cluster to connect to. + - broker1 + - broker2 + topics: + - topic: 'backstage.topic' # (Required) Replace with actual topic name as expected by subscribers + kafka: + topics: # (Required) The Kafka topics to subscribe to. + - topic1 + groupId: your-group-id # (Required) The GroupId to be used by the topic consumers. + # Optional offset management settings (these can be omitted to use defaults): + # fromBeginning: false # Start from earliest offset when no committed offset exists. Default: not set (latest) + # autoCommit: true # Enable auto-commit. Default: true (for backward compatibility) + # pauseOnError: false # Pause consumer on error. Default: false (for backward compatibility) +``` + +### KafkaPublishingEventConsumer Configuration + +To publish events from Backstage to Kafka queues, you can configure the `KafkaPublishingEventConsumer`: + +```yaml +events: + modules: + kafka: + kafkaPublishingEventConsumer: + production: # Instance name, will be included in logs + clientId: your-client-id # (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. + brokers: # (Required) List of brokers in the Kafka cluster to connect to. + - broker1 + - broker2 + topics: + - topic: 'catalog.entity.created' # (Required) The Backstage topic to consume from + kafka: + topic: kafka-topic-name # (Required) The Kafka topic to publish to ``` For a complete list of all available fields that can be configured, refer to the [config.d.ts file](./config.d.ts). +### Offset Management + +The plugin supports configurable offset management to control message delivery semantics: + +#### Auto Commit (Default - Backward Compatible) + +By default (`autoCommit: true` or not specified), Kafka automatically commits offsets at regular intervals. This is the original behavior and ensures backward compatibility. + +#### Manual Commit (Opt-in for Reliability) + +When you explicitly set `autoCommit: false`, the plugin will: + +1. Start consuming from the last committed offset for the consumer group +2. Process each message by publishing it to the Backstage events system +3. Only commit the offset after successful processing +4. If processing fails, pause the consumer and do not commit the offset + +**Example configuration for manual commit:** + +```yaml +kafka: + topics: + - topic1 + groupId: my-group + autoCommit: false # Enable manual commit +``` + +#### Error Handling + +The `pauseOnError` option controls how the consumer behaves when message processing fails: + +**Skip Failed Messages (Default - Backward Compatible)** + +By default (`pauseOnError: false` or not specified), the consumer will skip failed messages and continue processing: + +- The consumer logs the error but continues processing subsequent messages +- If `autoCommit: false`, the offset is still committed to skip the failed message +- If `autoCommit: true`, Kafka's auto-commit handles the offset +- Recommended when occasional message failures are acceptable and should not block processing + +**Pause on Error (Opt-in)** + +When you explicitly set `pauseOnError: true`, the consumer will pause when an error occurs during message processing: + +- The consumer pauses and stops processing new messages +- The failed message offset is not committed +- The error is re-thrown and logged +- Recommended when you want to investigate and fix issues before continuing + +**Example configuration to pause on error:** + +```yaml +kafka: + topics: + - topic1 + groupId: my-group + autoCommit: false + pauseOnError: true # Pause consumer when a message fails +``` + +**Note:** When using the default behavior (`pauseOnError: false`) with `autoCommit: false`, failed messages will have their offsets committed, meaning they will be skipped and not reprocessed. Use this configuration carefully based on your application's requirements. + +#### Starting Position + +The `fromBeginning` option controls where the consumer starts when no committed offset exists: + +- `fromBeginning: true` - Start from the earliest available message +- `fromBeginning: false` (default) - Start from the latest message (only new messages) + +Once the consumer group has committed an offset, it will always resume from that position, regardless of the `fromBeginning` setting. + ### Optional SSL Configuration -If your Kafka cluster requires SSL, you can configure it as follows: +If your Kafka cluster requires SSL, you can configure it for both `kafkaConsumingEventPublisher` and `kafkaPublishingEventConsumer` instances: ```yaml events: modules: kafka: kafkaConsumingEventPublisher: - ssl: - rejectUnauthorized: true # (Optional) If true, the server certificate is verified against the list of supplied CAs. - ca: [path/to/ca-cert] # (Optional) Array of trusted certificates in PEM format. - key: path/to/client-key # (Optional) Private key in PEM format. - cert: path/to/client-cert # (Optional) Public x509 certificate in PEM format. + production: + # ... other configuration ... + ssl: + rejectUnauthorized: true # (Optional) If true, the server certificate is verified against the list of supplied CAs. + ca: [path/to/ca-cert] # (Optional) Array of trusted certificates in PEM format. + key: path/to/client-key # (Optional) Private key in PEM format. + cert: path/to/client-cert # (Optional) Public x509 certificate in PEM format. + kafkaPublishingEventConsumer: + production: + # ... other configuration ... + ssl: + # Same SSL configuration options as above ``` ### Optional SASL Authentication Configuration -If your Kafka cluster requires `SASL` authentication, you can configure it as follows: +If your Kafka cluster requires SASL authentication, you can configure it for both components: ```yaml events: modules: kafka: kafkaConsumingEventPublisher: - sasl: - mechanism: 'plain' # SASL mechanism ('plain', 'scram-sha-256' or 'scram-sha-512') - username: your-username # SASL username - password: your-password # SASL password + production: + # ... other configuration ... + sasl: + mechanism: 'plain' # SASL mechanism ('plain', 'scram-sha-256' or 'scram-sha-512') + username: your-username # SASL username + password: your-password # SASL password + kafkaPublishingEventConsumer: + production: + # ... other configuration ... + sasl: + # Same SASL configuration options as above ``` -This section includes optional `SSL` and `SASL` authentication configuration for enhanced security. +These SSL and SASL configurations apply to both Kafka components and provide enhanced security for your Kafka connections. ## Installation diff --git a/plugins/events-backend-module-kafka/config.d.ts b/plugins/events-backend-module-kafka/config.d.ts index 49c417098c..1413f98a49 100644 --- a/plugins/events-backend-module-kafka/config.d.ts +++ b/plugins/events-backend-module-kafka/config.d.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { HumanDuration } from '@backstage/types'; export interface Config { @@ -25,179 +24,594 @@ export interface Config { kafka?: { /** * Configuration for KafkaConsumingEventPublisher + * + * Supports either: + * 1. Single configuration object (legacy format) + * 2. Multiple named instances as a record where each key is a unique name for the Kafka instance */ - kafkaConsumingEventPublisher?: { - /** - * (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. - */ - clientId: string; - /** - * (Required) List of brokers in the Kafka cluster to connect to. - */ - brokers: string[]; - /** - * Optional SSL connection parameters to connect to the cluster. Passed directly to Node tls.connect. - * See https://nodejs.org/dist/latest-v8.x/docs/api/tls.html#tls_tls_createsecurecontext_options - */ - ssl?: - | { - ca?: string[]; + kafkaConsumingEventPublisher?: + | { + /** + * (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. + */ + clientId: string; + /** + * (Required) List of brokers in the Kafka cluster to connect to. + */ + brokers: string[]; + /** + * Optional SSL connection parameters to connect to the cluster. Passed directly to Node tls.connect. + * See https://nodejs.org/dist/latest-v8.x/docs/api/tls.html#tls_tls_createsecurecontext_options + */ + ssl?: + | { + ca?: string[]; + /** @visibility secret */ + key?: string; + cert?: string; + rejectUnauthorized?: boolean; + } + | boolean; + /** + * Optional SASL connection parameters. + */ + sasl?: { + mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512'; + username: string; /** @visibility secret */ - key?: string; - cert?: string; - rejectUnauthorized?: boolean; - } - | boolean; - /** - * Optional SASL connection parameters. - */ - sasl?: { - mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512'; - username: string; - /** @visibility secret */ - password: string; - }; + password: string; + }; - /** - * Optional retry connection parameters. - */ - retry: { - /** - * (Optional) Maximum wait time for a retry - * Default: 30000 ms. - */ - maxRetryTime: HumanDuration | string; - - /** - * (Optional) Initial value used to calculate the retry (This is still randomized following the randomization factor) - * Default: 300 ms. - */ - initialRetryTime: HumanDuration | string; - - /** - * (Optional) Randomization factor - * Default: 0.2. - */ - factor: number; - - /** - * (Optional) Exponential factor - * Default: 2. - */ - multiplier: number; - - /** - * (Optional) Max number of retries per call - * Default: 5. - */ - retries: number; - }; - - /** - * (Optional) Timeout for authentication requests. - * Default: 10000 ms. - */ - authenticationTimeout: HumanDuration | string; - - /** - * (Optional) Time to wait for a successful connection. - * Default: 1000 ms. - */ - connectionTimeout: HumanDuration | string; - - /** - * (Optional) Time to wait for a successful request. - * Default: 30000 ms. - */ - requestTimeout: HumanDuration | string; - - /** - * (Optional) The request timeout can be disabled by setting enforceRequestTimeout to false. - * Default: true - */ - enforceRequestTimeout: boolean; - - /** - * Contains a object per topic for which an Kafka queue - * should be used as source of events. - */ - topics: Array<{ - /** - * (Required) The Backstage topic to publish to - */ - topic: string; - /** - * (Required) KafkaConsumer-related configuration. - */ - kafka: { /** - * (Required) The Kafka topics to subscribe to + * Optional retry connection parameters. */ - topics: string[]; - /** - * (Required) The GroupId to be used by the topic consumers - */ - groupId: string; + retry?: { + /** + * (Optional) Maximum wait time for a retry + * Default: 30000 ms. + */ + maxRetryTime?: HumanDuration | string; + + /** + * (Optional) Initial value used to calculate the retry (This is still randomized following the randomization factor) + * Default: 300 ms. + */ + initialRetryTime?: HumanDuration | string; + + /** + * (Optional) Randomization factor + * Default: 0.2. + */ + factor?: number; + + /** + * (Optional) Exponential factor + * Default: 2. + */ + multiplier?: number; + + /** + * (Optional) Max number of retries per call + * Default: 5. + */ + retries?: number; + }; /** - * (Optional) Timeout used to detect failures. - * The consumer sends periodic heartbeats to indicate its liveness to the broker. - * If no heartbeats are received by the broker before the expiration of this session timeout, - * then the broker will remove this consumer from the group and initiate a rebalance + * (Optional) Timeout for authentication requests. + * Default: 10000 ms. + */ + authenticationTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful connection. + * Default: 1000 ms. + */ + connectionTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful request. * Default: 30000 ms. */ - sessionTimeout: HumanDuration | string; + requestTimeout?: HumanDuration | string; /** - * (Optional) The maximum time that the coordinator will wait for each member to rejoin when rebalancing the group - * Default: 60000 ms. + * (Optional) The request timeout can be disabled by setting enforceRequestTimeout to false. + * Default: true */ - rebalanceTimeout: HumanDuration | string; + enforceRequestTimeout?: boolean; /** - * (Optional) The expected time between heartbeats to the consumer coordinator. - * Heartbeats are used to ensure that the consumer's session stays active. - * The value must be set lower than session timeout - * Default: 3000 ms. + * Contains an object per topic for which a Kafka queue + * should be used as source of events. */ - heartbeatInterval: HumanDuration | string; + topics: Array<{ + /** + * (Required) The Backstage topic to publish to + */ + topic: string; + /** + * (Required) KafkaConsumer-related configuration. + */ + kafka: { + /** + * (Required) The Kafka topics to subscribe to + */ + topics: string[]; + /** + * (Required) The GroupId to be used by the topic consumers + */ + groupId: string; - /** - * (Optional) The period of time after which we force a refresh of metadata - * even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions - * Default: 300000 ms (5 minutes). - */ - metadataMaxAge: HumanDuration | string; + /** + * (Optional) Timeout used to detect failures. + * The consumer sends periodic heartbeats to indicate its liveness to the broker. + * If no heartbeats are received by the broker before the expiration of this session timeout, + * then the broker will remove this consumer from the group and initiate a rebalance + * Default: 30000 ms. + */ + sessionTimeout?: HumanDuration | string; - /** - * (Optional) The maximum amount of data per-partition the server will return. - * This size must be at least as large as the maximum message size the server allows - * or else it is possible for the producer to send messages larger than the consumer can fetch. - * If that happens, the consumer can get stuck trying to fetch a large message on a certain partition - * Default: 1048576 (1MB) - */ - maxBytesPerPartition: number; + /** + * (Optional) The maximum time that the coordinator will wait for each member to rejoin when rebalancing the group + * Default: 60000 ms. + */ + rebalanceTimeout?: HumanDuration | string; - /** - * (Optional) Minimum amount of data the server should return for a fetch request, otherwise wait up to maxWaitTime for more data to accumulate. - * Default: 1 - */ - minBytes: number; + /** + * (Optional) The expected time between heartbeats to the consumer coordinator. + * Heartbeats are used to ensure that the consumer's session stays active. + * The value must be set lower than session timeout + * Default: 3000 ms. + */ + heartbeatInterval?: HumanDuration | string; - /** - * (Optional) Maximum amount of bytes to accumulate in the response. Supported by Kafka >= 0.10.1.0 - * Default: 10485760 (10MB) - */ - maxBytes: number; + /** + * (Optional) The period of time after which we force a refresh of metadata + * even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions + * Default: 300000 ms (5 minutes). + */ + metadataMaxAge?: HumanDuration | string; - /** - * (Optional) The maximum amount of time the server will block before answering the fetch request - * if there isn’t sufficient data to immediately satisfy the requirement given by minBytes - * Default: 5000 - */ - maxWaitTime: HumanDuration | string; + /** + * (Optional) The maximum amount of data per-partition the server will return. + * This size must be at least as large as the maximum message size the server allows + * or else it is possible for the producer to send messages larger than the consumer can fetch. + * If that happens, the consumer can get stuck trying to fetch a large message on a certain partition + * Default: 1048576 (1MB) + */ + maxBytesPerPartition?: number; + + /** + * (Optional) Minimum amount of data the server should return for a fetch request, otherwise wait up to maxWaitTime for more data to accumulate. + * Default: 1 + */ + minBytes?: number; + + /** + * (Optional) Maximum amount of bytes to accumulate in the response. Supported by Kafka >= 0.10.1.0 + * Default: 10485760 (10MB) + */ + maxBytes?: number; + + /** + * (Optional) The maximum amount of time the server will block before answering the fetch request + * if there isn't sufficient data to immediately satisfy the requirement given by minBytes + * Default: 5000 + */ + maxWaitTime?: HumanDuration | string; + + /** + * (Optional) If true, the consumer group will start from the earliest offset when no committed offset is found. + * If false or not specified, it will start from the latest offset. + * Default: undefined (start from latest) + */ + fromBeginning?: boolean; + + /** + * (Optional) Enable auto-commit of offsets. + * When true (default), offsets are automatically committed at regular intervals (at-most-once delivery). + * When false, offsets are only committed after successful message processing (at-least-once delivery). + * Default: true (auto-commit enabled for backward compatibility) + */ + autoCommit?: boolean; + + /** + * (Optional) When true, the consumer will pause on error and stop processing messages. + * When false (default), the consumer will skip failed messages and continue processing. + * Note: When pauseOnError is false and autoCommit is also false, failed messages will still have their offsets committed. + * Default: false (skip errors for backward compatibility) + */ + pauseOnError?: boolean; + }; + }>; + } + | { + [name: string]: { + /** + * (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. + */ + clientId: string; + /** + * (Required) List of brokers in the Kafka cluster to connect to. + */ + brokers: string[]; + /** + * Optional SSL connection parameters to connect to the cluster. Passed directly to Node tls.connect. + * See https://nodejs.org/dist/latest-v8.x/docs/api/tls.html#tls_tls_createsecurecontext_options + */ + ssl?: + | { + ca?: string[]; + /** @visibility secret */ + key?: string; + cert?: string; + rejectUnauthorized?: boolean; + } + | boolean; + /** + * Optional SASL connection parameters. + */ + sasl?: { + mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512'; + username: string; + /** @visibility secret */ + password: string; + }; + + /** + * Optional retry connection parameters. + */ + retry?: { + /** + * (Optional) Maximum wait time for a retry + * Default: 30000 ms. + */ + maxRetryTime?: HumanDuration | string; + + /** + * (Optional) Initial value used to calculate the retry (This is still randomized following the randomization factor) + * Default: 300 ms. + */ + initialRetryTime?: HumanDuration | string; + + /** + * (Optional) Randomization factor + * Default: 0.2. + */ + factor?: number; + + /** + * (Optional) Exponential factor + * Default: 2. + */ + multiplier?: number; + + /** + * (Optional) Max number of retries per call + * Default: 5. + */ + retries?: number; + }; + + /** + * (Optional) Timeout for authentication requests. + * Default: 10000 ms. + */ + authenticationTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful connection. + * Default: 1000 ms. + */ + connectionTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful request. + * Default: 30000 ms. + */ + requestTimeout?: HumanDuration | string; + + /** + * (Optional) The request timeout can be disabled by setting enforceRequestTimeout to false. + * Default: true + */ + enforceRequestTimeout?: boolean; + + /** + * Contains an object per topic for which a Kafka queue + * should be used as source of events. + */ + topics: Array<{ + /** + * (Required) The Backstage topic to publish to + */ + topic: string; + /** + * (Required) KafkaConsumer-related configuration. + */ + kafka: { + /** + * (Required) The Kafka topics to subscribe to + */ + topics: string[]; + /** + * (Required) The GroupId to be used by the topic consumers + */ + groupId: string; + + /** + * (Optional) Timeout used to detect failures. + * The consumer sends periodic heartbeats to indicate its liveness to the broker. + * If no heartbeats are received by the broker before the expiration of this session timeout, + * then the broker will remove this consumer from the group and initiate a rebalance + * Default: 30000 ms. + */ + sessionTimeout?: HumanDuration | string; + + /** + * (Optional) The maximum time that the coordinator will wait for each member to rejoin when rebalancing the group + * Default: 60000 ms. + */ + rebalanceTimeout?: HumanDuration | string; + + /** + * (Optional) The expected time between heartbeats to the consumer coordinator. + * Heartbeats are used to ensure that the consumer's session stays active. + * The value must be set lower than session timeout + * Default: 3000 ms. + */ + heartbeatInterval?: HumanDuration | string; + + /** + * (Optional) The period of time after which we force a refresh of metadata + * even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions + * Default: 300000 ms (5 minutes). + */ + metadataMaxAge?: HumanDuration | string; + + /** + * (Optional) The maximum amount of data per-partition the server will return. + * This size must be at least as large as the maximum message size the server allows + * or else it is possible for the producer to send messages larger than the consumer can fetch. + * If that happens, the consumer can get stuck trying to fetch a large message on a certain partition + * Default: 1048576 (1MB) + */ + maxBytesPerPartition?: number; + + /** + * (Optional) Minimum amount of data the server should return for a fetch request, otherwise wait up to maxWaitTime for more data to accumulate. + * Default: 1 + */ + minBytes?: number; + + /** + * (Optional) Maximum amount of bytes to accumulate in the response. Supported by Kafka >= 0.10.1.0 + * Default: 10485760 (10MB) + */ + maxBytes?: number; + + /** + * (Optional) The maximum amount of time the server will block before answering the fetch request + * if there isn't sufficient data to immediately satisfy the requirement given by minBytes + * Default: 5000 + */ + maxWaitTime?: HumanDuration | string; + + /** + * (Optional) If true, the consumer group will start from the earliest offset when no committed offset is found. + * If false or not specified, it will start from the latest offset. + * Default: undefined (start from latest) + */ + fromBeginning?: boolean; + + /** + * (Optional) Enable auto-commit of offsets. + * When true (default), offsets are automatically committed at regular intervals (at-most-once delivery). + * When false, offsets are only committed after successful message processing (at-least-once delivery). + * Default: true (auto-commit enabled for backward compatibility) + */ + autoCommit?: boolean; + + /** + * (Optional) When true, the consumer will pause on error and stop processing messages. + * When false (default), the consumer will skip failed messages and continue processing. + * Note: When pauseOnError is false and autoCommit is also false, failed messages will still have their offsets committed. + * Default: false (skip errors for backward compatibility) + */ + pauseOnError?: boolean; + }; + }>; + }; }; - }>; + + /** + * Configuration for KafkaPublishingEventConsumer + * + * Supports multiple named instances as a record where each key is a unique name + * for the Kafka producer configuration. + */ + kafkaPublishingEventConsumer?: { + [name: string]: { + /** + * (Required) Client ID used by Backstage to identify when connecting to the Kafka cluster. + */ + clientId: string; + /** + * (Required) List of brokers in the Kafka cluster to connect to. + */ + brokers: string[]; + /** + * Optional SSL connection parameters to connect to the cluster. Passed directly to Node tls.connect. + * See https://nodejs.org/dist/latest-v8.x/docs/api/tls.html#tls_tls_createsecurecontext_options + */ + ssl?: + | { + ca?: string[]; + /** @visibility secret */ + key?: string; + cert?: string; + rejectUnauthorized?: boolean; + } + | boolean; + /** + * Optional SASL connection parameters. + */ + sasl?: { + mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512'; + username: string; + /** @visibility secret */ + password: string; + }; + + /** + * Optional retry connection parameters. + */ + retry?: { + /** + * (Optional) Maximum wait time for a retry + * Default: 30000 ms. + */ + maxRetryTime?: HumanDuration | string; + + /** + * (Optional) Initial value used to calculate the retry (This is still randomized following the randomization factor) + * Default: 300 ms. + */ + initialRetryTime?: HumanDuration | string; + + /** + * (Optional) Randomization factor + * Default: 0.2. + */ + factor?: number; + + /** + * (Optional) Exponential factor + * Default: 2. + */ + multiplier?: number; + + /** + * (Optional) Max number of retries per call + * Default: 5. + */ + retries?: number; + }; + + /** + * (Optional) Timeout for authentication requests. + * Default: 10000 ms. + */ + authenticationTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful connection. + * Default: 1000 ms. + */ + connectionTimeout?: HumanDuration | string; + + /** + * (Optional) Time to wait for a successful request. + * Default: 30000 ms. + */ + requestTimeout?: HumanDuration | string; + + /** + * (Optional) The request timeout can be disabled by setting enforceRequestTimeout to false. + * Default: true + */ + enforceRequestTimeout?: boolean; + + /** + * Contains an object per topic for which a Kafka queue + * should be used as destination for events. + */ + topics: Array<{ + /** + * (Required) The Backstage topic to consume from + */ + topic: string; + /** + * (Required) KafkaProducer-related configuration. + */ + kafka: { + /** + * (Required) The Kafka topic to publish to + */ + topic: string; + + /** + * (Optional) Allow topic creation when querying metadata for non-existent topics. + * Default: true + */ + allowAutoTopicCreation?: boolean; + + /** + * (Optional) The period of time after which we force a refresh of metadata + * even if we haven't seen any partition leadership changes to proactively discover any new brokers or partitions + * Default: 300000 ms (5 minutes). + */ + metadataMaxAge?: HumanDuration | string; + + /** + * (Optional) The maximum amount of time in ms that the transaction coordinator will wait for a transaction status update + * from the producer before proactively aborting the ongoing transaction. + * If this value is larger than the `transaction.max.timeout.ms`` setting in the broker, the request will fail with a `InvalidTransactionTimeout` error + * Default: 60000 ms. + */ + transactionTimeout?: HumanDuration | string; + + /** + * (Optional) Experimental. If enabled producer will ensure each message is written exactly once. Acks must be set to -1 ("all"). + * Retries will default to MAX_SAFE_INTEGER. + * Default: false. + */ + idempotent?: boolean; + + /** + * (Optional) Max number of requests that may be in progress at any time. If falsey then no limit. + * Default: null. + */ + maxInFlightRequests?: number; + + /** + * Optional retry connection parameters. + */ + retry?: { + /** + * (Optional) Maximum wait time for a retry + * Default: 30000 ms. + */ + maxRetryTime?: HumanDuration | string; + + /** + * (Optional) Initial value used to calculate the retry (This is still randomized following the randomization factor) + * Default: 300 ms. + */ + initialRetryTime?: HumanDuration | string; + + /** + * (Optional) Randomization factor + * Default: 0.2. + */ + factor?: number; + + /** + * (Optional) Exponential factor + * Default: 2. + */ + multiplier?: number; + + /** + * (Optional) Max number of retries per call + * Default: 5. + */ + retries?: number; + }; + }; + }>; + }; }; }; }; diff --git a/plugins/events-backend-module-kafka/package.json b/plugins/events-backend-module-kafka/package.json index c8a3064ba4..8532fb1ef7 100644 --- a/plugins/events-backend-module-kafka/package.json +++ b/plugins/events-backend-module-kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-kafka", - "version": "0.1.5-next.0", + "version": "0.3.0", "description": "The kafka backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-module-kafka/report.api.md b/plugins/events-backend-module-kafka/report.api.md index a6b129b82e..c5e6d29c6a 100644 --- a/plugins/events-backend-module-kafka/report.api.md +++ b/plugins/events-backend-module-kafka/report.api.md @@ -6,6 +6,6 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @public -const eventsModuleKafkaConsumingEventPublisher: BackendFeature; -export default eventsModuleKafkaConsumingEventPublisher; +const _default: BackendFeature; +export default _default; ``` diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.test.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.test.ts new file mode 100644 index 0000000000..b0070166e1 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.test.ts @@ -0,0 +1,504 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; +import { Kafka } from 'kafkajs'; +import { ConfigReader } from '@backstage/config'; +import { mockServices } from '@backstage/backend-test-utils'; + +jest.mock('kafkajs'); + +describe('KafkaConsumingEventPublisher', () => { + const mockLogger = mockServices.logger.mock(); + const mockEvents = mockServices.events.mock(); + + const mockConsumer = { + connect: jest.fn(), + disconnect: jest.fn(), + subscribe: jest.fn(), + run: jest.fn(), + commitOffsets: jest.fn(), + }; + + const mockKafkaClient = { + consumer: jest.fn().mockReturnValue(mockConsumer), + } as unknown as Kafka; + + jest.mocked(Kafka).mockImplementation(() => mockKafkaClient); + + const mockConfig = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create instances from config', () => { + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toBeInstanceOf(Array); + expect(consumers).toHaveLength(1); + expect(consumers[0]).toBeInstanceOf(KafkaConsumingEventPublisher); + }); + + it('should return empty array when no config', () => { + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: new ConfigReader({}), + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toEqual([]); + }); + + it('should start all consumers', async () => { + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toHaveLength(1); + + await consumers[0].start(); + + expect(mockConsumer.connect).toHaveBeenCalled(); + expect(mockConsumer.subscribe).toHaveBeenCalledWith({ + topics: ['test-topic'], + }); + expect(mockConsumer.run).toHaveBeenCalled(); + }); + + it('should shutdown all consumers', async () => { + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toHaveLength(1); + + await consumers[0].shutdown(); + + expect(mockConsumer.disconnect).toHaveBeenCalled(); + }); + + it('should handle multiple consumer configs', () => { + const multiConsumerConfig = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'topic1', + kafka: { + topics: ['kafka-topic-1'], + groupId: 'group1', + }, + }, + { + topic: 'topic2', + kafka: { + topics: ['kafka-topic-2'], + groupId: 'group2', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: multiConsumerConfig, + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toHaveLength(1); + expect(mockKafkaClient.consumer).toHaveBeenCalledTimes(2); + }); + + describe('Offset Management', () => { + it('should commit offset after successful message processing when autoCommit is false', async () => { + const configWithManualCommit = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: false, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithManualCommit, + events: mockEvents, + logger: mockLogger, + }); + + mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => { + await eachMessage({ + topic: 'test-kafka-topic', + partition: 2, + message: { + key: Buffer.from('test-key'), + value: Buffer.from(JSON.stringify({ data: 'test-data' })), + offset: '12345', + timestamp: '1234567890', + headers: { + 'custom-header': Buffer.from('header-value'), + }, + }, + heartbeat: jest.fn(), + pause: jest.fn(), + }); + }); + + await consumers[0].start(); + + expect(mockEvents.publish).toHaveBeenCalledWith({ + topic: 'backstage-topic', + eventPayload: { data: 'test-data' }, + metadata: { + 'custom-header': 'header-value', + }, + }); + + // Verify offset + 1 + expect(mockConsumer.commitOffsets).toHaveBeenCalledWith([ + { topic: 'test-kafka-topic', partition: 2, offset: '12346' }, + ]); + }); + + it('should not commit offset when autoCommit is true (default)', async () => { + const configWithAutoCommit = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: true, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithAutoCommit, + events: mockEvents, + logger: mockLogger, + }); + + mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => { + await eachMessage({ + topic: 'test-kafka-topic', + partition: 2, + message: { + value: Buffer.from(JSON.stringify({ data: 'test-data' })), + offset: '12345', + }, + heartbeat: jest.fn(), + pause: jest.fn(), + }); + }); + + await consumers[0].start(); + + expect(mockEvents.publish).toHaveBeenCalled(); + expect(mockConsumer.commitOffsets).not.toHaveBeenCalled(); + }); + + it('should not commit offset when message processing fails and pauseOnError is true', async () => { + const pauseMock = jest.fn(); + const failingEvents = { + ...mockEvents, + publish: jest.fn().mockRejectedValue(new Error('Processing failed')), + }; + + const configWithPauseOnError = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: false, + pauseOnError: true, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithPauseOnError, + events: failingEvents, + logger: mockLogger, + }); + + mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => { + await expect( + eachMessage({ + topic: 'test-kafka-topic', + partition: 2, + message: { + value: Buffer.from(JSON.stringify({ data: 'test-data' })), + offset: '12345', + }, + heartbeat: jest.fn(), + pause: pauseMock, + }), + ).rejects.toThrow('Processing failed'); + }); + + await consumers[0].start(); + + expect(mockConsumer.commitOffsets).not.toHaveBeenCalled(); + expect(pauseMock).toHaveBeenCalled(); + }); + + it('should skip failed message and commit offset when pauseOnError is false and autoCommit is false', async () => { + const pauseMock = jest.fn(); + const failingEvents = { + ...mockEvents, + publish: jest.fn().mockRejectedValue(new Error('Processing failed')), + }; + + const configWithSkipOnError = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: false, + pauseOnError: false, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithSkipOnError, + events: failingEvents, + logger: mockLogger, + }); + + mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => { + await eachMessage({ + topic: 'test-kafka-topic', + partition: 2, + message: { + value: Buffer.from(JSON.stringify({ data: 'test-data' })), + offset: '12345', + }, + heartbeat: jest.fn(), + pause: pauseMock, + }); + }); + + await consumers[0].start(); + + // Should commit offset to skip the failed message + expect(mockConsumer.commitOffsets).toHaveBeenCalledWith([ + { topic: 'test-kafka-topic', partition: 2, offset: '12346' }, + ]); + expect(pauseMock).not.toHaveBeenCalled(); + }); + + it('should skip failed message without committing when pauseOnError is false and autoCommit is true', async () => { + const pauseMock = jest.fn(); + const failingEvents = { + ...mockEvents, + publish: jest.fn().mockRejectedValue(new Error('Processing failed')), + }; + + const configWithAutoCommitSkipOnError = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: true, + pauseOnError: false, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithAutoCommitSkipOnError, + events: failingEvents, + logger: mockLogger, + }); + + mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => { + await eachMessage({ + topic: 'test-kafka-topic', + partition: 2, + message: { + value: Buffer.from(JSON.stringify({ data: 'test-data' })), + offset: '12345', + }, + heartbeat: jest.fn(), + pause: pauseMock, + }); + }); + + await consumers[0].start(); + + // Should not commit offset (autoCommit handles it) + expect(mockConsumer.commitOffsets).not.toHaveBeenCalled(); + expect(pauseMock).not.toHaveBeenCalled(); + }); + + it('should pass autoCommit setting to consumer.run()', async () => { + const configWithManualCommit = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topics: ['test-topic'], + groupId: 'test-group', + autoCommit: false, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const consumers = KafkaConsumingEventPublisher.fromConfig({ + config: configWithManualCommit, + events: mockEvents, + logger: mockLogger, + }); + + await consumers[0].start(); + + expect(mockConsumer.run).toHaveBeenCalledWith( + expect.objectContaining({ + autoCommit: false, + }), + ); + }); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.ts new file mode 100644 index 0000000000..cfb9af8a4c --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/KafkaConsumingEventPublisher.ts @@ -0,0 +1,152 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 } from '@backstage/backend-plugin-api'; +import { EventsService } from '@backstage/plugin-events-node'; +import { Consumer, Kafka } from 'kafkajs'; +import { + KafkaConsumerConfig, + KafkaConsumingEventPublisherConfig, + readConsumerConfig, +} from './config'; +import { Config } from '@backstage/config'; +import { loggerServiceAdapter } from '../utils/LoggerServiceAdapter'; +import { convertHeadersToMetadata } from '../utils/kafkaTransformers'; + +type KafkaConsumer = { + consumer: Consumer; + config: KafkaConsumerConfig; +}; + +/** + * This class subscribes to Kafka topics and publishes events received to the registered subscriber. + * The message payload will be used as the event payload and passed to the subscribers. + */ +export class KafkaConsumingEventPublisher { + private readonly kafkaConsumers: KafkaConsumer[]; + private readonly logger: LoggerService; + + static fromConfig(env: { + config: Config; + events: EventsService; + logger: LoggerService; + }): KafkaConsumingEventPublisher[] { + const configs = readConsumerConfig(env.config, env.logger); + + return configs.map( + kafkaConfig => + new KafkaConsumingEventPublisher(env.logger, env.events, kafkaConfig), + ); + } + + private constructor( + logger: LoggerService, + private readonly events: EventsService, + config: KafkaConsumingEventPublisherConfig, + ) { + this.logger = logger.child({ + class: KafkaConsumingEventPublisher.prototype.constructor.name, + instance: config.instance, + }); + + const kafka = new Kafka({ + ...config.kafkaConfig, + logCreator: loggerServiceAdapter(this.logger), + }); + + this.kafkaConsumers = config.kafkaConsumerConfigs.map(consumerConfig => ({ + consumer: kafka.consumer(consumerConfig.consumerConfig), + config: consumerConfig, + })); + } + + async start(): Promise { + await Promise.all( + this.kafkaConsumers.map(async ({ consumer, config }) => { + const consumerLogger = this.logger.child({ + id: `events.kafka.publisher:${config.backstageTopic}`, + groupId: config.consumerConfig.groupId, + kafkaTopics: config.consumerSubscribeTopics.topics.toString(), + backstageTopic: config.backstageTopic, + }); + try { + await consumer.connect(); + await consumer.subscribe(config.consumerSubscribeTopics); + + await consumer.run({ + autoCommit: config.autoCommit, + eachMessage: async ({ + topic, + partition, + message, + heartbeat, + pause, + }) => { + try { + await this.events.publish({ + topic: config.backstageTopic, + eventPayload: JSON.parse(message.value?.toString()!), + metadata: convertHeadersToMetadata(message.headers), + }); + + // Only commit offset manually if autoCommit is disabled + if (!config.autoCommit) { + await consumer.commitOffsets([ + { + topic, + partition, + offset: (parseInt(message.offset, 10) + 1).toString(), + }, + ]); + } + + await heartbeat(); + } catch (error: any) { + consumerLogger.error( + `Failed to process message at offset ${message.offset} on partition ${partition} of topic ${topic}`, + error, + ); + + if (config.pauseOnError) { + pause(); + throw error; + } + + // Skip the failed message by committing its offset if autoCommit is disabled + if (!config.autoCommit) { + await consumer.commitOffsets([ + { + topic, + partition, + offset: (parseInt(message.offset, 10) + 1).toString(), + }, + ]); + } + } + }, + }); + } catch (error: any) { + consumerLogger.error('Kafka consumer connection failed', error); + } + }), + ); + } + + async shutdown(): Promise { + await Promise.all( + this.kafkaConsumers.map(({ consumer }) => consumer.disconnect()), + ); + } +} diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.test.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.test.ts new file mode 100644 index 0000000000..0f7bff4dbe --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.test.ts @@ -0,0 +1,416 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/config'; +import { readConsumerConfig } from './config'; +import { mockServices } from '@backstage/backend-test-utils'; + +const mockLogger = mockServices.logger.mock(); + +describe('readConsumerConfig', () => { + it('not configured', () => { + const publisherConfigs = readConsumerConfig( + new ConfigReader({}), + mockLogger, + ); + + expect(publisherConfigs).toEqual([]); + }); + + it('only required fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: [ + { + topic: 'fake1', + kafka: { + topics: ['topic-A'], + groupId: 'my-group', + }, + }, + { + topic: 'fake2', + kafka: { + topics: ['topic-B'], + groupId: 'my-group', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConsumerConfig(config, mockLogger); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + expect(devConfig.kafkaConsumerConfigs.length).toBe(2); + + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + + expect(devConfig.kafkaConsumerConfigs).toEqual([ + { + backstageTopic: 'fake1', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-A'], + }, + autoCommit: true, + pauseOnError: false, + }, + { + backstageTopic: 'fake2', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-B'], + }, + autoCommit: true, + pauseOnError: false, + }, + ]); + }); + + it('all fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + ssl: true, + sasl: { + mechanism: 'plain', + username: 'username', + password: 'password', + }, + retry: { + maxRetryTime: { milliseconds: 20000 }, + initialRetryTime: { milliseconds: 200 }, + factor: '0.4', + multiplier: '4', + retries: '10', + }, + authenticationTimeout: { milliseconds: 20000 }, + connectionTimeout: { milliseconds: 1500 }, + requestTimeout: { milliseconds: 20000 }, + enforceRequestTimeout: false, + topics: [ + { + topic: 'fake1', + kafka: { + topics: ['topic-A'], + groupId: 'my-group', + sessionTimeout: { milliseconds: 20000 }, + rebalanceTimeout: { milliseconds: 50000 }, + heartbeatInterval: { milliseconds: 2000 }, + metadataMaxAge: { milliseconds: 400000 }, + maxBytesPerPartition: 50000, + minBytes: 2, + maxBytes: 500000, + maxWaitTime: { milliseconds: 4000 }, + }, + }, + { + topic: 'fake2', + kafka: { + topics: ['topic-B'], + groupId: 'my-group', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConsumerConfig(config, mockLogger); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + + // Client configuration + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + expect(devConfig.kafkaConfig.ssl).toBeTruthy(); + expect(devConfig.kafkaConfig.sasl).toStrictEqual({ + mechanism: 'plain', + username: 'username', + password: 'password', + }); + expect(devConfig.kafkaConfig.authenticationTimeout).toBe(20000); + expect(devConfig.kafkaConfig.connectionTimeout).toBe(1500); + expect(devConfig.kafkaConfig.requestTimeout).toBe(20000); + expect(devConfig.kafkaConfig.enforceRequestTimeout).toBeFalsy(); + expect(devConfig.kafkaConfig.retry).toStrictEqual({ + maxRetryTime: 20000, + initialRetryTime: 200, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + + // Consumer configuration + expect(devConfig.kafkaConsumerConfigs.length).toBe(2); + + expect(devConfig.kafkaConsumerConfigs).toEqual([ + { + backstageTopic: 'fake1', + consumerConfig: { + groupId: 'my-group', + sessionTimeout: 20000, + rebalanceTimeout: 50000, + heartbeatInterval: 2000, + metadataMaxAge: 400000, + maxBytesPerPartition: 50000, + minBytes: 2, + maxBytes: 500000, + maxWaitTimeInMs: 4000, + }, + consumerSubscribeTopics: { + topics: ['topic-A'], + }, + autoCommit: true, + pauseOnError: false, + }, + { + backstageTopic: 'fake2', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-B'], + }, + autoCommit: true, + pauseOnError: false, + }, + ]); + }); + + it('should handle HumanDuration and string values for durations and timeouts', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + retry: { + maxRetryTime: { seconds: 1 }, + initialRetryTime: { minutes: 1 }, + factor: 0.4, + multiplier: 4, + retries: 10, + }, + authenticationTimeout: { hours: 1 }, + connectionTimeout: { days: 1 }, + topics: [], + requestTimeout: '1m', + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConsumerConfig(config, mockLogger); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + + // Client configuration + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + expect(devConfig.kafkaConfig.authenticationTimeout).toBe(3600000); + expect(devConfig.kafkaConfig.connectionTimeout).toBe(86400000); + expect(devConfig.kafkaConfig.requestTimeout).toBe(60000); + expect(devConfig.kafkaConfig.retry).toStrictEqual({ + maxRetryTime: 1000, + initialRetryTime: 60000, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + + // Consumer configuration + expect(devConfig.kafkaConsumerConfigs.length).toBe(0); + }); + + it('single instance configuration (legacy format)', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: [ + { + topic: 'fake1', + kafka: { + topics: ['topic-A'], + groupId: 'my-group', + }, + }, + { + topic: 'fake2', + kafka: { + topics: ['topic-B'], + groupId: 'my-group', + }, + }, + ], + }, + }, + }, + }, + }); + + const publisherConfigs = readConsumerConfig(config, mockLogger); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const defaultConfig = publisherConfigs[0]; + expect(defaultConfig.instance).toBe('default'); + expect(defaultConfig.kafkaConsumerConfigs.length).toBe(2); + + expect(defaultConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(defaultConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + + expect(defaultConfig.kafkaConsumerConfigs).toEqual([ + { + backstageTopic: 'fake1', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-A'], + }, + autoCommit: true, + pauseOnError: false, + }, + { + backstageTopic: 'fake2', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-B'], + }, + autoCommit: true, + pauseOnError: false, + }, + ]); + + // Verify deprecation warning was logged + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Legacy single config format detected at events.modules.kafka.kafkaConsumingEventPublisher.', + ); + }); + + it('offset management fields (autoCommit, pauseOnError, fromBeginning)', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'fake1', + kafka: { + topics: ['topic-A'], + groupId: 'my-group', + autoCommit: false, + pauseOnError: true, + fromBeginning: true, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConsumerConfig(config, mockLogger); + + expect(publisherConfigs).toBeDefined(); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.kafkaConsumerConfigs).toEqual([ + { + backstageTopic: 'fake1', + consumerConfig: { + groupId: 'my-group', + }, + consumerSubscribeTopics: { + topics: ['topic-A'], + fromBeginning: true, + }, + autoCommit: false, + pauseOnError: true, + }, + ]); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.ts new file mode 100644 index 0000000000..327bc57255 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/config.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ConsumerConfig, ConsumerSubscribeTopics, KafkaConfig } from 'kafkajs'; +import { + readKafkaConfig, + readOptionalHumanDurationInMs, +} from '../utils/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; + +export interface KafkaConsumerConfig { + backstageTopic: string; + consumerConfig: ConsumerConfig; + consumerSubscribeTopics: ConsumerSubscribeTopics; + autoCommit: boolean; + pauseOnError: boolean; +} + +export interface KafkaConsumingEventPublisherConfig { + instance: string; + kafkaConfig: KafkaConfig; + kafkaConsumerConfigs: KafkaConsumerConfig[]; +} + +const CONFIG_PREFIX_PUBLISHER = + 'events.modules.kafka.kafkaConsumingEventPublisher'; + +const processSinglePublisher = ( + instanceName: string, + publisherConfig: Config, +): KafkaConsumingEventPublisherConfig => { + return { + instance: instanceName, + kafkaConfig: readKafkaConfig(publisherConfig), + kafkaConsumerConfigs: publisherConfig + .getConfigArray('topics') + .map(topicConfig => { + return { + backstageTopic: topicConfig.getString('topic'), + consumerConfig: { + groupId: topicConfig.getString('kafka.groupId'), + sessionTimeout: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.sessionTimeout', + ), + rebalanceTimeout: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.rebalanceTimeout', + ), + heartbeatInterval: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.heartbeatInterval', + ), + metadataMaxAge: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.metadataMaxAge', + ), + maxBytesPerPartition: topicConfig.getOptionalNumber( + 'kafka.maxBytesPerPartition', + ), + minBytes: topicConfig.getOptionalNumber('kafka.minBytes'), + maxBytes: topicConfig.getOptionalNumber('kafka.maxBytes'), + maxWaitTimeInMs: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.maxWaitTime', + ), + }, + consumerSubscribeTopics: { + topics: topicConfig.getStringArray('kafka.topics'), + fromBeginning: topicConfig.getOptionalBoolean( + 'kafka.fromBeginning', + ), + }, + // Default autoCommit to true to match KafkaJS default and ensure consistency + // between KafkaJS's auto-commit behavior and our manual commit logic + autoCommit: + topicConfig.getOptionalBoolean('kafka.autoCommit') ?? true, + pauseOnError: + topicConfig.getOptionalBoolean('kafka.pauseOnError') ?? false, + }; + }), + }; +}; + +export const readConsumerConfig = ( + config: Config, + logger: LoggerService, +): KafkaConsumingEventPublisherConfig[] => { + const publishersConfig = config.getOptionalConfig(CONFIG_PREFIX_PUBLISHER); + + // Check for legacy single publisher format + if (publishersConfig?.getOptionalString('clientId')) { + logger.warn( + 'Legacy single config format detected at events.modules.kafka.kafkaConsumingEventPublisher.', + ); + return [ + processSinglePublisher( + 'default', // use `default` as instance name for legacy single config + publishersConfig, + ), + ]; + } + + return ( + publishersConfig + ?.keys() + ?.map(publisherKey => + processSinglePublisher( + publisherKey, + publishersConfig.getConfig(publisherKey), + ), + ) ?? [] + ); +}; diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/index.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/index.ts new file mode 100644 index 0000000000..94d36a81e0 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { eventsModuleKafkaConsumingEventPublisher } from './module'; diff --git a/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.test.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.test.ts new file mode 100644 index 0000000000..19ac969800 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsModuleKafkaConsumingEventPublisher } from './module'; +import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; + +jest.mock('./KafkaConsumingEventPublisher'); + +describe('eventsModuleKafkaConsumingEventPublisher', () => { + it('should be correctly wired and set up', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + const mockKafkaConsumingEventPublisher = { + start: jest.fn(), + shutdown: jest.fn(), + } as unknown as KafkaConsumingEventPublisher; + + jest + .mocked(KafkaConsumingEventPublisher.fromConfig) + .mockReturnValue([mockKafkaConsumingEventPublisher]); + + await startTestBackend({ + features: [ + eventsServiceFactory, + eventsModuleKafkaConsumingEventPublisher, + mockServices.rootConfig.factory({ + data: { + events: { + modules: { + kafka: { + kafkaConsumingEventPublisher: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: { + fake1: { + kafka: { + topics: ['topic-A'], + groupId: 'my-group', + }, + }, + fake2: { + kafka: { + topics: ['topic-B'], + groupId: 'my-group', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + ], + }); + + // Verify that the Kafka consumer client was started + expect(mockKafkaConsumingEventPublisher.start).toHaveBeenCalled(); + + // Verify that the shutdown hook was registered + expect(mockKafkaConsumingEventPublisher.shutdown).not.toHaveBeenCalled(); + }); + + it('should handle empty configuration gracefully', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + jest.mocked(KafkaConsumingEventPublisher.fromConfig).mockReturnValue([]); + + await startTestBackend({ + features: [ + eventsServiceFactory, + eventsModuleKafkaConsumingEventPublisher, + mockServices.rootConfig.factory({ + data: { + events: { + modules: { + kafka: { + // No kafkaConsumingEventPublisher config + }, + }, + }, + }, + }), + ], + }); + + // Verify that fromConfig was called but returned empty array + expect(KafkaConsumingEventPublisher.fromConfig).toHaveBeenCalled(); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.ts b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.ts similarity index 69% rename from plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.ts rename to plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.ts index b524454ca0..8ad3396c76 100644 --- a/plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.ts +++ b/plugins/events-backend-module-kafka/src/KafkaConsumingEventPublisher/module.ts @@ -17,11 +17,11 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { KafkaConsumerClient } from '../publisher/KafkaConsumerClient'; import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; /** - * Kafka module for the Events plugin. + * Reads messages off of Kafka topics and forwards them into the Backstage events system. * * @public */ @@ -34,22 +34,22 @@ export const eventsModuleKafkaConsumingEventPublisher = createBackendModule({ config: coreServices.rootConfig, events: eventsServiceRef, logger: coreServices.logger, - lifecycle: coreServices.lifecycle, + lifecycle: coreServices.rootLifecycle, }, async init({ config, logger, events, lifecycle }) { - const kafka = KafkaConsumerClient.fromConfig({ + const consumers = KafkaConsumingEventPublisher.fromConfig({ config, events, logger, }); - if (!kafka) { - return; - } + lifecycle.addStartupHook(async () => { + await Promise.all(consumers.map(consumer => consumer.start())); + }); - await kafka.start(); - - lifecycle.addShutdownHook(async () => await kafka.shutdown()); + lifecycle.addShutdownHook(async () => { + await Promise.all(consumers.map(consumer => consumer.shutdown())); + }); }, }); }, diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.test.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.test.ts new file mode 100644 index 0000000000..6379605901 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { KafkaPublishingEventConsumer } from './KafkaPublishingEventConsumer'; +import { Kafka } from 'kafkajs'; +import { mockServices } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; + +jest.mock('kafkajs'); + +describe('KafkaPublishingEventConsumer', () => { + const mockLogger = mockServices.logger.mock(); + const mockEvents = mockServices.events.mock(); + + const mockProducer = { + connect: jest.fn(), + disconnect: jest.fn(), + send: jest.fn(), + }; + + const mockKafkaClient = { + producer: jest.fn().mockReturnValue(mockProducer), + } as unknown as Kafka; + + jest.mocked(Kafka).mockImplementation(() => mockKafkaClient); + + const mockConfig = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092'], + topics: [ + { + topic: 'backstage-topic', + kafka: { + topic: 'kafka-topic', + allowAutoTopicCreation: true, + }, + }, + ], + }, + }, + }, + }, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create instances from config', () => { + const consumers = KafkaPublishingEventConsumer.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + expect(consumers).toHaveLength(1); + expect(consumers[0]).toBeInstanceOf(KafkaPublishingEventConsumer); + }); + + it('should start the consumer and subscribe to events', async () => { + const consumers = KafkaPublishingEventConsumer.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + await consumers[0].start(); + + expect(mockProducer.connect).toHaveBeenCalled(); + expect(mockEvents.subscribe).toHaveBeenCalledWith({ + id: 'kafka:publisher:backstage-topic', + topics: ['backstage-topic'], + onEvent: expect.any(Function), + }); + }); + + it('should shutdown the producer', async () => { + const consumers = KafkaPublishingEventConsumer.fromConfig({ + config: mockConfig, + events: mockEvents, + logger: mockLogger, + }); + + await consumers[0].shutdown(); + + expect(mockProducer.disconnect).toHaveBeenCalled(); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.ts new file mode 100644 index 0000000000..f9443f2d69 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/KafkaPublishingEventConsumer.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 } from '@backstage/backend-plugin-api'; +import { EventParams, EventsService } from '@backstage/plugin-events-node'; +import { Kafka, Producer } from 'kafkajs'; +import { + KafkaPublisherConfig, + KafkaPublishingEventConsumerConfig, + readPublisherConfig, +} from './config'; +import { Config } from '@backstage/config'; +import { loggerServiceAdapter } from '../utils/LoggerServiceAdapter'; +import { payloadToBuffer } from '../utils/kafkaTransformers'; + +type KafkaPublisher = { + producer: Producer; + config: KafkaPublisherConfig; +}; + +/** + * This class subscribes to Backstage internal events and publishes them to Kafka topics. + * The internal event payload will be serialized and sent to the configured Kafka topic. + */ +export class KafkaPublishingEventConsumer { + private readonly kafkaPublishers: KafkaPublisher[]; + private readonly logger: LoggerService; + + static fromConfig(env: { + config: Config; + events: EventsService; + logger: LoggerService; + }): KafkaPublishingEventConsumer[] { + const configs = readPublisherConfig(env.config); + + return configs.map( + kafkaConfig => + new KafkaPublishingEventConsumer(env.logger, env.events, kafkaConfig), + ); + } + + private constructor( + logger: LoggerService, + private readonly events: EventsService, + config: KafkaPublishingEventConsumerConfig, + ) { + this.logger = logger.child({ + class: KafkaPublishingEventConsumer.prototype.constructor.name, + instance: config.instance, + }); + + const kafka = new Kafka({ + ...config.kafkaConfig, + logCreator: loggerServiceAdapter(this.logger), + }); + + this.kafkaPublishers = config.kafkaPublisherConfigs.map( + publisherConfig => ({ + producer: kafka.producer(publisherConfig.producerConfig), + config: publisherConfig, + }), + ); + } + + async start(): Promise { + await Promise.all( + this.kafkaPublishers.map(async ({ producer, config }) => { + try { + await producer.connect(); + + this.events.subscribe({ + id: `kafka:publisher:${config.backstageTopic}`, + topics: [config.backstageTopic], + onEvent: async (params: EventParams) => { + await producer.send({ + topic: config.kafkaTopic, + messages: [ + { + value: payloadToBuffer(params.eventPayload), + }, + ], + }); + }, + }); + this.logger.info( + `Subscribed to EventService, publishing events to external topic: ${config.backstageTopic}`, + ); + } catch (error: any) { + this.logger.error( + `Kafka producer connection failed for topic ${config.backstageTopic}`, + error, + ); + } + }), + ); + } + + async shutdown(): Promise { + await Promise.all( + this.kafkaPublishers.map(({ producer }) => producer.disconnect()), + ); + } +} diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.test.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.test.ts new file mode 100644 index 0000000000..bd4d622238 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.test.ts @@ -0,0 +1,360 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/config'; +import { readPublisherConfig } from './config'; + +describe('readPublisherConfig', () => { + it('not configured', () => { + const publisherConfigs = readPublisherConfig(new ConfigReader({})); + + expect(publisherConfigs).toEqual([]); + }); + + it('only required fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: [ + { + topic: 'fake1', + kafka: { + topic: 'topic-A', + }, + }, + { + topic: 'fake2', + kafka: { + topic: 'topic-B', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readPublisherConfig(config); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + expect(devConfig.kafkaPublisherConfigs.length).toBe(2); + + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + + expect(devConfig.kafkaPublisherConfigs).toEqual([ + { + backstageTopic: 'fake1', + kafkaTopic: 'topic-A', + producerConfig: { + allowAutoTopicCreation: undefined, + metadataMaxAge: undefined, + transactionTimeout: undefined, + idempotent: undefined, + maxInFlightRequests: undefined, + retry: {}, + }, + }, + { + backstageTopic: 'fake2', + kafkaTopic: 'topic-B', + producerConfig: { + allowAutoTopicCreation: undefined, + metadataMaxAge: undefined, + transactionTimeout: undefined, + idempotent: undefined, + maxInFlightRequests: undefined, + retry: {}, + }, + }, + ]); + }); + + it('all fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + ssl: true, + sasl: { + mechanism: 'plain', + username: 'username', + password: 'password', + }, + retry: { + maxRetryTime: { milliseconds: 20000 }, + initialRetryTime: { milliseconds: 200 }, + factor: '0.4', + multiplier: '4', + retries: '10', + }, + authenticationTimeout: { milliseconds: 20000 }, + connectionTimeout: { milliseconds: 1500 }, + requestTimeout: { milliseconds: 20000 }, + enforceRequestTimeout: false, + topics: [ + { + topic: 'fake1', + kafka: { + topic: 'topic-A', + allowAutoTopicCreation: true, + metadataMaxAge: { milliseconds: 400000 }, + transactionTimeout: { milliseconds: 30000 }, + idempotent: true, + maxInFlightRequests: 5, + retry: { + maxRetryTime: { milliseconds: 15000 }, + initialRetryTime: { milliseconds: 100 }, + factor: '0.2', + multiplier: '2', + retries: '5', + }, + }, + }, + { + topic: 'fake2', + kafka: { + topic: 'topic-B', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readPublisherConfig(config); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + + // Client configuration + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + expect(devConfig.kafkaConfig.ssl).toBeTruthy(); + expect(devConfig.kafkaConfig.sasl).toStrictEqual({ + mechanism: 'plain', + username: 'username', + password: 'password', + }); + expect(devConfig.kafkaConfig.authenticationTimeout).toBe(20000); + expect(devConfig.kafkaConfig.connectionTimeout).toBe(1500); + expect(devConfig.kafkaConfig.requestTimeout).toBe(20000); + expect(devConfig.kafkaConfig.enforceRequestTimeout).toBeFalsy(); + expect(devConfig.kafkaConfig.retry).toStrictEqual({ + maxRetryTime: 20000, + initialRetryTime: 200, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + + // Publisher configuration + expect(devConfig.kafkaPublisherConfigs.length).toBe(2); + + expect(devConfig.kafkaPublisherConfigs).toEqual([ + { + backstageTopic: 'fake1', + kafkaTopic: 'topic-A', + producerConfig: { + allowAutoTopicCreation: true, + metadataMaxAge: 400000, + transactionTimeout: 30000, + idempotent: true, + maxInFlightRequests: 5, + retry: { + maxRetryTime: 15000, + initialRetryTime: 100, + factor: 0.2, + multiplier: 2, + retries: 5, + }, + }, + }, + { + backstageTopic: 'fake2', + kafkaTopic: 'topic-B', + producerConfig: { + allowAutoTopicCreation: undefined, + metadataMaxAge: undefined, + transactionTimeout: undefined, + idempotent: undefined, + maxInFlightRequests: undefined, + retry: {}, + }, + }, + ]); + }); + + it('should handle HumanDuration and string values for durations and timeouts', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + retry: { + maxRetryTime: { seconds: 1 }, + initialRetryTime: { minutes: 1 }, + factor: 0.4, + multiplier: 4, + retries: 10, + }, + authenticationTimeout: { hours: 1 }, + connectionTimeout: { days: 1 }, + topics: [ + { + topic: 'fake1', + kafka: { + topic: 'topic-A', + metadataMaxAge: { seconds: 300 }, + transactionTimeout: '30s', + }, + }, + ], + requestTimeout: '1m', + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readPublisherConfig(config); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(1); + + const devConfig = publisherConfigs[0]; + expect(devConfig.instance).toBe('dev'); + + // Client configuration + expect(devConfig.kafkaConfig.clientId).toEqual('backstage-events'); + expect(devConfig.kafkaConfig.brokers).toEqual([ + 'kafka1:9092', + 'kafka2:9092', + ]); + expect(devConfig.kafkaConfig.authenticationTimeout).toBe(3600000); + expect(devConfig.kafkaConfig.connectionTimeout).toBe(86400000); + expect(devConfig.kafkaConfig.requestTimeout).toBe(60000); + expect(devConfig.kafkaConfig.retry).toStrictEqual({ + maxRetryTime: 1000, + initialRetryTime: 60000, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + + // Publisher configuration + expect(devConfig.kafkaPublisherConfigs.length).toBe(1); + expect( + devConfig.kafkaPublisherConfigs[0].producerConfig.metadataMaxAge, + ).toBe(300000); + expect( + devConfig.kafkaPublisherConfigs[0].producerConfig.transactionTimeout, + ).toBe(30000); + }); + + it('should handle multiple instances', () => { + const config = new ConfigReader({ + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-dev', + brokers: ['kafka-dev:9092'], + topics: [ + { + topic: 'dev-topic', + kafka: { + topic: 'kafka-dev-topic', + }, + }, + ], + }, + prod: { + clientId: 'backstage-prod', + brokers: ['kafka-prod1:9092', 'kafka-prod2:9092'], + topics: [ + { + topic: 'prod-topic', + kafka: { + topic: 'kafka-prod-topic', + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readPublisherConfig(config); + + expect(publisherConfigs).toBeDefined(); + expect(Array.isArray(publisherConfigs)).toBe(true); + expect(publisherConfigs).toHaveLength(2); + + const devConfig = publisherConfigs.find(c => c.instance === 'dev')!; + expect(devConfig.kafkaConfig.clientId).toBe('backstage-dev'); + expect(devConfig.kafkaConfig.brokers).toEqual(['kafka-dev:9092']); + expect(devConfig.kafkaPublisherConfigs).toHaveLength(1); + expect(devConfig.kafkaPublisherConfigs[0].backstageTopic).toBe('dev-topic'); + + const prodConfig = publisherConfigs.find(c => c.instance === 'prod')!; + expect(prodConfig.kafkaConfig.clientId).toBe('backstage-prod'); + expect(prodConfig.kafkaConfig.brokers).toEqual([ + 'kafka-prod1:9092', + 'kafka-prod2:9092', + ]); + expect(prodConfig.kafkaPublisherConfigs).toHaveLength(1); + expect(prodConfig.kafkaPublisherConfigs[0].backstageTopic).toBe( + 'prod-topic', + ); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.ts new file mode 100644 index 0000000000..8439e24405 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/config.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + readKafkaConfig, + readOptionalHumanDurationInMs, + readRetryConfig, +} from '../utils/config'; +import { KafkaConfig, ProducerConfig } from 'kafkajs'; + +export interface KafkaPublisherConfig { + backstageTopic: string; + kafkaTopic: string; + producerConfig: ProducerConfig; +} + +export interface KafkaPublishingEventConsumerConfig { + instance: string; + kafkaConfig: KafkaConfig; + kafkaPublisherConfigs: KafkaPublisherConfig[]; +} + +const CONFIG_PREFIX_PUBLISHER = + 'events.modules.kafka.kafkaPublishingEventConsumer'; + +export const readPublisherConfig = ( + config: Config, +): KafkaPublishingEventConsumerConfig[] => { + const publishers = config.getOptionalConfig(CONFIG_PREFIX_PUBLISHER); + + return ( + publishers?.keys()?.map(publisherKey => { + const publisherConfig = publishers.getConfig(publisherKey); + + return { + instance: publisherKey, + kafkaConfig: readKafkaConfig(publisherConfig), + kafkaPublisherConfigs: publisherConfig + .getConfigArray('topics') + .map(topicConfig => { + return { + backstageTopic: topicConfig.getString('topic'), + kafkaTopic: topicConfig.getString('kafka.topic'), + producerConfig: { + allowAutoTopicCreation: topicConfig.getOptionalBoolean( + 'kafka.allowAutoTopicCreation', + ), + metadataMaxAge: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.metadataMaxAge', + ), + transactionTimeout: readOptionalHumanDurationInMs( + topicConfig, + 'kafka.transactionTimeout', + ), + idempotent: topicConfig.getOptionalBoolean('kafka.idempotent'), + maxInFlightRequests: topicConfig.getOptionalNumber( + 'kafka.maxInFlightRequests', + ), + retry: readRetryConfig( + topicConfig.getOptionalConfig('kafka.retry'), + ), + }, + }; + }), + }; + }) ?? [] + ); +}; diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/index.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/index.ts new file mode 100644 index 0000000000..42fadd2a5a --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { eventsModuleKafkaPublishingEventConsumer } from './module'; diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.test.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.test.ts new file mode 100644 index 0000000000..39c8268300 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsModuleKafkaPublishingEventConsumer } from './module'; +import { KafkaPublishingEventConsumer } from './KafkaPublishingEventConsumer'; + +jest.mock('./KafkaPublishingEventConsumer'); + +describe('eventsModuleKafkaPublishingEventConsumer', () => { + it('should be correctly wired and set up', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + const mockKafkaPublishingEventConsumer = { + start: jest.fn(), + shutdown: jest.fn(), + } as unknown as KafkaPublishingEventConsumer; + + jest + .mocked(KafkaPublishingEventConsumer.fromConfig) + .mockReturnValue([mockKafkaPublishingEventConsumer]); + + await startTestBackend({ + features: [ + eventsServiceFactory, + eventsModuleKafkaPublishingEventConsumer, + mockServices.rootConfig.factory({ + data: { + events: { + modules: { + kafka: { + kafkaPublishingEventConsumer: { + dev: { + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + topics: [ + { + topic: 'fake1', + kafka: { + topic: 'topic-A', + }, + }, + { + topic: 'fake2', + kafka: { + topic: 'topic-B', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }), + ], + }); + + // Verify that the Kafka publishing event consumer was started + expect(mockKafkaPublishingEventConsumer.start).toHaveBeenCalled(); + + // Verify that the shutdown hook was registered (but not called yet) + expect(mockKafkaPublishingEventConsumer.shutdown).not.toHaveBeenCalled(); + }); + + it('should handle empty configuration gracefully', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + jest.mocked(KafkaPublishingEventConsumer.fromConfig).mockReturnValue([]); + + await startTestBackend({ + features: [ + eventsServiceFactory, + eventsModuleKafkaPublishingEventConsumer, + mockServices.rootConfig.factory({ + data: { + events: { + modules: { + kafka: { + // No kafkaPublishingEventConsumer config + }, + }, + }, + }, + }), + ], + }); + + // Verify that fromConfig was called but returned empty array + expect(KafkaPublishingEventConsumer.fromConfig).toHaveBeenCalled(); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.ts b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.ts new file mode 100644 index 0000000000..dd5f0c50ab --- /dev/null +++ b/plugins/events-backend-module-kafka/src/KafkaPublishingEventConsumer/module.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { KafkaPublishingEventConsumer } from './KafkaPublishingEventConsumer'; + +/** + * Reads internal Backstage events and forwards them to Kafka topics. + * + * @public + */ +export const eventsModuleKafkaPublishingEventConsumer = createBackendModule({ + pluginId: 'events', + moduleId: 'kafka-publishing-event-consumer', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + events: eventsServiceRef, + logger: coreServices.logger, + lifecycle: coreServices.rootLifecycle, + }, + async init({ config, logger, events, lifecycle }) { + const consumers = KafkaPublishingEventConsumer.fromConfig({ + config, + events, + logger, + }); + + lifecycle.addStartupHook(async () => { + await Promise.all(consumers.map(consumer => consumer.start())); + }); + + lifecycle.addShutdownHook(async () => { + await Promise.all(consumers.map(consumer => consumer.shutdown())); + }); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-kafka/src/index.ts b/plugins/events-backend-module-kafka/src/index.ts index 3ba6207d7d..af4fb02bf1 100644 --- a/plugins/events-backend-module-kafka/src/index.ts +++ b/plugins/events-backend-module-kafka/src/index.ts @@ -13,14 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createBackendFeatureLoader } from '@backstage/backend-plugin-api'; +import { eventsModuleKafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; +import { eventsModuleKafkaPublishingEventConsumer } from './KafkaPublishingEventConsumer'; /** * The module "kafka" for the Backstage backend plugin "events" - * adding an Kafka-based publisher, - * receiving events from an Kafka topic and passing it to the - * internal event broker. + * adding Kafka-based event handling: + * - Consumer: receives events from Kafka topics and passes them to the internal event broker + * - Publisher: receives internal events and publishes them to Kafka topics * * @packageDocumentation */ -export { eventsModuleKafkaConsumingEventPublisher as default } from './service/eventsModuleKafkaConsumingEventPublisher'; +export default createBackendFeatureLoader({ + *loader() { + yield eventsModuleKafkaConsumingEventPublisher; + yield eventsModuleKafkaPublishingEventConsumer; + }, +}); diff --git a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.test.ts b/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.test.ts deleted file mode 100644 index 5824bf4d41..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { KafkaConsumerClient } from './KafkaConsumerClient'; -import { ConfigReader } from '@backstage/config'; -import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; -import { mockServices } from '@backstage/backend-test-utils'; - -jest.mock('kafkajs'); -jest.mock('./KafkaConsumingEventPublisher'); - -describe('KafkaConsumerClient', () => { - const mockLogger = mockServices.logger.mock(); - const mockEvents = mockServices.events.mock(); - - const mockConfig = new ConfigReader({ - events: { - modules: { - kafka: { - kafkaConsumingEventPublisher: { - clientId: 'backstage-events', - brokers: ['kafka1:9092', 'kafka2:9092'], - topics: [ - { - topic: 'fake1', - kafka: { - topics: ['topic-A'], - groupId: 'my-group', - }, - }, - { - topic: 'fake2', - kafka: { - topics: ['topic-B'], - groupId: 'my-group', - }, - }, - ], - }, - }, - }, - }, - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should create an instance from config', () => { - const client = KafkaConsumerClient.fromConfig({ - config: mockConfig, - events: mockEvents, - logger: mockLogger, - }); - - expect(client).toBeInstanceOf(KafkaConsumerClient); - }); - - it('should not create an instance from config', () => { - const client = KafkaConsumerClient.fromConfig({ - config: new ConfigReader({}), - events: mockEvents, - logger: mockLogger, - }); - - expect(client).toBeUndefined(); - }); - - it('should create a consumer for each topic from config', () => { - KafkaConsumerClient.fromConfig({ - config: mockConfig, - events: mockEvents, - logger: mockLogger, - }); - - expect(KafkaConsumingEventPublisher.fromConfig).toHaveBeenCalledTimes(2); - }); - - it('should start all consumers', async () => { - const mockConsumer = { - start: jest.fn().mockResolvedValue(undefined), - }; - (KafkaConsumingEventPublisher.fromConfig as jest.Mock).mockReturnValue( - mockConsumer, - ); - - const client = KafkaConsumerClient.fromConfig({ - config: mockConfig, - events: mockEvents, - logger: mockLogger, - }); - - expect(client).toBeDefined(); - - await client?.start(); - - expect(mockConsumer.start).toHaveBeenCalled(); - }); - - it('should shutdown all consumers', async () => { - const mockConsumer = { - shutdown: jest.fn().mockResolvedValue(undefined), - }; - (KafkaConsumingEventPublisher.fromConfig as jest.Mock).mockReturnValue( - mockConsumer, - ); - - const client = KafkaConsumerClient.fromConfig({ - config: mockConfig, - events: mockEvents, - logger: mockLogger, - }); - - expect(client).toBeDefined(); - - await client?.shutdown(); - - expect(mockConsumer.shutdown).toHaveBeenCalled(); - }); -}); diff --git a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.ts b/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.ts deleted file mode 100644 index d506e4a6af..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumerClient.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; -import { EventsService } from '@backstage/plugin-events-node'; -import { Kafka } from 'kafkajs'; -import { KafkaEventSourceConfig, readConfig } from './config'; -import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; -import { loggerServiceAdapter } from './LoggerServiceAdapter'; - -/** - * KafkaConsumerClient - * - * This class creates the Kafka client that will be used to create the KafkaConsumingEventPublisher - */ -export class KafkaConsumerClient { - private readonly kafka: Kafka; - private readonly consumers: KafkaConsumingEventPublisher[]; - - static fromConfig(options: { - config: Config; - events: EventsService; - logger: LoggerService; - }): KafkaConsumerClient | undefined { - const kafkaConfig = readConfig(options.config); - - if (!kafkaConfig) { - options.logger.info( - 'Kafka consumer not configured, skipping initialization', - ); - return undefined; - } - - return new KafkaConsumerClient(options.logger, options.events, kafkaConfig); - } - - private constructor( - logger: LoggerService, - events: EventsService, - config: KafkaEventSourceConfig, - ) { - this.kafka = new Kafka({ - ...config.kafkaConfig, - logCreator: loggerServiceAdapter(logger), - }); - - this.consumers = config.kafkaConsumerConfigs.map(consumerConfig => - KafkaConsumingEventPublisher.fromConfig({ - kafkaClient: this.kafka, - config: consumerConfig, - logger, - events, - }), - ); - } - - async start(): Promise { - this.consumers.map(async consumer => await consumer.start()); - } - - async shutdown(): Promise { - this.consumers.map(async consumer => await consumer.shutdown()); - } -} diff --git a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.test.ts b/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.test.ts deleted file mode 100644 index 64c53595ad..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { KafkaConsumingEventPublisher } from './KafkaConsumingEventPublisher'; -import { Kafka } from 'kafkajs'; -import { KafkaConsumerConfig } from './config'; -import { mockServices } from '@backstage/backend-test-utils'; - -jest.mock('kafkajs'); - -describe('KafkaConsumingEventPublisher', () => { - const mockLogger = mockServices.logger.mock(); - const mockEvents = mockServices.events.mock(); - - const mockConsumer = { - connect: jest.fn(), - disconnect: jest.fn(), - subscribe: jest.fn(), - run: jest.fn(), - }; - - const mockKafkaClient = { - consumer: jest.fn().mockReturnValue(mockConsumer), - } as unknown as Kafka; - - const kafkaConsumerConfig: KafkaConsumerConfig = { - consumerConfig: { - groupId: 'test-group', - }, - consumerSubscribeTopics: { - topics: ['test-topic'], - }, - backstageTopic: 'backstage-topic', - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should create an instance from config', () => { - const consumer = KafkaConsumingEventPublisher.fromConfig({ - kafkaClient: mockKafkaClient, - config: kafkaConsumerConfig, - events: mockEvents, - logger: mockLogger, - }); - - expect(consumer).toBeInstanceOf(KafkaConsumingEventPublisher); - }); - - it('should start the consumer', async () => { - const consumer = KafkaConsumingEventPublisher.fromConfig({ - kafkaClient: mockKafkaClient, - config: kafkaConsumerConfig, - events: mockEvents, - logger: mockLogger, - }); - - await consumer.start(); - - expect(mockConsumer.connect).toHaveBeenCalled(); - expect(mockConsumer.subscribe).toHaveBeenCalledWith( - kafkaConsumerConfig.consumerSubscribeTopics, - ); - expect(mockConsumer.run).toHaveBeenCalled(); - }); - - it('should shutdown the consumer', async () => { - const consumer = KafkaConsumingEventPublisher.fromConfig({ - kafkaClient: mockKafkaClient, - config: kafkaConsumerConfig, - events: mockEvents, - logger: mockLogger, - }); - - await consumer.shutdown(); - - expect(mockConsumer.disconnect).toHaveBeenCalled(); - }); -}); diff --git a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.ts b/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.ts deleted file mode 100644 index dfc6186fb5..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/KafkaConsumingEventPublisher.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 } from '@backstage/backend-plugin-api'; -import { EventParams, EventsService } from '@backstage/plugin-events-node'; -import { Consumer, ConsumerSubscribeTopics, IHeaders, Kafka } from 'kafkajs'; -import { KafkaConsumerConfig } from './config'; - -type EventMetadata = EventParams['metadata']; - -/** - * This class subscribes to Kafka topics and publishes events received to the registered subscriber. - * The message payload will be used as the event payload and passed to the subscribers. - */ -export class KafkaConsumingEventPublisher { - private readonly kafkaConsumer: Consumer; - private readonly consumerSubscribeTopics: ConsumerSubscribeTopics; - private readonly backstageTopic: string; - private readonly logger: LoggerService; - - static fromConfig(env: { - kafkaClient: Kafka; - config: KafkaConsumerConfig; - events: EventsService; - logger: LoggerService; - }): KafkaConsumingEventPublisher { - return new KafkaConsumingEventPublisher( - env.kafkaClient, - env.logger, - env.events, - env.config, - ); - } - - private readonly events: EventsService; - - private constructor( - kafkaClient: Kafka, - logger: LoggerService, - events: EventsService, - config: KafkaConsumerConfig, - ) { - this.events = events; - this.kafkaConsumer = kafkaClient.consumer(config.consumerConfig); - this.consumerSubscribeTopics = config.consumerSubscribeTopics; - this.backstageTopic = config.backstageTopic; - const id = `events.kafka.publisher:${this.backstageTopic}`; - this.logger = logger.child({ - class: KafkaConsumingEventPublisher.prototype.constructor.name, - groupId: config.consumerConfig.groupId, - kafkaTopics: config.consumerSubscribeTopics.topics.toString(), - backstageTopic: config.backstageTopic, - taskId: id, - }); - } - - async start(): Promise { - try { - await this.kafkaConsumer.connect(); - - await this.kafkaConsumer.subscribe(this.consumerSubscribeTopics); - - await this.kafkaConsumer.run({ - eachMessage: async ({ message }) => { - this.events.publish({ - topic: this.backstageTopic, - eventPayload: JSON.parse(message.value?.toString()!), - metadata: this.convertHeadersToMetadata(message.headers), - }); - }, - }); - } catch (error: any) { - this.logger.error('Kafka consumer connection failed ', error); - } - } - - async shutdown(): Promise { - await this.kafkaConsumer.disconnect(); - } - - private convertHeadersToMetadata = ( - headers: IHeaders | undefined, - ): EventParams['metadata'] => { - if (!headers) return undefined; - - const metadata: EventMetadata = {}; - - Object.entries(headers).forEach(([key, value]) => { - // If value is an array use toString() on all values converting any Buffer types to valid strings - if (Array.isArray(value)) metadata[key] = value.map(v => v.toString()); - // Always return the values using toString() to catch all Buffer types that should be converted to strings - else metadata[key] = value?.toString(); - }); - - return metadata; - }; -} diff --git a/plugins/events-backend-module-kafka/src/publisher/config.test.ts b/plugins/events-backend-module-kafka/src/publisher/config.test.ts deleted file mode 100644 index fc7e3049b0..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/config.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ConfigReader } from '@backstage/config'; -import { readConfig } from './config'; - -describe('readConfig', () => { - it('not configured', () => { - const publisherConfigs = readConfig(new ConfigReader({})); - - expect(publisherConfigs).toBeUndefined(); - }); - - it('only required fields configured', () => { - const config = new ConfigReader({ - events: { - modules: { - kafka: { - kafkaConsumingEventPublisher: { - clientId: 'backstage-events', - brokers: ['kafka1:9092', 'kafka2:9092'], - topics: [ - { - topic: 'fake1', - kafka: { - topics: ['topic-A'], - groupId: 'my-group', - }, - }, - { - topic: 'fake2', - kafka: { - topics: ['topic-B'], - groupId: 'my-group', - }, - }, - ], - }, - }, - }, - }, - }); - - const publisherConfigs = readConfig(config); - - expect(publisherConfigs).toBeDefined(); - - expect(publisherConfigs?.kafkaConsumerConfigs.length).toBe(2); - - expect(publisherConfigs?.kafkaConfig.clientId).toEqual('backstage-events'); - expect(publisherConfigs?.kafkaConfig.brokers).toEqual([ - 'kafka1:9092', - 'kafka2:9092', - ]); - expect(publisherConfigs?.kafkaConsumerConfigs[0].backstageTopic).toEqual( - 'fake1', - ); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.groupId, - ).toEqual('my-group'); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerSubscribeTopics.topics, - ).toEqual(['topic-A']); - }); - - it('all fields configured', () => { - const config = new ConfigReader({ - events: { - modules: { - kafka: { - kafkaConsumingEventPublisher: { - clientId: 'backstage-events', - brokers: ['kafka1:9092', 'kafka2:9092'], - ssl: true, - sasl: { - mechanism: 'plain', - username: 'username', - password: 'password', - }, - retry: { - maxRetryTime: { milliseconds: 20000 }, - initialRetryTime: { milliseconds: 200 }, - factor: '0.4', - multiplier: '4', - retries: '10', - }, - authenticationTimeout: { milliseconds: 20000 }, - connectionTimeout: { milliseconds: 1500 }, - requestTimeout: { milliseconds: 20000 }, - enforceRequestTimeout: false, - topics: [ - { - topic: 'fake1', - kafka: { - topics: ['topic-A'], - groupId: 'my-group', - sessionTimeout: { milliseconds: 20000 }, - rebalanceTimeout: { milliseconds: 50000 }, - heartbeatInterval: { milliseconds: 2000 }, - metadataMaxAge: { milliseconds: 400000 }, - maxBytesPerPartition: 50000, - minBytes: 2, - maxBytes: 500000, - maxWaitTime: { milliseconds: 4000 }, - }, - }, - { - topic: 'fake2', - kafka: { - topics: ['topic-B'], - groupId: 'my-group', - }, - }, - ], - }, - }, - }, - }, - }); - - const publisherConfigs = readConfig(config); - - expect(publisherConfigs).toBeDefined(); - - // Client configuration - expect(publisherConfigs?.kafkaConfig.clientId).toEqual('backstage-events'); - expect(publisherConfigs?.kafkaConfig.brokers).toEqual([ - 'kafka1:9092', - 'kafka2:9092', - ]); - expect(publisherConfigs?.kafkaConfig.ssl).toBeTruthy(); - expect(publisherConfigs?.kafkaConfig.sasl).toStrictEqual({ - mechanism: 'plain', - username: 'username', - password: 'password', - }); - expect(publisherConfigs?.kafkaConfig.authenticationTimeout).toBe(20000); - expect(publisherConfigs?.kafkaConfig.connectionTimeout).toBe(1500); - expect(publisherConfigs?.kafkaConfig.requestTimeout).toBe(20000); - expect(publisherConfigs?.kafkaConfig.enforceRequestTimeout).toBeFalsy(); - expect(publisherConfigs?.kafkaConfig.retry).toStrictEqual({ - maxRetryTime: 20000, - initialRetryTime: 200, - factor: 0.4, - multiplier: 4, - retries: 10, - }); - - // Consumer configuration - expect(publisherConfigs?.kafkaConsumerConfigs.length).toBe(2); - expect(publisherConfigs?.kafkaConsumerConfigs[0].backstageTopic).toEqual( - 'fake1', - ); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.groupId, - ).toEqual('my-group'); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerSubscribeTopics.topics, - ).toEqual(['topic-A']); - - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.sessionTimeout, - ).toBe(20000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.rebalanceTimeout, - ).toBe(50000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig - .heartbeatInterval, - ).toBe(2000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.metadataMaxAge, - ).toBe(400000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig - .maxBytesPerPartition, - ).toBe(50000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.minBytes, - ).toBe(2); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.maxBytes, - ).toBe(500000); - expect( - publisherConfigs?.kafkaConsumerConfigs[0].consumerConfig.maxWaitTimeInMs, - ).toBe(4000); - }); - - it('should handle HumanDuration and string values for durations and timeouts', () => { - const config = new ConfigReader({ - events: { - modules: { - kafka: { - kafkaConsumingEventPublisher: { - clientId: 'backstage-events', - brokers: ['kafka1:9092', 'kafka2:9092'], - retry: { - maxRetryTime: { seconds: 1 }, - initialRetryTime: { minutes: 1 }, - factor: 0.4, - multiplier: 4, - retries: 10, - }, - authenticationTimeout: { hours: 1 }, - connectionTimeout: { days: 1 }, - topics: [], - requestTimeout: '1m', - }, - }, - }, - }, - }); - - const publisherConfigs = readConfig(config); - - expect(publisherConfigs).toBeDefined(); - - // Client configuration - expect(publisherConfigs?.kafkaConfig.clientId).toEqual('backstage-events'); - expect(publisherConfigs?.kafkaConfig.brokers).toEqual([ - 'kafka1:9092', - 'kafka2:9092', - ]); - expect(publisherConfigs?.kafkaConfig.authenticationTimeout).toBe(3600000); - expect(publisherConfigs?.kafkaConfig.connectionTimeout).toBe(86400000); - expect(publisherConfigs?.kafkaConfig.requestTimeout).toBe(60000); - expect(publisherConfigs?.kafkaConfig.retry).toStrictEqual({ - maxRetryTime: 1000, - initialRetryTime: 60000, - factor: 0.4, - multiplier: 4, - retries: 10, - }); - - // Consumer configuration - expect(publisherConfigs?.kafkaConsumerConfigs.length).toBe(0); - }); -}); diff --git a/plugins/events-backend-module-kafka/src/publisher/config.ts b/plugins/events-backend-module-kafka/src/publisher/config.ts deleted file mode 100644 index 7534ce4d38..0000000000 --- a/plugins/events-backend-module-kafka/src/publisher/config.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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, readDurationFromConfig } from '@backstage/config'; -import { durationToMilliseconds } from '@backstage/types'; -import { ConsumerConfig, ConsumerSubscribeTopics, KafkaConfig } from 'kafkajs'; - -export interface KafkaConsumerConfig { - backstageTopic: string; - consumerConfig: ConsumerConfig; - consumerSubscribeTopics: ConsumerSubscribeTopics; -} - -export interface KafkaEventSourceConfig { - kafkaConfig: KafkaConfig; - kafkaConsumerConfigs: KafkaConsumerConfig[]; -} - -const CONFIG_PREFIX_PUBLISHER = - 'events.modules.kafka.kafkaConsumingEventPublisher'; - -/** - * Reads an optional HumanDuration from the config and returns the value in milliseconds if the key is defined. - * - * @param config - The configuration object to read from. - * @param key - The key to look up in the configuration. - * @returns The duration in milliseconds, or undefined if the key is not defined. - */ -const readOptionalHumanDurationInMs = ( - config: Config, - key: string, -): number | undefined => { - const humanDuration = config.has(key) - ? readDurationFromConfig(config, { key }) - : undefined; - - if (!humanDuration) return undefined; - - return durationToMilliseconds(humanDuration); -}; - -export const readConfig = ( - config: Config, -): KafkaEventSourceConfig | undefined => { - const kafkaConfig = config.getOptionalConfig(CONFIG_PREFIX_PUBLISHER); - - if (!kafkaConfig) { - return undefined; - } - - const clientId = kafkaConfig.getString('clientId'); - const brokers = kafkaConfig.getStringArray('brokers'); - - const authenticationTimeout = readOptionalHumanDurationInMs( - kafkaConfig, - 'authenticationTimeout', - ); - - const connectionTimeout = readOptionalHumanDurationInMs( - kafkaConfig, - 'connectionTimeout', - ); - const requestTimeout = readOptionalHumanDurationInMs( - kafkaConfig, - 'requestTimeout', - ); - const enforceRequestTimeout = kafkaConfig.getOptionalBoolean( - 'enforceRequestTimeout', - ); - - const ssl = kafkaConfig.getOptional('ssl') as KafkaConfig['ssl']; - const sasl = kafkaConfig.getOptional('sasl') as KafkaConfig['sasl']; - - const retry: KafkaConfig['retry'] = { - maxRetryTime: readOptionalHumanDurationInMs( - kafkaConfig, - 'retry.maxRetryTime', - ), - initialRetryTime: readOptionalHumanDurationInMs( - kafkaConfig, - 'retry.initialRetryTime', - ), - factor: kafkaConfig.getOptionalNumber('retry.factor'), - multiplier: kafkaConfig.getOptionalNumber('retry.multiplier'), - retries: kafkaConfig.getOptionalNumber('retry.retries'), - }; - - const kafkaConsumerConfigs: KafkaConsumerConfig[] = kafkaConfig - .getConfigArray('topics') - .map(topic => { - return { - backstageTopic: topic.getString('topic'), - consumerConfig: { - groupId: topic.getString('kafka.groupId'), - sessionTimeout: readOptionalHumanDurationInMs( - topic, - 'kafka.sessionTimeout', - ), - rebalanceTimeout: readOptionalHumanDurationInMs( - topic, - 'kafka.rebalanceTimeout', - ), - heartbeatInterval: readOptionalHumanDurationInMs( - topic, - 'kafka.heartbeatInterval', - ), - metadataMaxAge: readOptionalHumanDurationInMs( - topic, - 'kafka.metadataMaxAge', - ), - maxBytesPerPartition: topic.getOptionalNumber( - 'kafka.maxBytesPerPartition', - ), - minBytes: topic.getOptionalNumber('kafka.minBytes'), - maxBytes: topic.getOptionalNumber('kafka.maxBytes'), - maxWaitTimeInMs: readOptionalHumanDurationInMs( - topic, - 'kafka.maxWaitTime', - ), - }, - consumerSubscribeTopics: { - topics: topic.getStringArray('kafka.topics'), - }, - }; - }); - - return { - kafkaConfig: { - clientId, - brokers, - ssl, - sasl, - authenticationTimeout, - connectionTimeout, - requestTimeout, - enforceRequestTimeout, - retry, - }, - kafkaConsumerConfigs, - }; -}; diff --git a/plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.test.ts b/plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.test.ts deleted file mode 100644 index 816793f635..0000000000 --- a/plugins/events-backend-module-kafka/src/service/eventsModuleKafkaConsumingEventPublisher.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import { eventsServiceRef } from '@backstage/plugin-events-node'; -import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; -import { eventsModuleKafkaConsumingEventPublisher } from './eventsModuleKafkaConsumingEventPublisher'; -import { KafkaConsumerClient } from '../publisher/KafkaConsumerClient'; - -jest.mock('../publisher/KafkaConsumerClient'); - -describe('eventsModuleKafkaConsumingEventPublisher', () => { - it('should be correctly wired and set up', async () => { - const events = new TestEventsService(); - const eventsServiceFactory = createServiceFactory({ - service: eventsServiceRef, - deps: {}, - async factory({}) { - return events; - }, - }); - - const mockKafkaConsumerClient = { - start: jest.fn(), - shutdown: jest.fn(), - }; - (KafkaConsumerClient.fromConfig as jest.Mock).mockReturnValue( - mockKafkaConsumerClient, - ); - - await startTestBackend({ - features: [ - eventsServiceFactory, - eventsModuleKafkaConsumingEventPublisher, - mockServices.rootConfig.factory({ - data: { - events: { - modules: { - kafka: { - kafkaConsumingEventPublisher: { - clientId: 'backstage-events', - brokers: ['kafka1:9092', 'kafka2:9092'], - topics: { - fake1: { - kafka: { - topics: ['topic-A'], - groupId: 'my-group', - }, - }, - fake2: { - kafka: { - topics: ['topic-B'], - groupId: 'my-group', - }, - }, - }, - }, - }, - }, - }, - }, - }), - ], - }); - - // Verify that the Kafka consumer client was started - expect(mockKafkaConsumerClient.start).toHaveBeenCalled(); - - // Verify that the shutdown hook was registered - expect(mockKafkaConsumerClient.shutdown).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/events-backend-module-kafka/src/publisher/LoggerServiceAdapter.ts b/plugins/events-backend-module-kafka/src/utils/LoggerServiceAdapter.ts similarity index 100% rename from plugins/events-backend-module-kafka/src/publisher/LoggerServiceAdapter.ts rename to plugins/events-backend-module-kafka/src/utils/LoggerServiceAdapter.ts diff --git a/plugins/events-backend-module-kafka/src/utils/config.test.ts b/plugins/events-backend-module-kafka/src/utils/config.test.ts new file mode 100644 index 0000000000..2bd6121ba2 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/utils/config.test.ts @@ -0,0 +1,235 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/config'; +import { readRetryConfig, readKafkaConfig } from './config'; + +describe('readRetryConfig', () => { + it('should return empty object when config is undefined', () => { + const result = readRetryConfig(undefined); + expect(result).toEqual({}); + }); + + it('should return empty object when config is empty', () => { + const config = new ConfigReader({}); + const result = readRetryConfig(config); + expect(result).toEqual({}); + }); + + it('should read retry configuration with milliseconds', () => { + const config = new ConfigReader({ + maxRetryTime: { milliseconds: 20000 }, + initialRetryTime: { milliseconds: 200 }, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + + const result = readRetryConfig(config); + + expect(result).toEqual({ + maxRetryTime: 20000, + initialRetryTime: 200, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + }); + + it('should read retry configuration with string values', () => { + const config = new ConfigReader({ + maxRetryTime: { seconds: 20 }, + initialRetryTime: { minutes: 1 }, + factor: '0.4', + multiplier: '4', + retries: '10', + }); + + const result = readRetryConfig(config); + + expect(result).toEqual({ + maxRetryTime: 20000, + initialRetryTime: 60000, + factor: 0.4, + multiplier: 4, + retries: 10, + }); + }); + + it('should handle HumanDuration values', () => { + const config = new ConfigReader({ + maxRetryTime: { hours: 1 }, + initialRetryTime: { days: 1 }, + }); + + const result = readRetryConfig(config); + + expect(result).toEqual({ + maxRetryTime: 3600000, + initialRetryTime: 86400000, + factor: undefined, + multiplier: undefined, + retries: undefined, + }); + }); + + it('should handle partial configuration', () => { + const config = new ConfigReader({ + maxRetryTime: { milliseconds: 15000 }, + retries: 5, + }); + + const result = readRetryConfig(config); + + expect(result).toEqual({ + maxRetryTime: 15000, + initialRetryTime: undefined, + factor: undefined, + multiplier: undefined, + retries: 5, + }); + }); +}); + +describe('readKafkaConfig', () => { + it('should read minimal kafka configuration', () => { + const config = new ConfigReader({ + clientId: 'test-client', + brokers: ['kafka1:9092', 'kafka2:9092'], + }); + + const result = readKafkaConfig(config); + + expect(result).toEqual({ + clientId: 'test-client', + brokers: ['kafka1:9092', 'kafka2:9092'], + authenticationTimeout: undefined, + connectionTimeout: undefined, + requestTimeout: undefined, + enforceRequestTimeout: undefined, + ssl: undefined, + sasl: undefined, + retry: {}, + }); + }); + + it('should read full kafka configuration with all optional fields', () => { + const config = new ConfigReader({ + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + authenticationTimeout: { milliseconds: 20000 }, + connectionTimeout: { milliseconds: 1500 }, + requestTimeout: { milliseconds: 20000 }, + enforceRequestTimeout: false, + ssl: true, + sasl: { + mechanism: 'plain', + username: 'username', + password: 'password', + }, + retry: { + maxRetryTime: { milliseconds: 20000 }, + initialRetryTime: { milliseconds: 200 }, + factor: 0.4, + multiplier: 4, + retries: 10, + }, + }); + + const result = readKafkaConfig(config); + + expect(result).toEqual({ + clientId: 'backstage-events', + brokers: ['kafka1:9092', 'kafka2:9092'], + authenticationTimeout: 20000, + connectionTimeout: 1500, + requestTimeout: 20000, + enforceRequestTimeout: false, + ssl: true, + sasl: { + mechanism: 'plain', + username: 'username', + password: 'password', + }, + retry: { + maxRetryTime: 20000, + initialRetryTime: 200, + factor: 0.4, + multiplier: 4, + retries: 10, + }, + }); + }); + + it('should handle HumanDuration values for timeouts', () => { + const config = new ConfigReader({ + clientId: 'test-client', + brokers: ['kafka:9092'], + authenticationTimeout: { hours: 1 }, + connectionTimeout: { days: 1 }, + requestTimeout: { minutes: 5 }, + }); + + const result = readKafkaConfig(config); + + expect(result.authenticationTimeout).toBe(3600000); + expect(result.connectionTimeout).toBe(86400000); + expect(result.requestTimeout).toBe(300000); + }); + + it('should handle complex SSL configuration', () => { + const config = new ConfigReader({ + clientId: 'secure-client', + brokers: ['secure-kafka:9093'], + ssl: { + rejectUnauthorized: false, + ca: 'ca-certificate', + key: 'client-key', + cert: 'client-cert', + }, + }); + + const result = readKafkaConfig(config); + + expect(result.ssl).toEqual({ + rejectUnauthorized: false, + ca: 'ca-certificate', + key: 'client-key', + cert: 'client-cert', + }); + }); + + it('should handle complex SASL configuration', () => { + const config = new ConfigReader({ + clientId: 'sasl-client', + brokers: ['sasl-kafka:9094'], + sasl: { + mechanism: 'scram-sha-256', + username: 'kafka-user', + password: 'kafka-password', + authorizationIdentity: 'authz-user', + }, + }); + + const result = readKafkaConfig(config); + + expect(result.sasl).toEqual({ + mechanism: 'scram-sha-256', + username: 'kafka-user', + password: 'kafka-password', + authorizationIdentity: 'authz-user', + }); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/utils/config.ts b/plugins/events-backend-module-kafka/src/utils/config.ts new file mode 100644 index 0000000000..4e140d26a3 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/utils/config.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, readDurationFromConfig } from '@backstage/config'; +import { durationToMilliseconds } from '@backstage/types'; +import { KafkaConfig, RetryOptions } from 'kafkajs'; + +/** + * Reads an optional HumanDuration from the config and returns the value in milliseconds if the key is defined. + * + * @param config - The configuration object to read from. + * @param key - The key to look up in the configuration. + * @returns The duration in milliseconds, or undefined if the key is not defined. + */ +export const readOptionalHumanDurationInMs = ( + config: Config, + key: string, +): number | undefined => { + const humanDuration = config.has(key) + ? readDurationFromConfig(config, { key }) + : undefined; + + if (!humanDuration) return undefined; + + return durationToMilliseconds(humanDuration); +}; + +/** + * Reads retry configuration options from the provided config object. + * + * @param config - The configuration object to read retry options from, or undefined. + * @returns A RetryOptions object with optional retry settings, or an empty object if config is undefined. + */ +export const readRetryConfig = (config: Config | undefined): RetryOptions => { + if (!config) { + return {}; + } + + return { + maxRetryTime: readOptionalHumanDurationInMs(config, 'maxRetryTime'), + initialRetryTime: readOptionalHumanDurationInMs(config, 'initialRetryTime'), + factor: config.getOptionalNumber('factor'), + multiplier: config.getOptionalNumber('multiplier'), + retries: config.getOptionalNumber('retries'), + }; +}; + +/** + * Reads Kafka configuration from the provided config object. + * + * @param config - The configuration object containing Kafka settings. + * @returns A KafkaConfig object with all necessary Kafka connection and authentication settings. + */ +export const readKafkaConfig = (config: Config): KafkaConfig => { + return { + clientId: config.getString('clientId'), + brokers: config.getStringArray('brokers'), + authenticationTimeout: readOptionalHumanDurationInMs( + config, + 'authenticationTimeout', + ), + connectionTimeout: readOptionalHumanDurationInMs( + config, + 'connectionTimeout', + ), + requestTimeout: readOptionalHumanDurationInMs(config, 'requestTimeout'), + enforceRequestTimeout: config.getOptionalBoolean('enforceRequestTimeout'), + ssl: config.getOptional('ssl') as KafkaConfig['ssl'], + sasl: config.getOptional('sasl') as KafkaConfig['sasl'], + retry: readRetryConfig(config.getOptionalConfig('retry')), + }; +}; diff --git a/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.test.ts b/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.test.ts new file mode 100644 index 0000000000..bfb373ea53 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { convertHeadersToMetadata, payloadToBuffer } from './kafkaTransformers'; + +describe('kafka-transformers', () => { + describe('convertHeadersToMetadata', () => { + it('should return undefined when headers is undefined', () => { + const result = convertHeadersToMetadata(undefined); + expect(result).toBeUndefined(); + }); + + it('should convert string headers to metadata', () => { + const headers = { + 'content-type': 'application/json', + 'user-id': '12345', + }; + + const result = convertHeadersToMetadata(headers); + + expect(result).toEqual({ + 'content-type': 'application/json', + 'user-id': '12345', + }); + }); + + it('should convert Buffer headers to string metadata', () => { + const headers = { + 'content-type': Buffer.from('application/json'), + 'correlation-id': Buffer.from('abc-123'), + }; + + const result = convertHeadersToMetadata(headers); + + expect(result).toEqual({ + 'content-type': 'application/json', + 'correlation-id': 'abc-123', + }); + }); + + it('should convert array headers to string array metadata', () => { + const headers = { + tags: ['tag1', 'tag2'], + 'buffer-tags': [Buffer.from('tag3'), Buffer.from('tag4')], + 'mixed-tags': ['tag5', Buffer.from('tag6')], + }; + + const result = convertHeadersToMetadata(headers); + + expect(result).toEqual({ + tags: ['tag1', 'tag2'], + 'buffer-tags': ['tag3', 'tag4'], + 'mixed-tags': ['tag5', 'tag6'], + }); + }); + + it('should handle mixed header types', () => { + const headers = { + 'string-header': 'value', + 'buffer-header': Buffer.from('buffer-value'), + 'array-header': ['item1', Buffer.from('item2')], + 'undefined-header': undefined, + }; + + const result = convertHeadersToMetadata(headers); + + expect(result).toEqual({ + 'string-header': 'value', + 'buffer-header': 'buffer-value', + 'array-header': ['item1', 'item2'], + 'undefined-header': undefined, + }); + }); + + it('should handle empty headers object', () => { + const headers = {}; + const result = convertHeadersToMetadata(headers); + expect(result).toEqual({}); + }); + }); + + describe('payloadToBuffer', () => { + it('should return the same Buffer when payload is already a Buffer', () => { + const originalBuffer = Buffer.from('test data'); + + const result = payloadToBuffer(originalBuffer); + + expect(result).toBe(originalBuffer); + expect(Buffer.isBuffer(result)).toBe(true); + }); + + it('should convert string to Buffer', () => { + const payload = 'hello world'; + + const result = payloadToBuffer(payload); + + expect(Buffer.isBuffer(result)).toBe(true); + expect(result.toString()).toBe('hello world'); + }); + + it('should convert object to JSON Buffer', () => { + const payload = { name: 'John', age: 30 }; + + const result = payloadToBuffer(payload); + + expect(Buffer.isBuffer(result)).toBe(true); + expect(JSON.parse(result.toString())).toEqual(payload); + }); + + it('should convert array to JSON Buffer', () => { + const payload = [1, 2, 3, 'test']; + + const result = payloadToBuffer(payload); + + expect(Buffer.isBuffer(result)).toBe(true); + expect(JSON.parse(result.toString())).toEqual(payload); + }); + + it('should convert primitives to JSON Buffer', () => { + expect(payloadToBuffer(42).toString()).toBe('42'); + expect(payloadToBuffer(true).toString()).toBe('true'); + expect(payloadToBuffer(null).toString()).toBe('null'); + }); + }); +}); diff --git a/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.ts b/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.ts new file mode 100644 index 0000000000..871cb08a04 --- /dev/null +++ b/plugins/events-backend-module-kafka/src/utils/kafkaTransformers.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventParams } from '@backstage/plugin-events-node'; +import { IHeaders } from 'kafkajs'; + +type EventMetadata = EventParams['metadata']; + +export const convertHeadersToMetadata = ( + headers: IHeaders | undefined, +): EventMetadata => { + if (!headers) return undefined; + + const metadata: EventMetadata = {}; + + Object.entries(headers).forEach(([key, value]) => { + // If value is an array use toString() on all values converting any Buffer types to valid strings + if (Array.isArray(value)) metadata[key] = value.map(v => v.toString()); + // Always return the values using toString() to catch all Buffer types that should be converted to strings + else metadata[key] = value?.toString(); + }); + + return metadata; +}; + +export const payloadToBuffer = (payload: unknown): Buffer => { + if (Buffer.isBuffer(payload)) { + return payload; + } + + if (typeof payload === 'string') { + return Buffer.from(payload, 'utf8'); // More explicit encoding + } + + // Convert to JSON string then encode + return Buffer.from(JSON.stringify(payload), 'utf8'); +}; diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 8c3f506f9b..a9233cc14a 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.51 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18 + +## 0.1.51-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.18-next.0 + +## 0.1.50 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.17 + ## 0.1.50-next.0 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 407984484f..814fb23241 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.50-next.0", + "version": "0.1.51", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 32d86d124f..ff897a066f 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,80 @@ # @backstage/plugin-events-backend +## 0.5.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.1 + - @backstage/backend-openapi-utils@0.6.5 + +## 0.5.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.5-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.18 + +## 0.5.9 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4 + - @backstage/plugin-events-node@0.4.18 + - @backstage/backend-plugin-api@1.6.0 + +## 0.5.9-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.4-next.1 + - @backstage/plugin-events-node@0.4.18-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/backend-openapi-utils@0.6.4-next.0 + - @backstage/plugin-events-node@0.4.18-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.5.8 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-events-node@0.4.17 + - @backstage/config@1.3.6 + - @backstage/backend-openapi-utils@0.6.3 + +## 0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/backend-openapi-utils@0.6.3-next.1 + - @backstage/plugin-events-node@0.4.17-next.1 + ## 0.5.8-next.0 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index 04aa18fd9a..4d20cb5236 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.5.8-next.0", + "version": "0.5.10", "backstage": { "role": "backend-plugin", "pluginId": "events", @@ -61,7 +61,7 @@ "@backstage/types": "workspace:^", "@types/express": "^4.17.6", "content-type": "^1.0.5", - "express": "^4.17.1", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "knex": "^3.0.0" }, diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index f2c61737f5..d748102a2b 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-events-node +## 0.4.18 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## 0.4.18-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.4.17 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## 0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 0.4.17-next.0 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 2c95bda276..c8dc226198 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.4.17-next.0", + "version": "0.4.18", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", @@ -59,13 +59,13 @@ "@types/express": "^4.17.6", "content-type": "^1.0.5", "cross-fetch": "^4.0.0", - "express": "^4.17.1", + "express": "^4.22.0", "uri-template": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "msw": "^1.0.0" + "msw": "^2.0.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/events-node/src/api/DefaultEventsService.test.ts b/plugins/events-node/src/api/DefaultEventsService.test.ts index 13a6a68fa4..a62cc24f14 100644 --- a/plugins/events-node/src/api/DefaultEventsService.test.ts +++ b/plugins/events-node/src/api/DefaultEventsService.test.ts @@ -17,7 +17,7 @@ import { DefaultEventsService } from './DefaultEventsService'; import { EventParams } from './EventParams'; import { EVENTS_NOTIFY_TIMEOUT_HEADER } from './EventsService'; -import { rest } from 'msw'; +import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { mockServices, @@ -129,18 +129,18 @@ describe('DefaultEventsService', () => { }); mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => res(ctx.status(200)), + () => new HttpResponse(null, { status: 200 }), ), - rest.get( + http.get( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester/events', - (_req, res, ctx) => - res( - ctx.status(200), - ctx.json({ + () => + HttpResponse.json( + { events: [{ topic: 'test', payload: { foo: 'bar' } }], - }), + }, + { status: 200 }, ), ), ); @@ -180,38 +180,36 @@ describe('DefaultEventsService', () => { }); mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => res(ctx.status(200)), + () => new HttpResponse(null, { status: 200 }), ), // The first and third calls result in a blocking 202 that is resolved after 100ms // The second and fourth calls result in a 200 with an event // The fifth call blocks until the end of the test // No more than 5 calls should be made - rest.get( + http.get( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester/events', - (_req, res, ctx) => { + () => { callCount += 1; if (callCount === 1 || callCount === 3) { - return res( - ctx.status(202), - ctx.body( - new ReadableStream({ - start(controller) { - setTimeout(() => controller.close(), 100); - }, - }), - ), + return new HttpResponse( + new ReadableStream({ + start(controller) { + setTimeout(() => controller.close(), 100); + }, + }), + { status: 202 }, ); } else if (callCount === 2 || callCount === 4) { - return res( - ctx.status(200), - ctx.json({ + return HttpResponse.json( + { events: [{ topic: 'test', payload: { callCount } }], - }), + }, + { status: 200 }, ); } else if (callCount === 5) { - return res(ctx.status(202), ctx.body(blockingStream)); + return new HttpResponse(blockingStream, { status: 202 }); } throw new Error(`events endpoint called too many times`); }, @@ -268,45 +266,42 @@ describe('DefaultEventsService', () => { }); mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => res(ctx.status(200)), + () => new HttpResponse(null, { status: 200 }), ), // The first and third calls result in a blocking 202 that is resolved after 100ms // The second and fourth calls result in a 200 with an event // The fifth call blocks until the end of the test // No more than 5 calls should be made - rest.get( + http.get( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester/events', - (_req, res, ctx) => { + () => { callCount += 1; if (callCount === 1 || callCount === 3) { - return res( - ctx.status(202), - ctx.body( - new ReadableStream({ - start(controller) { - setTimeout(() => controller.close(), 100); - }, - }), - ), + return new HttpResponse( + new ReadableStream({ + start(controller) { + setTimeout(() => controller.close(), 100); + }, + }), + { status: 202 }, ); } else if (callCount === 2 || callCount === 4) { - return res( - ctx.status(200), - ctx.json({ + return HttpResponse.json( + { events: [{ topic: 'test', payload: { callCount } }], - }), + }, + { status: 200 }, ); } else if (callCount === 5) { // 5th call has a timeout header so polling should proceed to the next call - return res( - ctx.set(EVENTS_NOTIFY_TIMEOUT_HEADER, '100'), - ctx.status(202), - ctx.body(blockingStream), - ); + return new HttpResponse(blockingStream, { + status: 202, + headers: { [EVENTS_NOTIFY_TIMEOUT_HEADER]: '100' }, + }); } else if (callCount === 6) { - return res(ctx.status(202), ctx.body(blockingStream)); + return new HttpResponse(blockingStream, { status: 202 }); } throw new Error(`events endpoint called too many times`); }, @@ -358,11 +353,11 @@ describe('DefaultEventsService', () => { let calledApi = false; mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => { + () => { calledApi = true; - res(ctx.status(200)); + return new HttpResponse(null, { status: 200 }); }, ), ); @@ -393,9 +388,9 @@ describe('DefaultEventsService', () => { }); mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => res(ctx.status(404)), + () => new HttpResponse(null, { status: 404 }), ), ); @@ -432,9 +427,9 @@ describe('DefaultEventsService', () => { }); mswServer.use( - rest.put( + http.put( 'http://localhost:0/api/events/bus/v1/subscriptions/a.tester', - (_req, res, ctx) => res(ctx.status(404)), + () => new HttpResponse(null, { status: 404 }), ), ); diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index adab889c52..4d7a89509f 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,42 @@ # @internal/plugin-todo-list-backend +## 1.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## 1.0.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.7 + +## 1.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/errors@1.2.7 + +## 1.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## 1.0.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 1.0.45-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 35c519c5b3..108655947a 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.45-next.0", + "version": "1.0.46", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", @@ -41,7 +41,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@types/express": "^4.17.6", - "express": "^4.17.1", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "uuid": "^11.0.0" }, diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index c3bcdb7990..d0cdfc8f69 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.3 + ## 1.0.28-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 75bc78e24d..7dc0e36cb1 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.28-next.0", + "version": "1.0.28", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index d6d35bfcc0..5399144b6b 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,43 @@ # @internal/plugin-todo-list +## 1.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5 + +## 1.0.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## 1.0.46 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## 1.0.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + +## 1.0.45 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + ## 1.0.45-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index ecec99b269..045a5e109b 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.45-next.0", + "version": "1.0.47", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/gateway-backend/CHANGELOG.md b/plugins/gateway-backend/CHANGELOG.md index 96c380f821..5e6c6254d9 100644 --- a/plugins/gateway-backend/CHANGELOG.md +++ b/plugins/gateway-backend/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-gateway-backend +## 1.1.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 7e860dd: Updated dependency `eventsource` to `^4.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + +## 1.1.1-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + +## 1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 229f63e: Added hop count tracking to prevent proxy loops. The gateway now tracks the number of proxy hops using the `backstage-gateway-hops` header and rejects requests that exceed 3 hops with a 508 Loop Detected error. +- a17d9df: Update usage of the `instanceMetadata` service. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0 + +## 1.1.0-next.1 + +### Minor Changes + +- a17d9df: Update usage of the `instanceMetadata` service. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 1.0.7-next.0 ### Patch Changes diff --git a/plugins/gateway-backend/package.json b/plugins/gateway-backend/package.json index 8dfdf08fd9..4cc39faf7b 100644 --- a/plugins/gateway-backend/package.json +++ b/plugins/gateway-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gateway-backend", - "version": "1.0.7-next.0", + "version": "1.1.1", "backstage": { "role": "backend-plugin", "pluginId": "gateway", @@ -37,7 +37,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.29.0", - "express": "^4.17.1", + "express": "^4.22.0", "http-proxy-middleware": "^3.0.3" }, "devDependencies": { @@ -46,7 +46,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/express": "^4.17.6", - "eventsource": "^3.0.6", + "eventsource": "^4.0.0", "wait-for-expect": "^3.0.2" } } diff --git a/plugins/gateway-backend/src/plugin.test.ts b/plugins/gateway-backend/src/plugin.test.ts index 078fde044e..8db99e39a5 100644 --- a/plugins/gateway-backend/src/plugin.test.ts +++ b/plugins/gateway-backend/src/plugin.test.ts @@ -161,6 +161,41 @@ describe('gateway', () => { expect(data).toEqual({ bar: true }); }); + it('should detect looped requests', async () => { + const response = await fetch( + 'http://localhost:7777/api/nonexistent-plugin/foo', + ); + expect(response.status).toBe(508); + + const data = await response.json(); + expect(data).toEqual({ + error: { + name: 'LoopDetectedError', + message: 'Maximum proxy hop count exceeded (3)', + }, + }); + }); + + it('should detect looped requests with intentional negative hop count', async () => { + const response = await fetch( + 'http://localhost:7777/api/nonexistent-plugin/foo', + { + headers: { + 'backstage-gateway-hops': '-1000000', + }, + }, + ); + expect(response.status).toBe(508); + + const data = await response.json(); + expect(data).toEqual({ + error: { + name: 'LoopDetectedError', + message: 'Maximum proxy hop count exceeded (3)', + }, + }); + }); + it('should close the response for sse connections', async () => { const eventSource = new EventSource( 'http://localhost:7777/api/external-plugin/endpoint-sse', diff --git a/plugins/gateway-backend/src/plugin.ts b/plugins/gateway-backend/src/plugin.ts index 94b979d1bc..6facc1ddea 100644 --- a/plugins/gateway-backend/src/plugin.ts +++ b/plugins/gateway-backend/src/plugin.ts @@ -18,7 +18,6 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; import { Handler } from 'express'; /** @@ -33,17 +32,17 @@ export const gatewayPlugin = createBackendPlugin({ deps: { logger: coreServices.logger, rootHttpRouter: coreServices.rootHttpRouter, - instanceMeta: instanceMetadataServiceRef, + instanceMeta: coreServices.rootInstanceMetadata, discovery: coreServices.discovery, }, async init({ logger, discovery, instanceMeta, rootHttpRouter }) { rootHttpRouter.use( '/api/:pluginId', - createRouter({ + (await createRouter({ discovery, instanceMeta, logger, - }) as Handler, + })) as Handler, ); }, }); diff --git a/plugins/gateway-backend/src/router.ts b/plugins/gateway-backend/src/router.ts index 74b3c6e066..6c9b4ba6c7 100644 --- a/plugins/gateway-backend/src/router.ts +++ b/plugins/gateway-backend/src/router.ts @@ -13,27 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api'; -import { InstanceMetadataService } from '@backstage/backend-plugin-api/alpha'; +import { + DiscoveryService, + RootInstanceMetadataService, + LoggerService, +} from '@backstage/backend-plugin-api'; import { Request, Response, NextFunction } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { context } from '@opentelemetry/api'; import { getRPCMetadata } from '@opentelemetry/core'; -export function createRouter({ +const MAX_HOPS = 3; +const HOPS_HEADER = 'backstage-gateway-hops'; + +export async function createRouter({ discovery, instanceMeta, + logger, }: { discovery: DiscoveryService; - instanceMeta: InstanceMetadataService; + instanceMeta: RootInstanceMetadataService; logger: LoggerService; }) { - const localPluginIds = new Set( - instanceMeta - .getInstalledFeatures() - .filter(f => f.type === 'plugin') - .map(f => f.pluginId), - ); + const plugins = await instanceMeta.getInstalledPlugins(); + const localPluginIds = new Set(plugins.map(f => f.pluginId)); const proxy = createProxyMiddleware({ changeOrigin: true, @@ -42,6 +45,12 @@ export function createRouter({ return discovery.getBaseUrl(pluginId); }, on: { + proxyReq(proxyReq, req: Request<{ pluginId: string }>) { + const currentHops = + Math.max(parseInt(req.headers[HOPS_HEADER] as string, 10), 0) || 0; + + proxyReq.setHeader(HOPS_HEADER, currentHops + 1); + }, proxyRes(proxyRes, _req, res) { // https://github.com/chimurai/http-proxy-middleware/discussions/765 proxyRes.on('close', () => { @@ -63,6 +72,20 @@ export function createRouter({ return; } + const currentHops = parseInt(req.headers[HOPS_HEADER] as string, 10) || 0; + if (currentHops >= MAX_HOPS) { + logger.warn( + `Proxy loop detected for plugin '${req.params.pluginId}': request exceeded maximum hop count (${currentHops})`, + ); + res.status(508).json({ + error: { + name: 'LoopDetectedError', + message: `Maximum proxy hop count exceeded (${currentHops})`, + }, + }); + return; + } + const rpcMetadata = getRPCMetadata(context.active()); if (rpcMetadata) { rpcMetadata.route = req.baseUrl; diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 8b3c322325..57d6e2b2a4 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-home-react +## 0.1.34 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## 0.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## 0.1.33 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + +## 0.1.33-next.1 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/core-components@0.18.4-next.2 + - @backstage/core-plugin-api@1.12.1-next.0 + +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/core-components@0.18.4-next.0 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + ## 0.1.32-next.0 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index 8375222222..4cdb1e6af3 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.32-next.0", + "version": "0.1.34", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", @@ -60,7 +60,7 @@ "@backstage/frontend-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@rjsf/utils": "5.23.2" + "@rjsf/utils": "5.24.13" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 77b44362a5..23f51fa263 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,125 @@ # @backstage/plugin-home +## 0.9.0 + +### Minor Changes + +- e091a83: Widget configurations are now only saved to storage when the Save button is explicitly clicked. Added a Cancel button that allows users to discard unsaved changes and revert to the last saved state. + +### Patch Changes + +- bdda543: Updated WidgetOverlay color to use `alpha(theme.palette.background.paper, 0.93)` for better theme alignment instead of hardcoded RGBA +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-home-react@0.1.34 + +## 0.9.0-next.2 + +### Minor Changes + +- e091a83: Widget configurations are now only saved to storage when the Save button is explicitly clicked. Added a Cancel button that allows users to discard unsaved changes and revert to the last saved state. + +## 0.8.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-home-react@0.1.34-next.0 + +## 0.8.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.8.15 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/core-app-api@1.19.3 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-home-react@0.1.33 + - @backstage/theme@0.7.1 + +## 0.8.15-next.1 + +### Patch Changes + +- be21c5c: Updated dependency `@rjsf/utils` to `5.24.13`. + Updated dependency `@rjsf/core` to `5.24.13`. + Updated dependency `@rjsf/material-ui` to `5.24.13`. + Updated dependency `@rjsf/validator-ajv8` to `5.24.13`. +- Updated dependencies + - @backstage/core-app-api@1.19.3-next.1 + - @backstage/frontend-plugin-api@0.13.2-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.2 + - @backstage/core-components@0.18.4-next.2 + - @backstage/plugin-home-react@0.1.33-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + +## 0.8.15-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-app-api@1.19.3-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/plugin-home-react@0.1.33-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + +## 0.8.14 + +### Patch Changes + +- 2ac5d29: Allow customization of `VisitList` by adding optional `enrichVisit`, `transformPathname`, `canSave` functions to `VisitsStorageApi`, along with `VisitDisplayProvider` for colors, labels +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/core-app-api@1.19.2 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-home-react@0.1.32 + - @backstage/catalog-client@1.12.1 + +## 0.8.14-next.1 + +### Patch Changes + +- 2ac5d29: Allow customization of VisitList by adding optional enrichVisit, transformPathname, canSave functions to VisitsStorageApi, along with VisitDisplayProvider for colors, labels +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.3-next.2 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + ## 0.8.14-next.0 ### Patch Changes diff --git a/plugins/home/README.md b/plugins/home/README.md index 3498d6700f..242e66ff40 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -132,7 +132,7 @@ export const RandomJokeHomePageComponent = homePlugin.provide( ); ``` -These settings can also be defined for components that use `createReactExtension` instead `createCardExtension` by using +These settings can also be defined for components that use `createReactExtension` instead of `createCardExtension` by using the data property: ```tsx @@ -356,13 +356,189 @@ home: In order to validate the config you can use `backstage/cli config:check` +### Customizing the VisitList + +If you want more control over the recent and top visited lists, you can write your own functions to transform the path names and determine which visits to save. You can also enrich each visit with other fields and customize the chip colors/labels in the visit lists. + +#### Transform Pathname Function + +Provide a `transformPathname` function to transform the pathname before it's processed for visit tracking. This can be used for transforming the pathname for the visit (before any other consideration). As an example, you can treat multiple sub-path visits to be counted as a singular path, e.g. `/entity-path/sub1` , `/entity-path/sub-2`, `/entity-path/sub-2/sub-sub-2` can all be mapped to `/entity-path` so visits to any of those routes are all counted as the same. + +```tsx +import { + AnyApiFactory, + createApiFactory, + identityApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { VisitsStorageApi } from '@backstage/plugin-home'; + +const transformPathname = (pathname: string) => { + const pathnameParts = pathname.split('/').filter(part => part !== ''); + const rootPathFromPathname = pathnameParts[0] ?? ''; + if (rootPathFromPathname === 'catalog' && pathnameParts.length >= 4) { + return `/${pathnameParts.slice(0, 4).join('/')}`; + } + return pathname; +}; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: visitsApiRef, + deps: { + storageApi: storageApiRef, + identityApi: identityApiRef, + }, + factory: ({ storageApi, identityApi }) => + VisitsStorageApi.create({ + storageApi, + identityApi, + transformPathname, + }), + }), +]; +``` + +#### Can Save Function + +Provide a `canSave` function to determine which visits should be tracked and saved. This allows you to conditionally save visits to the list: + +```tsx +import { + AnyApiFactory, + createApiFactory, + identityApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { VisitInput, VisitsStorageApi } from '@backstage/plugin-home'; + +const canSave = (visit: VisitInput) => { + // Don't save visits to admin or settings pages + return ( + !visit.pathname.startsWith('/admin') && + !visit.pathname.startsWith('/settings') + ); +}; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: visitsApiRef, + deps: { + storageApi: storageApiRef, + identityApi: identityApiRef, + }, + factory: ({ storageApi, identityApi }) => + VisitsStorageApi.create({ + storageApi, + identityApi, + canSave, + }), + }), +]; +``` + +#### Enrich Visit Function + +You can also add the `enrichVisit` function to put additional values on each `Visit`. The values could later be used to customize the chips in the `VisitList`. For example, you could add the entity `type` on the `Visit` so that `type` is used for labels instead of `kind`. + +```tsx +import { + AnyApiFactory, + createApiFactory, + identityApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { VisitsStorageApi } from '@backstage/plugin-home'; + +type EnrichedVisit = VisitInput & { + type?: string; +}; + +const createEnrichVisit = + (catalogApi: CatalogApi) => + async (visit: VisitInput): Promise => { + if (!visit.entityRef) { + return visit; + } + try { + const entity = await catalogApi.getEntityByRef(visit.entityRef); + const type = entity?.spec?.type?.toString(); + return { ...visit, type }; + } catch (error) { + return visit; + } + }; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: visitsApiRef, + deps: { + storageApi: storageApiRef, + identityApi: identityApiRef, + catalogApi: catalogApiRef, + }, + factory: ({ storageApi, identityApi, catalogApi }) => + VisitsStorageApi.create({ + storageApi, + identityApi, + enrichVisit: createEnrichVisit(catalogApi), + }), + }), +]; +``` + +#### Custom Chip Colors and Labels + +To provide your own chip colors and/or labels for the recent and top visited lists, wrap the components in `VisitDisplayProvider` with `getChipColor` and `getChipLabel` functions. The colors provided will be used instead of the hard coded [`colorVariants`](https://github.com/backstage/backstage/blob/2da352043425bcab4c4422e4d2820c26c0a83382/packages/theme/src/base/pageTheme.ts#L46) provided via `@backstage/theme`. + +```tsx +import { + CustomHomepageGrid, + HomePageTopVisited, + HomePageRecentlyVisited, + VisitDisplayProvider, +} from '@backstage/plugin-home'; + +const getChipColor = (visit: any) => { + const type = visit.type; + switch (type) { + case 'application': + return '#b39ddb'; + case 'service': + return '#90caf9'; + case 'account': + return '#a5d6a7'; + case 'suite': + return '#fff59d'; + default: + return '#ef9a9a'; + } +}; + +const getChipLabel = (visit?: any) => { + return visit?.type ? visit.type : 'Other'; +}; + +export default function HomePage() { + return ( + + + + + + + ); +} +``` + ## Contributing ### Homepage Components -We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for every to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components, than can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) +We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for everyone to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components that can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) -Additionally, the API is at a very early state, so contributing with additional use cases may expose weaknesses in the current solution that we may iterate on, to provide more flexibility and ease of use for those who wish to develop components for the Home Page. +Additionally, the API is at a very early state, so contributing additional use cases may expose weaknesses in the current solution that we may iterate on to provide more flexibility and ease of use for those who wish to develop components for the Home Page. ### Homepage Templates diff --git a/plugins/home/package.json b/plugins/home/package.json index 5557f9c2b8..dfe28cb08f 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.14-next.0", + "version": "0.9.0", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", @@ -60,7 +60,6 @@ "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", - "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", @@ -70,10 +69,10 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "@rjsf/core": "5.23.2", - "@rjsf/material-ui": "5.23.2", - "@rjsf/utils": "5.23.2", - "@rjsf/validator-ajv8": "5.23.2", + "@rjsf/core": "5.24.13", + "@rjsf/material-ui": "5.24.13", + "@rjsf/utils": "5.24.13", + "@rjsf/validator-ajv8": "5.24.13", "lodash": "^4.17.21", "luxon": "^3.4.3", "react-grid-layout": "1.3.4", diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 8dbbb99b5d..6344805372 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -9,9 +9,9 @@ import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; @@ -23,7 +23,7 @@ const _default: OverridableFrontendPlugin< }, {}, { - 'api:home/visits': ExtensionDefinition<{ + 'api:home/visits': OverridableExtensionDefinition<{ kind: 'api'; name: 'visits'; config: {}; @@ -38,7 +38,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'app-root-element:home/visit-listener': ExtensionDefinition<{ + 'app-root-element:home/visit-listener': OverridableExtensionDefinition<{ kind: 'app-root-element'; name: 'visit-listener'; config: {}; @@ -49,7 +49,7 @@ const _default: OverridableFrontendPlugin< element: JSX.Element; }; }>; - 'page:home': ExtensionDefinition<{ + 'page:home': OverridableExtensionDefinition<{ config: { path: string | undefined; }; @@ -107,6 +107,7 @@ export const homeTranslationRef: TranslationRef< { readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; readonly 'addWidgetDialog.title': 'Add new widget to dashboard'; + readonly 'customHomepageButtons.cancel': 'Cancel'; readonly 'customHomepageButtons.clearAll': 'Clear all'; readonly 'customHomepageButtons.edit': 'Edit'; readonly 'customHomepageButtons.restoreDefaults': 'Restore defaults'; diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index dd33b952ee..7b15ecd8a7 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -3,7 +3,7 @@ > 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 { ApiRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CardConfig as CardConfig_2 } from '@backstage/plugin-home-react'; import { CardExtensionProps as CardExtensionProps_2 } from '@backstage/plugin-home-react'; @@ -117,6 +117,12 @@ export type FeaturedDocsCardProps = { subLinkText?: string; }; +// @public +export type GetChipColorFunction = (visit: Visit) => string; + +// @public +export type GetLabelFunction = (visit: Visit) => string; + // @public export const HeaderWorldClock: (props: { clockConfigs: ClockConfig[]; @@ -245,6 +251,9 @@ export type ToolkitContentProps = { tools: Tool[]; }; +// @public +export const useVisitDisplay: () => VisitDisplayContextValue; + // @public export type Visit = { id: string; @@ -255,6 +264,31 @@ export type Visit = { entityRef?: string; }; +// @public +export interface VisitDisplayContextValue { + // (undocumented) + getChipColor: GetChipColorFunction; + // (undocumented) + getLabel: GetLabelFunction; +} + +// @public +export const VisitDisplayProvider: ({ + children, + getChipColor, + getLabel, +}: VisitDisplayProviderProps) => JSX_2.Element; + +// @public +export interface VisitDisplayProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + getChipColor?: GetChipColorFunction; + // (undocumented) + getLabel?: GetLabelFunction; +} + // @public (undocumented) export type VisitedByTypeKind = 'recent' | 'top'; @@ -267,6 +301,13 @@ export type VisitedByTypeProps = { kind: VisitedByTypeKind; }; +// @public +export type VisitInput = { + name: string; + pathname: string; + entityRef?: string; +}; + // @public export const VisitListener: ({ children, @@ -280,8 +321,13 @@ export const VisitListener: ({ // @public export interface VisitsApi { + canSave?(visit: VisitInput): boolean | Promise; + enrichVisit?( + visit: VisitInput, + ): Promise> | Record; list(queryParams?: VisitsApiQueryParams): Promise; save(saveParams: VisitsApiSaveParams): Promise; + transformPathname?(pathname: string): string; } // @public @@ -308,10 +354,13 @@ export type VisitsApiSaveParams = { // @public export class VisitsStorageApi implements VisitsApi { + canSave(visit: VisitInput): Promise; // (undocumented) static create(options: VisitsStorageApiOptions): VisitsStorageApi; + enrichVisit(visit: VisitInput): Promise>; list(queryParams?: VisitsApiQueryParams): Promise; save(saveParams: VisitsApiSaveParams): Promise; + transformPathname(pathname: string): string; } // @public (undocumented) @@ -319,6 +368,11 @@ export type VisitsStorageApiOptions = { limit?: number; storageApi: StorageApi; identityApi: IdentityApi; + transformPathname?: (pathname: string) => string; + canSave?: (visit: VisitInput) => boolean | Promise; + enrichVisit?: ( + visit: VisitInput, + ) => Promise> | Record; }; // @public diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index a92316c7f0..6cad71d643 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -26,7 +26,6 @@ import { storageApiRef, ApiBlueprint, } from '@backstage/frontend-plugin-api'; -import { compatWrapper } from '@backstage/core-compat-api'; import { VisitListener } from './components/'; import { visitsApiRef, VisitsStorageApi } from './api'; @@ -57,14 +56,12 @@ const homePage = PageBlueprint.makeWithOverrides({ path: '/home', routeRef: rootRouteRef, loader: () => - import('./components/').then(m => - compatWrapper( - , - ), - ), + import('./components/').then(m => ( + + )), }); }, }); diff --git a/plugins/home/src/api/VisitsApi.ts b/plugins/home/src/api/VisitsApi.ts index 610f4be483..5cbd65fab3 100644 --- a/plugins/home/src/api/VisitsApi.ts +++ b/plugins/home/src/api/VisitsApi.ts @@ -15,6 +15,7 @@ */ import { createApiRef } from '@backstage/core-plugin-api'; +import { VisitInput } from './VisitsStorageApi'; /** * @public @@ -126,6 +127,23 @@ export interface VisitsApi { * @param queryParams - optional search query params. */ list(queryParams?: VisitsApiQueryParams): Promise; + /** + * Transform the pathname before it is considered for any other processing. + * @param pathname - the original pathname + */ + transformPathname?(pathname: string): string; + /** + * Determine whether a visit should be saved. + * @param visit - page visit data + */ + canSave?(visit: VisitInput): boolean | Promise; + /** + * Add additional data to the visit before saving. + * @param visit - page visit data + */ + enrichVisit?( + visit: VisitInput, + ): Promise> | Record; } /** @public */ diff --git a/plugins/home/src/api/VisitsStorageApi.test.ts b/plugins/home/src/api/VisitsStorageApi.test.ts index 55cf623149..c6629a39a0 100644 --- a/plugins/home/src/api/VisitsStorageApi.test.ts +++ b/plugins/home/src/api/VisitsStorageApi.test.ts @@ -359,4 +359,202 @@ describe('VisitsStorageApi.create', () => { expect(visits.length).toEqual(8); }); }); + describe('.save() with transformPathname', () => { + it('transforms pathname before saving', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + transformPathname: (pathname: string) => + pathname.replace(/\/admin$/, ''), + }); + + const visit = { + pathname: '/catalog/default/component/test/admin', + entityRef: 'component:default/test', + name: 'Test Component', + }; + + const savedVisit = await api.save({ visit }); + expect(savedVisit.pathname).toBe('/catalog/default/component/test'); + }); + }); + + describe('.save() with canSave', () => { + it('skips saving when canSave returns false', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + canSave: visitInput => !visitInput.pathname.includes('/private'), + }); + + const privateVisit = { + pathname: '/private/admin', + entityRef: 'component:default/admin', + name: 'Admin Component', + }; + + const result = await api.save({ visit: privateVisit }); + expect(result.id).toBe(''); + expect(result.hits).toBe(0); + + const visits = await api.list(); + expect(visits).toHaveLength(0); + }); + + it('saves when canSave returns true', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + canSave: visitInput => !visitInput.pathname.includes('/private'), + }); + + const publicVisit = { + pathname: '/catalog/default/component/public', + entityRef: 'component:default/public', + name: 'Public Component', + }; + + const result = await api.save({ visit: publicVisit }); + expect(result.id).toBeTruthy(); + expect(result.hits).toBe(1); + + const visits = await api.list(); + expect(visits).toHaveLength(1); + }); + + it('handles async canSave function', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + canSave: async visitInput => + Promise.resolve(!visitInput.pathname.includes('/restricted')), + }); + + const restrictedVisit = { + pathname: '/restricted/area', + entityRef: 'component:default/restricted', + name: 'Restricted Component', + }; + + const result = await api.save({ visit: restrictedVisit }); + expect(result.id).toBe(''); + + const visits = await api.list(); + expect(visits).toHaveLength(0); + }); + }); + + describe('.save() with enrichVisit', () => { + it('enriches visit data before saving', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + enrichVisit: visitInput => ({ + category: visitInput.entityRef?.split(':')[0] || 'unknown', + source: 'test', + }), + }); + + const visit = { + pathname: '/catalog/default/component/test', + entityRef: 'component:default/test', + name: 'Test Component', + }; + + const savedVisit = await api.save({ visit }); + expect(savedVisit).toEqual( + expect.objectContaining({ + ...visit, + category: 'component', + source: 'test', + }), + ); + + const visits = await api.list(); + expect(visits[0]).toEqual( + expect.objectContaining({ + category: 'component', + source: 'test', + }), + ); + }); + + it('handles async enrichVisit function', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + enrichVisit: async visitInput => + Promise.resolve({ + enrichedAt: Date.now(), + type: visitInput.entityRef?.split(':')[0], + }), + }); + + const visit = { + pathname: '/catalog/default/api/test-api', + entityRef: 'api:default/test-api', + name: 'Test API', + }; + + const savedVisit = await api.save({ visit }); + expect(savedVisit).toEqual( + expect.objectContaining({ + type: 'api', + enrichedAt: expect.any(Number), + }), + ); + }); + }); + + describe('.save() with combined options', () => { + it('applies transformPathname, canSave, and enrichVisit in sequence', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + transformPathname: pathname => pathname.toLowerCase(), + canSave: visitInput => !visitInput.pathname.includes('forbidden'), + enrichVisit: visitInput => ({ + processed: true, + originalPath: visitInput.pathname, + }), + }); + + const visit = { + pathname: '/CATALOG/Default/Component/Test', + entityRef: 'component:default/test', + name: 'Test Component', + }; + + const savedVisit = await api.save({ visit }); + expect(savedVisit).toEqual( + expect.objectContaining({ + pathname: '/catalog/default/component/test', + processed: true, + originalPath: '/catalog/default/component/test', + }), + ); + }); + + it('prevents saving when canSave returns false after pathname transformation', async () => { + const api = VisitsStorageApi.create({ + storageApi: mockApis.storage(), + identityApi: mockIdentityApi, + transformPathname: pathname => + pathname.replace('/test/', '/forbidden/'), + canSave: visitInput => !visitInput.pathname.includes('forbidden'), + }); + + const visit = { + pathname: '/catalog/test/component/sample', + entityRef: 'component:default/sample', + name: 'Sample Component', + }; + + const result = await api.save({ visit }); + expect(result.id).toBe(''); + + const visits = await api.list(); + expect(visits).toHaveLength(0); + }); + }); }); diff --git a/plugins/home/src/api/VisitsStorageApi.ts b/plugins/home/src/api/VisitsStorageApi.ts index da3adc285e..3a55a6b16f 100644 --- a/plugins/home/src/api/VisitsStorageApi.ts +++ b/plugins/home/src/api/VisitsStorageApi.ts @@ -21,11 +21,26 @@ import { VisitsApiSaveParams, } from './VisitsApi'; +/** + * @public + * Type definition for visit data before it's saved (without auto-generated fields) + */ +export type VisitInput = { + name: string; + pathname: string; + entityRef?: string; +}; + /** @public */ export type VisitsStorageApiOptions = { limit?: number; storageApi: StorageApi; identityApi: IdentityApi; + transformPathname?: (pathname: string) => string; + canSave?: (visit: VisitInput) => boolean | Promise; + enrichVisit?: ( + visit: VisitInput, + ) => Promise> | Record; }; type ArrayElement = A extends readonly (infer T)[] ? T : never; @@ -43,6 +58,13 @@ export class VisitsStorageApi implements VisitsApi { private readonly storageApi: StorageApi; private readonly storageKeyPrefix = '@backstage/plugin-home:visits'; private readonly identityApi: IdentityApi; + private readonly transformPathnameImpl?: (pathname: string) => string; + private readonly canSaveImpl?: ( + visit: VisitInput, + ) => boolean | Promise; + private readonly enrichVisitImpl?: ( + visit: VisitInput, + ) => Promise> | Record; static create(options: VisitsStorageApiOptions) { return new VisitsStorageApi(options); @@ -52,6 +74,9 @@ export class VisitsStorageApi implements VisitsApi { this.limit = Math.abs(options.limit ?? 100); this.storageApi = options.storageApi; this.identityApi = options.identityApi; + this.transformPathnameImpl = options.transformPathname; + this.canSaveImpl = options.canSave; + this.enrichVisitImpl = options.enrichVisit; } /** @@ -88,34 +113,90 @@ export class VisitsStorageApi implements VisitsApi { return visits.slice(0, queryParams?.limit ?? DEFAULT_LIST_LIMIT); } + /** + * Transform the pathname before it is considered for any other processing. + * @param pathname - the original pathname + * @returns the transformed pathname + */ + transformPathname(pathname: string): string { + return this.transformPathnameImpl?.(pathname) ?? pathname; + } + + /** + * Determine whether a visit should be saved. + * @param visit - page visit data + */ + async canSave(visit: VisitInput): Promise { + if (!this.canSaveImpl) { + return true; + } + return Promise.resolve(this.canSaveImpl(visit)); + } + + /** + * Add additional data to the visit before saving. + * @param visit - page visit data + */ + async enrichVisit(visit: VisitInput): Promise> { + if (!this.enrichVisitImpl) { + return {}; + } + return Promise.resolve(this.enrichVisitImpl(visit)); + } + /** * Saves a visit through the visitsApi */ async save(saveParams: VisitsApiSaveParams): Promise { + let visit = saveParams.visit; + + // Transform pathname if needed + visit = { + ...visit, + pathname: this.transformPathname(visit.pathname), + }; + + // Check if visit should be saved + if (!(await this.canSave(visit))) { + // Return a minimal visit object without saving + return { + ...visit, + id: '', + hits: 0, + timestamp: Date.now(), + }; + } + + // Enrich the visit + const enrichedData = await this.enrichVisit(visit); + const enrichedVisit = { ...visit, ...enrichedData }; + const visits: Visit[] = [...(await this.retrieveAll())]; - const visit: Visit = { - ...saveParams.visit, + const visitToSave: Visit = { + ...enrichedVisit, id: window.crypto.randomUUID(), hits: 1, timestamp: Date.now(), }; // Updates entry if pathname is already registered - const visitIndex = visits.findIndex(e => e.pathname === visit.pathname); + const visitIndex = visits.findIndex( + e => e.pathname === visitToSave.pathname, + ); if (visitIndex >= 0) { - visit.id = visits[visitIndex].id; - visit.hits = visits[visitIndex].hits + 1; - visits[visitIndex] = visit; + visitToSave.id = visits[visitIndex].id; + visitToSave.hits = visits[visitIndex].hits + 1; + visits[visitIndex] = visitToSave; } else { - visits.push(visit); + visits.push(visitToSave); } // Sort by time, most recent first visits.sort((a, b) => b.timestamp - a.timestamp); // Keep the most recent items up to limit await this.persistAll(visits.splice(0, this.limit)); - return visit; + return visitToSave; } private async persistAll(visits: Array) { diff --git a/plugins/home/src/api/index.ts b/plugins/home/src/api/index.ts index 944fa65330..3ac9d8fce8 100644 --- a/plugins/home/src/api/index.ts +++ b/plugins/home/src/api/index.ts @@ -17,3 +17,4 @@ export * from './VisitsStorageApi'; export * from './VisitsWebStorageApi'; export * from './VisitsApi'; +export type { VisitInput } from './VisitsStorageApi'; diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageButtons.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageButtons.tsx index 0496e923de..b7e397c995 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageButtons.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageButtons.tsx @@ -45,6 +45,7 @@ interface CustomHomepageButtonsProps { changeEditMode: (mode: boolean) => void; defaultConfigAvailable: boolean; restoreDefault: () => void; + cancel: () => void; } export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => { const { @@ -55,6 +56,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => { changeEditMode, defaultConfigAvailable, restoreDefault, + cancel, } = props; const styles = useStyles(); const { t } = useTranslationRef(homeTranslationRef); @@ -73,6 +75,9 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => { ) : ( <> + {defaultConfigAvailable && (
    { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const namespace = ingress.metadata?.namespace; return ( {namespace && ( - + )} diff --git a/plugins/kubernetes-react/src/components/KubernetesDialog/KubernetesDialog.tsx b/plugins/kubernetes-react/src/components/KubernetesDialog/KubernetesDialog.tsx index d1a2a8a825..182265d23e 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDialog/KubernetesDialog.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDialog/KubernetesDialog.tsx @@ -22,6 +22,8 @@ import IconButton from '@material-ui/core/IconButton'; import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@material-ui/icons/Close'; import { ReactNode, useState } from 'react'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -59,6 +61,7 @@ export const KubernetesDialog = ({ title, }: KubernetesDialogProps) => { const classes = useStyles(); + const { t } = useTranslationRef(kubernetesReactTranslationRef); const [open, setOpen] = useState(false); const openDialog = () => { @@ -80,7 +83,7 @@ export const KubernetesDialog = ({ {title} diff --git a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesDrawer.tsx b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesDrawer.tsx index 323ce650d9..aa0609ca06 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesDrawer.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesDrawer.tsx @@ -31,6 +31,8 @@ import { } from '@material-ui/core/styles'; import CloseIcon from '@material-ui/icons/Close'; import { ManifestYaml } from './ManifestYaml'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; const useDrawerContentStyles = makeStyles((_theme: Theme) => createStyles({ @@ -72,6 +74,7 @@ const KubernetesDrawerContent = ({ close, }: KubernetesDrawerContentProps) => { const classes = useDrawerContentStyles(); + const { t } = useTranslationRef(kubernetesReactTranslationRef); const [isYaml, setIsYaml] = useState(false); return ( @@ -86,7 +89,7 @@ const KubernetesDrawerContent = ({ close()} color="inherit" > @@ -104,10 +107,10 @@ const KubernetesDrawerContent = ({ onChange={event => { setIsYaml(event.target.checked); }} - name="YAML" + name={t('kubernetesDrawer.yaml')} /> } - label="YAML" + label={t('kubernetesDrawer.yaml')} /> diff --git a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx index fb52063177..fb9977fb4a 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDrawer/KubernetesStructuredMetadataTableDrawer.tsx @@ -38,6 +38,8 @@ import { ManifestYaml } from './ManifestYaml'; import { useApi } from '@backstage/core-plugin-api'; import { kubernetesClusterLinkFormatterApiRef } from '../../api'; import useAsync from 'react-use/esm/useAsync'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -102,20 +104,24 @@ export type LinkErrorPanelProps = { export const LinkErrorPanel = ({ cluster, errorMessage, -}: LinkErrorPanelProps) => ( - - {errorMessage && ( - Errors: {errorMessage} - )} - -); +}: LinkErrorPanelProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); + return ( + + {errorMessage && ( + + {t('linkErrorPanel.errorsLabel')} {errorMessage} + + )} + + ); +}; /** * @@ -152,6 +158,7 @@ const KubernetesStructuredMetadataTableDrawerContent = < renderObject, kind, }: KubernetesStructuredMetadataTableDrawerContentProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const [isYaml, setIsYaml] = useState(false); const formatter = useApi(kubernetesClusterLinkFormatterApiRef); @@ -175,13 +182,13 @@ const KubernetesStructuredMetadataTableDrawerContent = < - {object.metadata?.name ?? 'unknown name'} + {object.metadata?.name ?? t('kubernetesDrawer.unknownName')} toggleDrawer(e, false)} color="inherit" > @@ -201,10 +208,10 @@ const KubernetesStructuredMetadataTableDrawerContent = < onChange={event => { setIsYaml(event.target.checked); }} - name="YAML" + name={t('kubernetesDrawer.yaml')} /> } - label="YAML" + label={t('kubernetesDrawer.yaml')} /> diff --git a/plugins/kubernetes-react/src/components/KubernetesDrawer/ManifestYaml.tsx b/plugins/kubernetes-react/src/components/KubernetesDrawer/ManifestYaml.tsx index 3ccd32fcfb..604efc3055 100644 --- a/plugins/kubernetes-react/src/components/KubernetesDrawer/ManifestYaml.tsx +++ b/plugins/kubernetes-react/src/components/KubernetesDrawer/ManifestYaml.tsx @@ -18,6 +18,8 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'; import Switch from '@material-ui/core/Switch'; import jsyaml from 'js-yaml'; import { useState } from 'react'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; /** * Props of ManifestYaml @@ -34,6 +36,7 @@ export interface ManifestYamlProps { * @public */ export const ManifestYaml = ({ object }: ManifestYamlProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); // Toggle whether the Kubernetes resource managed fields should be shown in // the YAML display. This toggle is only available when the YAML is being // shown because managed fields are never visible in the structured display. @@ -47,10 +50,10 @@ export const ManifestYaml = ({ object }: ManifestYamlProps) => { onChange={event => { setManagedFields(event.target.checked); }} - name="Managed Fields" + name={t('kubernetesDrawer.managedFields')} /> } - label="Managed Fields" + label={t('kubernetesDrawer.managedFields')} /> { const { cluster, containerName, podName } = props; - + const { t } = useTranslationRef(kubernetesReactTranslationRef); const isPodExecTerminalSupported = useIsPodExecTerminalSupported(); return ( !isPodExecTerminalSupported.loading && isPodExecTerminalSupported.value && ( } - buttonText="Terminal" + buttonText={t('podExecTerminal.buttonText')} disabled={ isPodExecTerminalSupported.loading || !isPodExecTerminalSupported.value } - title={`${podName} - ${containerName} terminal shell on cluster ${ - cluster.title || cluster.name - }`} + title={t('podExecTerminal.titleTemplate', { + podName, + containerName, + clusterName: cluster.title || cluster.name, + })} > diff --git a/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx b/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx index e460abea8b..ec7b24644a 100644 --- a/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/ErrorList/ErrorList.test.tsx @@ -17,44 +17,57 @@ import { render } from '@testing-library/react'; import { ErrorList } from './ErrorList'; import { Pod } from 'kubernetes-models/v1'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { errorApiRef } from '@backstage/core-plugin-api'; describe('ErrorList', () => { + const translationApi = mockApis.translation(); + const errorApi = { post: jest.fn(), error$: jest.fn() }; + it('error highlight should render', () => { const { getByText } = render( - + , + ], + }, + ]} + /> + , ); expect(getByText('some-pod')).toBeInTheDocument(); expect(getByText('some error message')).toBeInTheDocument(); diff --git a/plugins/kubernetes-react/src/components/Pods/Events/Events.test.tsx b/plugins/kubernetes-react/src/components/Pods/Events/Events.test.tsx index 3f70bb700a..b74929a2e5 100644 --- a/plugins/kubernetes-react/src/components/Pods/Events/Events.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/Events/Events.test.tsx @@ -17,63 +17,25 @@ import { EventsContent } from './Events'; import { render } from '@testing-library/react'; import { Event } from 'kubernetes-models/v1'; import { DateTime } from 'luxon'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { errorApiRef } from '@backstage/core-plugin-api'; describe('EventsContent', () => { const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + const translationApi = mockApis.translation(); + const errorApi = { post: jest.fn(), error$: jest.fn() }; + it('should show info events', () => { const { getByText } = render( - , - ); - expect(getByText('First event 1 hour ago (count: 52)')).toBeInTheDocument(); - expect(getByText('something happened: hello there')).toBeInTheDocument(); - }); - it('should show warning events', () => { - const { getByText } = render( - , - ); - expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument(); - expect(getByText('something happened: uh oh')).toBeInTheDocument(); - }); - - it('should only show warning events when warningEventsOnly set', () => { - const { getByText, queryByText } = render( - + { metadata: { creationTimestamp: oneHourAgo, }, - }, - ] as Event[] - } - />, + } as Event, + ]} + /> + , + ); + expect(getByText('First event 1 hour ago (count: 52)')).toBeInTheDocument(); + expect(getByText('something happened: hello there')).toBeInTheDocument(); + }); + it('should show warning events', () => { + const { getByText } = render( + + + , + ); + expect(getByText('First event 1 hour ago (count: 23)')).toBeInTheDocument(); + expect(getByText('something happened: uh oh')).toBeInTheDocument(); + }); + + it('should only show warning events when warningEventsOnly set', () => { + const { getByText, queryByText } = render( + + + , ); expect(queryByText('First event 1 hour ago (count: 52)')).toBeNull(); expect(queryByText('something happened: hello there')).toBeNull(); diff --git a/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx b/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx index 94fc768e61..dc6abb6fd9 100644 --- a/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx +++ b/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx @@ -31,6 +31,8 @@ import { useEvents } from './useEvents'; import Skeleton from '@material-ui/lab/Skeleton'; import { DismissableBanner } from '@backstage/core-components'; import { Event } from 'kubernetes-models/v1'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../../translation'; /** * Props for Events @@ -59,8 +61,9 @@ export const EventsContent = ({ events, warningEventsOnly, }: EventsContentProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); if (events.length === 0) { - return No events found; + return {t('events.noEventsFound')}; } return ( diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx index 644c17606b..65961dc22d 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx @@ -35,6 +35,8 @@ import { Events } from '../Events'; import { LinkButton } from '@backstage/core-components'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../../translation'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -72,6 +74,7 @@ export const FixDialog: FC = ({ }: FixDialogProps) => { const [isOpen, setOpen] = useState(!!open); const classes = useStyles(); + const { t } = useTranslationRef(kubernetesReactTranslationRef); const openDialog = () => { setOpen(true); @@ -87,21 +90,27 @@ export const FixDialog: FC = ({ return ( - Detected error: + {t('fixDialog.detectedError')} {error.message} - Cause explanation: + + {t('fixDialog.causeExplanation')} + - {error.proposedFix?.rootCauseExplanation ?? 'unknown'} + {error.proposedFix?.rootCauseExplanation ?? t('podsTable.unknown')} - Fix: + {t('fixDialog.fix')} {(error.proposedFix?.actions ?? []).map((fix, i) => { return ( - + {fix} ); @@ -112,14 +121,15 @@ export const FixDialog: FC = ({ {pf && pf.type === 'logs' && ( <> - Crash logs: + {t('fixDialog.crashLogs')} = ({ {pf && pf.type === 'events' && ( <> - Events: + {t('fixDialog.events')} = ({ <> - {pod.metadata?.name} - {error.type} + {t('fixDialog.title', { + podName: pod.metadata?.name ?? '', + errorType: error.type, + })} @@ -178,7 +191,7 @@ export const FixDialog: FC = ({ target="_blank" rel="noopener" > - Open docs + {t('fixDialog.openDocs')} )} diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx index 74ef538bf1..c2b750a7d9 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -30,6 +30,8 @@ import { bytesToMiB, formatMillicores } from '../../../utils/resources'; import { PodExecTerminalDialog } from '../../PodExecTerminal/PodExecTerminalDialog'; import { ResourceUtilization } from '../../ResourceUtilization'; import { PodLogsDialog, PodScope } from '../PodLogs'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../../translation'; const getContainerHealthChecks = ( containerSpec: IContainer, @@ -112,6 +114,7 @@ export const ContainerCard: FC = ({ containerStatus, containerMetrics, }: ContainerCardProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const isPodExecTerminalEnabled = useIsPodExecTerminalEnabled(); // This should never be undefined @@ -183,13 +186,13 @@ export const ContainerCard: FC = ({ - Resource utilization + {t('podDrawer.resourceUtilization')} = ({ /> = ({ /> = ({ /> createStyles({ @@ -77,6 +79,7 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { const classes = useDrawerContentStyles(); const podMetrics = usePodMetrics(podAndErrors.cluster.name, podAndErrors.pod); const isPodDeleteEnabled = useIsPodDeleteEnabled(); + const { t } = useTranslationRef(kubernetesReactTranslationRef); return ( { {podMetrics && ( - Resource utilization + + {t('podDrawer.resourceUtilization')} + { = ({ containerScope, previous, }: PodLogsProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const { value, error, loading } = usePodLogs({ containerScope, previous, @@ -72,8 +75,8 @@ export const PodLogs: FC = ({ (value.text === '' ? ( ) : ( diff --git a/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx b/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx index cb4ccfdf5d..59524e4ed1 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodLogs/PodLogsDialog.tsx @@ -18,6 +18,8 @@ import SubjectIcon from '@material-ui/icons/Subject'; import { KubernetesDialog } from '../../KubernetesDialog'; import { PodLogs } from './PodLogs'; import { ContainerScope } from './types'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../../translation'; /** * Props for PodLogsDialog @@ -34,17 +36,19 @@ export interface PodLogsDialogProps { * @public */ export const PodLogsDialog = ({ containerScope }: PodLogsDialogProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); return ( } - buttonText="Logs" + buttonText={t('podLogs.buttonText')} disabled={false} - title={`${containerScope.podName} - ${ - containerScope.containerName - } logs on cluster ${ - containerScope.cluster.title || containerScope.cluster.name - }`} + title={t('podLogs.titleTemplate', { + podName: containerScope.podName, + containerName: containerScope.containerName, + clusterName: + containerScope.cluster.title || containerScope.cluster.name, + })} > diff --git a/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx b/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx index 8d3c14d86a..ee2fce2822 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodsTable.tsx @@ -30,6 +30,8 @@ import { Pod } from 'kubernetes-models/v1/Pod'; import type { V1Pod } from '@kubernetes/client-node'; import { usePodMetrics } from '../../hooks/usePodMetrics'; import Typography from '@material-ui/core/Typography'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; /** * @@ -63,22 +65,6 @@ export type PodsTablesProps = { children?: ReactNode; }; -const READY: TableColumn[] = [ - { - title: 'containers ready', - align: 'center', - render: containersReady, - width: 'auto', - }, - { - title: 'total restarts', - align: 'center', - render: totalRestarts, - type: 'numeric', - width: 'auto', - }, -]; - const PodDrawerTrigger = ({ pod }: { pod: Pod }) => { const errors = useMatchingErrors({ kind: 'Pod', @@ -98,9 +84,10 @@ const PodDrawerTrigger = ({ pod }: { pod: Pod }) => { const Cpu = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => { const metrics = usePodMetrics(clusterName, pod); + const { t } = useTranslationRef(kubernetesReactTranslationRef); if (!metrics) { - return unknown; + return {t('podsTable.unknown')}; } return <>{podStatusToCpuUtil(metrics)}; @@ -108,9 +95,10 @@ const Cpu = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => { const Memory = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => { const metrics = usePodMetrics(clusterName, pod); + const { t } = useTranslationRef(kubernetesReactTranslationRef); if (!metrics) { - return unknown; + return {t('podsTable.unknown')}; } return <>{podStatusToMemoryUtil(metrics)}; @@ -123,26 +111,44 @@ const Memory = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => { */ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { const cluster = useContext(ClusterContext); + const { t } = useTranslationRef(kubernetesReactTranslationRef); + + const READY: TableColumn[] = [ + { + title: t('podsTable.columns.containersReady'), + align: 'center', + render: containersReady, + width: 'auto', + }, + { + title: t('podsTable.columns.totalRestarts'), + align: 'center', + render: totalRestarts, + type: 'numeric', + width: 'auto', + }, + ]; + const defaultColumns: TableColumn[] = [ { - title: 'ID', + title: t('podsTable.columns.id'), field: 'metadata.uid', hidden: true, }, { - title: 'name', + title: t('podsTable.columns.name'), highlight: true, render: (pod: Pod) => { return ; }, }, { - title: 'phase', - render: (pod: Pod) => pod.status?.phase ?? 'unknown', + title: t('podsTable.columns.phase'), + render: (pod: Pod) => pod.status?.phase ?? t('podsTable.unknown'), width: 'auto', }, { - title: 'status', + title: t('podsTable.columns.status'), render: containerStatuses, }, ]; @@ -154,14 +160,14 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { if (extraColumns.includes(RESOURCE_COLUMNS)) { const resourceColumns: TableColumn[] = [ { - title: 'CPU usage %', + title: t('podsTable.columns.cpuUsage'), render: (pod: Pod) => { return ; }, width: 'auto', }, { - title: 'Memory usage %', + title: t('podsTable.columns.memoryUsage'), render: (pod: Pod) => { return ; }, diff --git a/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.test.tsx b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.test.tsx new file mode 100644 index 0000000000..1bdafa293b --- /dev/null +++ b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { SecretsAccordions } from './SecretsAccordions'; +import * as oneSecretsFixture from '../../__fixtures__/1-secrets.json'; +import * as twoSecretsFixture from '../../__fixtures__/2-secrets.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('SecretsAccordions', () => { + it('should render 1 secret', async () => { + const wrapper = kubernetesProviders(oneSecretsFixture, new Set()); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-secret')).toBeInTheDocument(); + expect(screen.getByText('Secret')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + }); + + it('should render 2 secrets', async () => { + const wrapper = kubernetesProviders(twoSecretsFixture, new Set()); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-secret')).toBeInTheDocument(); + expect(screen.getByText('redis-secret')).toBeInTheDocument(); + expect(screen.getAllByText('Secret')).toHaveLength(2); + expect(screen.getAllByText('namespace: default')).toHaveLength(2); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 3')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.tsx b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.tsx new file mode 100644 index 0000000000..4271a2af8b --- /dev/null +++ b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsAccordions.tsx @@ -0,0 +1,108 @@ +/* + * 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 { useContext } from 'react'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import type { V1Secret } from '@kubernetes/client-node'; +import { SecretsDrawer } from './SecretsDrawer.tsx'; +import { GroupedResponsesContext } from '../../hooks'; +import { StructuredMetadataTable } from '@backstage/core-components'; + +type SecretSummaryProps = { + secret: V1Secret; +}; + +const SecretSummary = ({ secret }: SecretSummaryProps) => { + return ( + + + + + + + + Data Count: {secret.data ? Object.keys(secret.data).length : 0} + + + + ); +}; + +type SecretsCardProps = { + secret: V1Secret; +}; + +const SecretCard = ({ secret }: SecretsCardProps) => { + const metadata: any = {}; + + metadata.data = secret.data; + + return ( + + ); +}; + +export type SecretsAccordionsProps = {}; + +type SecretsAccordionProps = { + secret: V1Secret; +}; + +const SecretsAccordion = ({ secret }: SecretsAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const SecretsAccordions = ({}: SecretsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + return ( + + {groupedResponses.secrets.map((secret, i) => ( + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.test.tsx b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.test.tsx new file mode 100644 index 0000000000..6dd1230ee4 --- /dev/null +++ b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as oneSecretsFixture from '../../__fixtures__/1-secrets.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { SecretsDrawer } from './SecretsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('SecretsDrawer', () => { + it('should render secret drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + ); + + expect(getAllByText('app-secret')).toHaveLength(3); + expect(getAllByText('Secret')).toHaveLength(3); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.tsx b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.tsx new file mode 100644 index 0000000000..72df6bdae9 --- /dev/null +++ b/plugins/kubernetes-react/src/components/SecretsAccordions/SecretsDrawer.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import Chip from '@material-ui/core/Chip'; +import type { V1Secret } from '@kubernetes/client-node'; + +export const SecretsDrawer = ({ + secret, + expanded, +}: { + secret: V1Secret; + expanded?: boolean; +}) => { + const namespace = secret.metadata?.namespace; + return ( + { + return secretObject || {}; + }} + > + + + + {secret.metadata?.name ?? 'unknown object'} + + + + + Secret + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/SecretsAccordions/index.ts b/plugins/kubernetes-react/src/components/SecretsAccordions/index.ts new file mode 100644 index 0000000000..461537fe72 --- /dev/null +++ b/plugins/kubernetes-react/src/components/SecretsAccordions/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 * from './SecretsAccordions.tsx'; diff --git a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.tsx index a9f7b7b5ad..d9d08d04cb 100644 --- a/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.tsx +++ b/plugins/kubernetes-react/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -19,6 +19,8 @@ import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import Chip from '@material-ui/core/Chip'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; export const ServiceDrawer = ({ service, @@ -27,6 +29,7 @@ export const ServiceDrawer = ({ service: V1Service; expanded?: boolean; }) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const namespace = service.metadata?.namespace; return ( {namespace && ( - + )} diff --git a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx index 7b7a2b7bb2..ebb446b40c 100644 --- a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx +++ b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetDrawer.tsx @@ -21,6 +21,8 @@ import { renderCondition } from '../../utils/pod'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import Chip from '@material-ui/core/Chip'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; export const StatefulSetDrawer = ({ statefulset, @@ -29,6 +31,7 @@ export const StatefulSetDrawer = ({ statefulset: V1StatefulSet; expanded?: boolean; }) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const namespace = statefulset.metadata?.namespace; return ( {namespace && ( - + )} diff --git a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx index e3ae9776bd..e1a33474f2 100644 --- a/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx +++ b/plugins/kubernetes-react/src/components/StatefulSetsAccordions/StatefulSetsAccordions.tsx @@ -36,6 +36,8 @@ import { } from '../../hooks'; import { StatusError, StatusOK } from '@backstage/core-components'; import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../translation'; type StatefulSetsAccordionsProps = { children?: ReactNode; @@ -62,6 +64,7 @@ const StatefulSetSummary = ({ numberOfPodsWithErrors, hpa, }: StatefulSetSummaryProps) => { + const { t } = useTranslationRef(kubernetesReactTranslationRef); const specCpuUtil = hpa?.spec?.metrics?.find( metric => metric.type === 'Resource' && metric.resource?.name === 'cpu', )?.resource?.target.averageUtilization; @@ -94,18 +97,18 @@ const StatefulSetSummary = ({ > - min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '} - {hpa.spec?.maxReplicas ?? '?'} + {t('hpa.minReplicas')} {hpa.spec?.minReplicas ?? '?'} /{' '} + {t('hpa.maxReplicas')} {hpa.spec?.maxReplicas ?? '?'} - current CPU usage: {cpuUtil ?? '?'}% + {t('hpa.currentCpuUsage')} {cpuUtil ?? '?'}% - target CPU usage: {specCpuUtil ?? '?'}% + {t('hpa.targetCpuUsage')} {specCpuUtil ?? '?'}% @@ -122,16 +125,15 @@ const StatefulSetSummary = ({ spacing={0} > - {numberOfCurrentPods} pods + {t('pods.pods', { count: numberOfCurrentPods })} {numberOfPodsWithErrors > 0 ? ( - {numberOfPodsWithErrors} pod - {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + {t('cluster.podsWithErrors', { count: numberOfPodsWithErrors })} ) : ( - No pods with errors + {t('cluster.noPodsWithErrors')} )} diff --git a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts index 1dbf16c57e..750ee58846 100644 --- a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts @@ -28,6 +28,7 @@ export const GroupedResponsesContext = createContext({ daemonSets: [], services: [], configMaps: [], + secrets: [], horizontalPodAutoscalers: [], ingresses: [], jobs: [], diff --git a/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.test.ts b/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.test.ts index c4df347c6f..26ba8f5e91 100644 --- a/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.test.ts +++ b/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.test.ts @@ -55,7 +55,7 @@ describe('useIsPodExecTerminalSupported', () => { }, { condition: 'AuthProvider is google', - returnValue: false, + returnValue: true, testClusters: [{ authProvider: 'google' }], }, { diff --git a/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.ts b/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.ts index 5795b900ae..06c1a8808f 100644 --- a/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.ts +++ b/plugins/kubernetes-react/src/hooks/useIsPodExecTerminalSupported.ts @@ -34,8 +34,8 @@ export const useIsPodExecTerminalSupported = (): AsyncState => { } const { authProvider } = clusters[0]; - const isClientAuthProvider = ['aks', 'google', 'oidc'].some( - authProviderName => authProvider.includes(authProviderName), + const isClientAuthProvider = ['aks', 'oidc'].some(authProviderName => + authProvider.includes(authProviderName), ); return !isClientAuthProvider; diff --git a/plugins/kubernetes-react/src/translation.ts b/plugins/kubernetes-react/src/translation.ts index 5b8a26b0d5..0420cc04bf 100644 --- a/plugins/kubernetes-react/src/translation.ts +++ b/plugins/kubernetes-react/src/translation.ts @@ -20,10 +20,130 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; export const kubernetesReactTranslationRef = createTranslationRef({ id: 'kubernetes-react', messages: { + cluster: { + label: 'Cluster', + pods: 'pods', + pods_one: '{{count}} pod', + pods_other: '{{count}} pods', + podsWithErrors: 'pods with errors', + podsWithErrors_one: '{{count}} pod with errors', + podsWithErrors_other: '{{count}} pods with errors', + noPodsWithErrors: 'No pods with errors', + }, + podsTable: { + columns: { + id: 'ID', + name: 'name', + phase: 'phase', + status: 'status', + containersReady: 'containers ready', + totalRestarts: 'total restarts', + cpuUsage: 'CPU usage %', + memoryUsage: 'Memory usage %', + }, + unknown: 'unknown', + status: { + running: 'Running', + ok: 'OK', + }, + }, + errorPanel: { + title: 'There was a problem retrieving Kubernetes objects', + message: + 'There was a problem retrieving some Kubernetes resources for the entity: {{entityName}}. This could mean that the Error Reporting card is not completely accurate.', + errorsLabel: 'Errors', + clusterLabel: 'Cluster', + clusterLabelValue: 'Cluster: {{cluster}}', + fetchError: + 'Error communicating with Kubernetes: {{errorType}}, message: {{message}}', + resourceError: + "Error fetching Kubernetes resource: '{{resourcePath}}', error: {{errorType}}, status code: {{statusCode}}", + }, + fixDialog: { + helpButton: 'Help', + title: '{{podName}} - {{errorType}}', + detectedError: 'Detected error:', + causeExplanation: 'Cause explanation:', + fix: 'Fix:', + crashLogs: 'Crash logs:', + events: 'Events:', + openDocs: 'Open docs', + ariaLabels: { + fixIssue: 'fix issue', + close: 'close', + }, + }, podDrawer: { buttons: { delete: 'Delete Pod', }, + cpuRequests: 'CPU requests', + cpuLimits: 'CPU limits', + memoryRequests: 'Memory requests', + memoryLimits: 'Memory limits', + resourceUtilization: 'Resource utilization', + }, + hpa: { + minReplicas: 'min replicas', + maxReplicas: 'max replicas', + replicasSummary: 'min replicas {{min}} / max replicas {{max}}', + currentCpuUsage: 'current CPU usage:', + currentCpuUsageLabel: 'current CPU usage: {{value}}%', + targetCpuUsage: 'target CPU usage:', + targetCpuUsageLabel: 'target CPU usage: {{value}}%', + }, + pods: { + pods_one: '{{count}} pod', + pods_other: '{{count}} pods', + }, + errorReporting: { + title: 'Error Reporting', + columns: { + cluster: 'cluster', + namespace: 'namespace', + kind: 'kind', + name: 'name', + messages: 'messages', + }, + }, + podLogs: { + title: 'No logs emitted', + description: 'No logs were emitted by the container', + buttonText: 'Logs', + buttonAriaLabel: 'get logs', + titleTemplate: + '{{podName}} - {{containerName}} logs on cluster {{clusterName}}', + }, + podExecTerminal: { + buttonText: 'Terminal', + buttonAriaLabel: 'open terminal', + titleTemplate: + '{{podName}} - {{containerName}} terminal shell on cluster {{clusterName}}', + }, + events: { + noEventsFound: 'No events found', + eventTooltip: '{{eventType}} event', + firstEvent: 'First event {{timeAgo}} (count: {{count}})', + }, + kubernetesDrawer: { + closeDrawer: 'Close the drawer', + yaml: 'YAML', + managedFields: 'Managed Fields', + unknownName: 'unknown name', + }, + linkErrorPanel: { + title: + 'There was a problem formatting the link to the Kubernetes dashboard', + message: + "Could not format the link to the dashboard of your cluster named '{{clusterName}}'. Its dashboardApp property has been set to '{{dashboardApp}}.'", + errorsLabel: 'Errors:', + }, + namespace: { + label: 'namespace:', + labelWithValue: 'namespace: {{namespace}}', + }, + kubernetesDialog: { + closeAriaLabel: 'close', }, }, }); diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index f8dcfb151f..2d2a53852b 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-kubernetes +## 0.12.15 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + - @backstage/plugin-catalog-react@1.21.5 + - @backstage/plugin-kubernetes-react@0.5.15 + +## 0.12.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + - @backstage/plugin-catalog-react@1.21.5-next.1 + - @backstage/plugin-kubernetes-react@0.5.15-next.0 + +## 0.12.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.5-next.0 + - @backstage/frontend-plugin-api@0.13.2 + +## 0.12.14 + +### Patch Changes + +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/plugin-kubernetes-react@0.5.14 + - @backstage/core-components@0.18.4 + - @backstage/plugin-catalog-react@1.21.4 + - @backstage/plugin-kubernetes-common@0.9.9 + - @backstage/core-plugin-api@1.12.1 + - @backstage/plugin-permission-react@0.4.39 + +## 0.12.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.5.14-next.1 + - @backstage/core-components@0.18.4-next.1 + - @backstage/plugin-catalog-react@1.21.4-next.1 + - @backstage/plugin-kubernetes-common@0.9.9-next.0 + +## 0.12.14-next.0 + +### Patch Changes + +- f15d5f1: add missing i18n support for kubernetes and kubernetes-react +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/plugin-kubernetes-react@0.5.14-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/plugin-catalog-react@1.21.4-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-permission-react@0.4.39-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-kubernetes-common@0.9.8 + +## 0.12.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-catalog-react@1.21.3 + - @backstage/core-components@0.18.3 + - @backstage/plugin-kubernetes-common@0.9.8 + - @backstage/plugin-kubernetes-react@0.5.13 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-permission-react@0.4.38 + - @backstage/catalog-model@1.7.6 + ## 0.12.13-next.0 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index f191ce2a0b..9781e5be7c 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.13-next.0", + "version": "0.12.15", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", @@ -59,7 +59,6 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", - "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 9bcbeb4ede..9b847b862a 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -11,11 +11,12 @@ import { Entity } from '@backstage/catalog-model'; import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; -import { RouteRef } from '@backstage/frontend-plugin-api'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) const _default: OverridableFrontendPlugin< @@ -24,7 +25,7 @@ const _default: OverridableFrontendPlugin< }, {}, { - 'api:kubernetes': ExtensionDefinition<{ + 'api:kubernetes': OverridableExtensionDefinition<{ kind: 'api'; name: undefined; config: {}; @@ -39,7 +40,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'api:kubernetes/auth-providers': ExtensionDefinition<{ + 'api:kubernetes/auth-providers': OverridableExtensionDefinition<{ kind: 'api'; name: 'auth-providers'; config: {}; @@ -54,7 +55,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'api:kubernetes/cluster-link-formatter': ExtensionDefinition<{ + 'api:kubernetes/cluster-link-formatter': OverridableExtensionDefinition<{ kind: 'api'; name: 'cluster-link-formatter'; config: {}; @@ -69,7 +70,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'api:kubernetes/proxy': ExtensionDefinition<{ + 'api:kubernetes/proxy': OverridableExtensionDefinition<{ kind: 'api'; name: 'proxy'; config: {}; @@ -84,7 +85,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'entity-content:kubernetes/kubernetes': ExtensionDefinition<{ + 'entity-content:kubernetes/kubernetes': OverridableExtensionDefinition<{ kind: 'entity-content'; name: 'kubernetes'; config: { @@ -103,7 +104,7 @@ const _default: OverridableFrontendPlugin< | ExtensionDataRef | ExtensionDataRef | ExtensionDataRef< - RouteRef, + RouteRef_2, 'core.routing.ref', { optional: true; @@ -140,11 +141,11 @@ const _default: OverridableFrontendPlugin< defaultGroup?: [Error: `Use the 'group' param instead`]; group?: keyof defaultEntityContentGroups | (string & {}); loader: () => Promise; - routeRef?: RouteRef; + routeRef?: RouteRef_2; filter?: string | EntityPredicate | ((entity: Entity) => boolean); }; }>; - 'page:kubernetes': ExtensionDefinition<{ + 'page:kubernetes': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; config: { @@ -157,7 +158,7 @@ const _default: OverridableFrontendPlugin< | ExtensionDataRef | ExtensionDataRef | ExtensionDataRef< - RouteRef, + RouteRef_2, 'core.routing.ref', { optional: true; @@ -168,7 +169,7 @@ const _default: OverridableFrontendPlugin< defaultPath?: [Error: `Use the 'path' param instead`]; path: string; loader: () => Promise; - routeRef?: RouteRef; + routeRef?: RouteRef_2; }; }>; } @@ -179,6 +180,10 @@ export default _default; export const kubernetesTranslationRef: TranslationRef< 'kubernetes', { + readonly 'entityContent.title': 'Kubernetes'; + readonly 'kubernetesContentPage.title': 'Your Clusters'; + readonly 'kubernetesContentPage.emptyState.title': 'No Kubernetes resources'; + readonly 'kubernetesContentPage.emptyState.description': 'No resources on any known clusters for {{entityName}}'; readonly 'kubernetesContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission."; readonly 'kubernetesContentPage.permissionAlert.title': 'Permission required'; } diff --git a/plugins/kubernetes/src/KubernetesContent.tsx b/plugins/kubernetes/src/KubernetesContent.tsx index a8d62a5a3d..4f7c7e73e2 100644 --- a/plugins/kubernetes/src/KubernetesContent.tsx +++ b/plugins/kubernetes/src/KubernetesContent.tsx @@ -31,6 +31,8 @@ import { } from '@backstage/plugin-kubernetes-common'; import { EmptyState, Progress } from '@backstage/core-components'; import { RequireKubernetesPermissions } from './RequireKubernetesPermissions'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesTranslationRef } from './alpha/translation'; type KubernetesContentProps = { entity: Entity; @@ -46,6 +48,7 @@ export const KubernetesContent = ({ entity, refreshIntervalMs, ); + const { t } = useTranslationRef(kubernetesTranslationRef); const clusters = kubernetesObjects?.items.map(item => item.cluster) ?? []; @@ -97,7 +100,9 @@ export const KubernetesContent = ({ /> - Your Clusters + + {t('kubernetesContentPage.title')} + {kubernetesObjects?.items.length <= 0 && ( @@ -111,8 +116,11 @@ export const KubernetesContent = ({ diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx index d75893c6e4..0a85cadaed 100644 --- a/plugins/kubernetes/src/alpha/entityContents.tsx +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { compatWrapper } from '@backstage/core-compat-api'; import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { isKubernetesAvailable } from '../Router'; @@ -26,8 +25,6 @@ export const entityKubernetesContent = EntityContentBlueprint.make({ group: 'deployment', filter: isKubernetesAvailable, loader: () => - import('./KubernetesContentPage').then(m => - compatWrapper(), - ), + import('./KubernetesContentPage').then(m => ), }, }); diff --git a/plugins/kubernetes/src/alpha/pages.tsx b/plugins/kubernetes/src/alpha/pages.tsx index 6a2d98ddcc..3069d25cd6 100644 --- a/plugins/kubernetes/src/alpha/pages.tsx +++ b/plugins/kubernetes/src/alpha/pages.tsx @@ -14,20 +14,13 @@ * limitations under the License. */ -import { PageBlueprint } from '@backstage/frontend-plugin-api'; // Add this line to import React -import { - compatWrapper, - convertLegacyRouteRef, -} from '@backstage/core-compat-api'; +import { PageBlueprint } from '@backstage/frontend-plugin-api'; import { rootCatalogKubernetesRouteRef } from '../plugin'; export const kubernetesPage = PageBlueprint.make({ params: { path: '/kubernetes', - // you can reuse the existing routeRef - // by wrapping into the convertLegacyRouteRef. - routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), - // these inputs usually match the props required by the component. - loader: () => import('../Router').then(m => compatWrapper()), + routeRef: rootCatalogKubernetesRouteRef, + loader: () => import('../Router').then(m => ), }, }); diff --git a/plugins/kubernetes/src/alpha/plugin.tsx b/plugins/kubernetes/src/alpha/plugin.tsx index db7b2439bf..fdca467886 100644 --- a/plugins/kubernetes/src/alpha/plugin.tsx +++ b/plugins/kubernetes/src/alpha/plugin.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { convertLegacyRouteRefs } from '@backstage/core-compat-api'; import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import { kubernetesPage } from './pages'; import { entityKubernetesContent } from './entityContents'; @@ -37,5 +36,5 @@ export default createFrontendPlugin({ kubernetesAuthProvidersApi, kubernetesClusterLinkFormatterApi, ], - routes: convertLegacyRouteRefs({ kubernetes: rootCatalogKubernetesRouteRef }), + routes: { kubernetes: rootCatalogKubernetesRouteRef }, }); diff --git a/plugins/kubernetes/src/alpha/translation.ts b/plugins/kubernetes/src/alpha/translation.ts index 7f9704af84..238bd3b084 100644 --- a/plugins/kubernetes/src/alpha/translation.ts +++ b/plugins/kubernetes/src/alpha/translation.ts @@ -25,6 +25,14 @@ export const kubernetesTranslationRef = createTranslationRef({ message: "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission.", }, + title: 'Your Clusters', + emptyState: { + title: 'No Kubernetes resources', + description: 'No resources on any known clusters for {{entityName}}', + }, + }, + entityContent: { + title: 'Kubernetes', }, }, }); diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index 71c045db64..f0d0ff7424 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,93 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.7 + +### Patch Changes + +- 4d82a35: build(deps): bump `@modelcontextprotocol/sdk` from 1.24.3 to 1.25.2 +- Updated dependencies + - @backstage/backend-defaults@0.15.0 + - @backstage/backend-plugin-api@1.6.1 + +## 0.1.7-next.1 + +### Patch Changes + +- 4d82a35: build(deps): bump `@modelcontextprotocol/sdk` from 1.24.3 to 1.25.2 +- Updated dependencies + - @backstage/backend-defaults@0.15.0-next.2 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.14.1-next.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1 + +## 0.1.6 + +### Patch Changes + +- e83e038: Added `@cfworker/json-schema` as a dependency to this package part of the `@modelcontextprotocol/sdk` bump as it's required in the types +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- 79ef471: Clarify error handling in readme and update handleError.ts to include all backstage/errors +- Updated dependencies + - @backstage/backend-defaults@0.14.0 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + +## 0.1.6-next.1 + +### Patch Changes + +- e83e038: Added `@cfworker/json-schema` as a dependency to this package part of the `@modelcontextprotocol/sdk` bump as it's required in the types +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- 79ef471: Clarify error handling in readme and update handleError.ts to include all backstage/errors +- Updated dependencies + - @backstage/backend-defaults@0.14.0-next.0 + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.5 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/backend-defaults@0.13.1 + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-client@1.12.1 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-defaults@0.13.1-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index d81d0207c5..39ec169dec 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -71,6 +71,31 @@ export const myPlugin = createBackendPlugin({ }); ``` +### Error Handling + +When errors are thrown from MCP actions, the backend will handle and surface error message for any error from `@backstage/errors`. Unknown errors will be handled by `@modelcontextprotocol/sdk`'s default error handling, which may result in a generic `500 Server Error` being returned. As a result, we recommend using errors from `@backstage/errors` when applicable. + +See https://backstage.io/docs/reference/errors/ for a full list of supported errors. + +When writing MCP tools, use the appropriate error from `@backstage/errors` when applicable: + +```ts +action: async ({ input }) => { + // ... get current user and some resource + + if (!resource) { + throw new NotFoundError(`Resource ${input.id} not found`); + } + + // Check if the user has permissions to access/use the resource + if (!hasPermission(user, resource)) { + throw new NotAllowedError( + `user does not have sufficient permissions for ${resource}`, + ); + } +}; +``` + ### Authentication Configuration By default, the Backstage backend requires authentication for all requests. diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 9acff7aafc..a6ecc6c408 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.5-next.0", + "version": "0.1.7", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", @@ -40,8 +40,9 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", - "@modelcontextprotocol/sdk": "^1.12.3", - "express": "^4.17.1", + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/sdk": "^1.25.2", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "zod": "^3.22.4" }, diff --git a/plugins/mcp-actions-backend/src/plugin.test.ts b/plugins/mcp-actions-backend/src/plugin.test.ts index ace988cfc1..47dd89c9fc 100644 --- a/plugins/mcp-actions-backend/src/plugin.test.ts +++ b/plugins/mcp-actions-backend/src/plugin.test.ts @@ -20,7 +20,7 @@ import { createBackendPlugin } from '@backstage/backend-plugin-api'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; -import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types'; +import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; describe('Mcp Backend', () => { const mockPluginWithActions = createBackendPlugin({ diff --git a/plugins/mcp-actions-backend/src/services/handleErrors.ts b/plugins/mcp-actions-backend/src/services/handleErrors.ts index 67d92664eb..5266c80e75 100644 --- a/plugins/mcp-actions-backend/src/services/handleErrors.ts +++ b/plugins/mcp-actions-backend/src/services/handleErrors.ts @@ -32,6 +32,7 @@ const knownErrors = new Set([ 'NotModifiedError', 'NotImplementedError', 'ResponseError', + 'ServiceUnavailableError', ]); // Extracts the cause error, if the provided error is `ResponseError` or diff --git a/plugins/mui-to-bui/CHANGELOG.md b/plugins/mui-to-bui/CHANGELOG.md index 4cb1bd9045..63583963ed 100644 --- a/plugins/mui-to-bui/CHANGELOG.md +++ b/plugins/mui-to-bui/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-mui-to-bui +## 0.2.3 + +### Patch Changes + +- f157f43: Fix installation command +- e4a1180: Updated tokens from `--bui-bg` to `--bui-bg-surface-0` +- Updated dependencies + - @backstage/ui@0.11.0 + - @backstage/frontend-plugin-api@0.13.3 + +## 0.2.3-next.0 + +### Patch Changes + +- e4a1180: Updated tokens from `--bui-bg` to `--bui-bg-surface-0` +- Updated dependencies + - @backstage/ui@0.11.0-next.0 + - @backstage/core-plugin-api@1.12.1 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/theme@0.7.1 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0 + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + +## 0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.10.0-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.9.1-next.0 + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + +## 0.2.1 + +### Patch Changes + +- 5c614ff: Updated BUI checkbox preview example to align with new component API. +- Updated dependencies + - @backstage/ui@0.9.0 + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/core-plugin-api@1.12.0 + +## 0.2.1-next.1 + +### Patch Changes + +- 5c614ff: Updated BUI checkbox preview example to align with new component API. +- Updated dependencies + - @backstage/ui@0.9.0-next.1 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/mui-to-bui/README.md b/plugins/mui-to-bui/README.md index 4e11db20b6..d964b4827f 100644 --- a/plugins/mui-to-bui/README.md +++ b/plugins/mui-to-bui/README.md @@ -11,7 +11,7 @@ The Backstage UI Themer helps you convert an existing MUI v5 theme into Backstag Run this from your Backstage repo root: ```bash -yarn add --cwd packages/app @backstage/plugin-mui-to-bui +yarn --cwd packages/app add @backstage/plugin-mui-to-bui ``` ### 2) Wire it up depending on your frontend system diff --git a/plugins/mui-to-bui/package.json b/plugins/mui-to-bui/package.json index 8edd1fb287..10e149f388 100644 --- a/plugins/mui-to-bui/package.json +++ b/plugins/mui-to-bui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mui-to-bui", - "version": "0.2.1-next.0", + "version": "0.2.3", "backstage": { "role": "frontend-plugin", "pluginId": "mui-to-bui", @@ -35,7 +35,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/core-compat-api": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/mui-to-bui/report.api.md b/plugins/mui-to-bui/report.api.md index 593130ea86..db1d49f828 100644 --- a/plugins/mui-to-bui/report.api.md +++ b/plugins/mui-to-bui/report.api.md @@ -6,12 +6,12 @@ import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSX as JSX_3 } from 'react/jsx-runtime'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; -import { RouteRef } from '@backstage/frontend-plugin-api'; -import { RouteRef as RouteRef_2 } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; // @public (undocumented) export const BuiThemerPage: () => JSX_3.Element; @@ -19,7 +19,7 @@ export const BuiThemerPage: () => JSX_3.Element; // @public (undocumented) export const buiThemerPlugin: BackstagePlugin< { - root: RouteRef_2; + root: RouteRef; }, {} >; @@ -31,7 +31,7 @@ const _default: OverridableFrontendPlugin< }, {}, { - 'page:mui-to-bui': ExtensionDefinition<{ + 'page:mui-to-bui': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; config: { @@ -44,7 +44,7 @@ const _default: OverridableFrontendPlugin< | ExtensionDataRef | ExtensionDataRef | ExtensionDataRef< - RouteRef, + RouteRef_2, 'core.routing.ref', { optional: true; @@ -55,7 +55,7 @@ const _default: OverridableFrontendPlugin< defaultPath?: [Error: `Use the 'path' param instead`]; path: string; loader: () => Promise; - routeRef?: RouteRef; + routeRef?: RouteRef_2; }; }>; } diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts index 6ba3f7d2d9..48bb318a71 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -59,7 +59,7 @@ describe('convertMuiToBuiTheme', () => { // Border radius tokens are only generated when radius is 0 expect(result.css).not.toContain('--bui-radius-3:'); // Background default maps to surface-2 - expect(result.css).toContain('--bui-bg: #f5f5f5;'); + expect(result.css).toContain('--bui-bg-surface-0: #f5f5f5;'); expect(result.css).toContain('--bui-bg-surface-2: #f5f5f5;'); expect(result.css).toContain('--bui-bg-surface-1: #ffffff;'); expect(result.css).toContain('--bui-fg-primary: #000000;'); @@ -98,7 +98,7 @@ describe('convertMuiToBuiTheme', () => { expect(result.css).toContain("[data-theme-mode='dark'] {"); // Background default maps to surface-2 in dark mode as well - expect(result.css).toContain('--bui-bg: #121212;'); + expect(result.css).toContain('--bui-bg-surface-0: #121212;'); expect(result.css).toContain('--bui-bg-surface-2: #121212;'); expect(result.css).toContain('--bui-bg-surface-1: #1e1e1e;'); expect(result.css).toContain('--bui-fg-primary: #ffffff;'); diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index 1b8528922b..029d70cec7 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -110,9 +110,10 @@ function generateBuiVariables(theme: Mui5Theme): Record { // Generate surface colors Object.entries({ - '': palette.background.default, + 'surface-0': palette.background.default, 'surface-1': palette.background.paper, 'surface-2': palette.background.default, + 'surface-3': palette.background.default, solid: palette.primary.main, 'solid-hover': blend(palette.primary.main, palette.primary.dark, 0.5), 'solid-pressed': palette.primary.dark, @@ -125,7 +126,7 @@ function generateBuiVariables(theme: Mui5Theme): Record { warning: palette.warning.light, success: palette.success.light, }).forEach(([key, value]) => { - styleObject[`--bui-bg${key ? `-${key}` : ''}`] = value; + styleObject[`--bui-bg-${key}`] = value; }); // Border colors diff --git a/plugins/mui-to-bui/src/plugin.tsx b/plugins/mui-to-bui/src/plugin.tsx index aca19c9978..ccebe6e379 100644 --- a/plugins/mui-to-bui/src/plugin.tsx +++ b/plugins/mui-to-bui/src/plugin.tsx @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - convertLegacyRouteRef, - convertLegacyRouteRefs, -} from '@backstage/core-compat-api'; import { createPlugin, createRoutableExtension, @@ -57,11 +53,11 @@ export default createFrontendPlugin({ path: '/mui-to-bui', loader: () => import('./components/BuiThemerPage').then(m => ), - routeRef: convertLegacyRouteRef(rootRouteRef), + routeRef: rootRouteRef, }, }), ], - routes: convertLegacyRouteRefs({ + routes: { root: rootRouteRef, - }), + }, }); diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 9bb47c5da4..21cb61c4c4 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,98 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.17 + +### Patch Changes + +- a5d5b3a: SES config for the notification email processor now supports utilizing an ARN for the SES identity when sending an email after the SES SDK V2 update. + + The `sesConfig.fromArn` will set the `fromEmailAddressIdentityArn` option for the SES `SendEmailCommand`. The `sesConfig.sourceArn` field is removed since no equivalent option is available in the send email command options. Setting `sesConfig.sourceArn` will have no effect and log a warning. Example changes: + + ```diff + notifications: + processors: + email: + transportConfig: + transport: "ses" + region: "us-west-2" + sender: "sender@mycompany.com" + replyTo: "no-reply@mycompany.com" + sesConfig: + - sourceArn: "arn:aws:ses:us-west-2:123456789012:identity/example.com" + fromArn: "arn:aws:ses:us-west-2:123456789012:identity/example.com" + ``` + +- b267aea: Updated dependency `@types/nodemailer` to `^7.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## 0.3.17-next.1 + +### Patch Changes + +- b267aea: Updated dependency `@types/nodemailer` to `^7.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## 0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.3.16 + +### Patch Changes + +- 22a5362: Updated `AWS SES` client to version 2 to support `nodemailer` version 7. +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/integration-aws-node@0.1.19 + - @backstage/catalog-client@1.12.1 + +## 0.3.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## 0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + ## 0.3.15-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index c1ead48876..248bb2c501 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -81,7 +81,6 @@ notifications: receiver: 'users' # Optional SES config # sesConfig: - # sourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' # fromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' # configurationSetName: 'custom-config' # How many emails to send concurrently, defaults to 2 diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 7116826c5d..55a751f8fa 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -136,10 +136,6 @@ export interface Config { * Optional SES config for mail options. Allows for delegated sender */ sesConfig?: { - /** - * ARN of the identity to use as the source of the email - */ - sourceArn?: string; /** * ARN of the identity to use for the "From"/sender address of the email */ diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index e4a7ee7afd..b1f2831758 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.15-next.0", + "version": "0.3.17", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", @@ -53,7 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@types/nodemailer": "^6.4.14" + "@types/nodemailer": "^7.0.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts index ebe1b90f35..ad0817521f 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts @@ -497,9 +497,13 @@ describe('NotificationsEmailProcessor', () => { text: 'https://example.org/notifications', to: 'mock@backstage.io', ses: { - SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', - FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + FromEmailAddressIdentityArn: + 'arn:aws:ses:us-west-2:123456789012:identity/example.com', }, }); + + expect(logger.warn).toHaveBeenCalledWith( + 'sourceArn is not supported in SESv2 and will be ignored', + ); }); }); diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index eadfe7747e..e0e27232c2 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -45,6 +45,7 @@ import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node'; import { NotificationTemplateRenderer } from '../extensions'; import Mail from 'nodemailer/lib/mailer'; import pThrottle from 'p-throttle'; +import { SendEmailCommandInput } from '@aws-sdk/client-sesv2'; export class NotificationsEmailProcessor implements NotificationProcessor { private transporter: any; @@ -53,6 +54,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly sender: string; private readonly replyTo?: string; private readonly sesConfig?: Config; + private readonly sesOptions?: Partial; private readonly cacheTtl: number; private readonly concurrencyLimit: number; private readonly throttleInterval: number; @@ -91,6 +93,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { this.sender = emailProcessorConfig.getString('sender'); this.replyTo = emailProcessorConfig.getOptionalString('replyTo'); this.sesConfig = emailProcessorConfig.getOptionalConfig('sesConfig'); + this.sesOptions = this.getSesOptions(); this.concurrencyLimit = emailProcessorConfig.getOptionalNumber('concurrencyLimit') ?? 2; this.throttleInterval = emailProcessorConfig.has('throttleInterval') @@ -307,20 +310,23 @@ export class NotificationsEmailProcessor implements NotificationProcessor { return contentParts.join('\n\n'); } - private async getSesOptions() { + private getSesOptions(): Partial | undefined { if (!this.sesConfig) { return undefined; } - const ses: Record = {}; - const sourceArn = this.sesConfig.getOptionalString('sourceArn'); + const ses: Partial = {}; const fromArn = this.sesConfig.getOptionalString('fromArn'); + const sourceArn = this.sesConfig.getOptionalString('sourceArn'); const configurationSetName = this.sesConfig.getOptionalString( 'configurationSetName', ); - if (sourceArn) ses.SourceArn = sourceArn; - if (fromArn) ses.FromArn = fromArn; + if (fromArn) ses.FromEmailAddressIdentityArn = fromArn; if (configurationSetName) ses.ConfigurationSetName = configurationSetName; + if (sourceArn) + this.logger.warn( + 'sourceArn is not supported in SESv2 and will be ignored', + ); return Object.keys(ses).length > 0 ? ses : undefined; } @@ -332,7 +338,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { html: this.getHtmlContent(notification), text: this.getTextContent(notification), replyTo: this.replyTo, - ses: await this.getSesOptions(), + ses: this.sesOptions, }; await this.sendMails(mailOptions, emails); @@ -350,7 +356,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { html: await this.templateRenderer?.getHtml?.(notification), text: await this.templateRenderer?.getText?.(notification), replyTo: this.replyTo, - ses: await this.getSesOptions(), + ses: this.sesOptions, }; await this.sendMails(mailOptions, emails); diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index ab56577b63..4d9c711297 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.3.0 + +### Minor Changes + +- f95a516: Enables optional routes to Slack channels for broadcast notifications based on origin and/or topics. + +### Patch Changes + +- b80857a: Slack notification handler throttling can now be configured with the `concurrencyLimit` and `throttleInterval` options. +- f8230e4: Updated dependency `@faker-js/faker` to `^10.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## 0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.2.1 + +### Patch Changes + +- d959bec: When an error message is logged due to inability to send a message with the Slack SDK, include the Slack Channel ID in the message to aid debugging. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + +## 0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## 0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/config.d.ts b/plugins/notifications-backend-module-slack/config.d.ts index 8cd450940e..17a4e25e0d 100644 --- a/plugins/notifications-backend-module-slack/config.d.ts +++ b/plugins/notifications-backend-module-slack/config.d.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { notifications?: { processors?: { @@ -26,8 +28,40 @@ export interface Config { * Broadcast notification receivers when receiver is set to config * These can be Slack User IDs, Slack User Email addresses, Slack Channel * Names, or Slack Channel IDs. Any valid identifier that chat.postMessage can accept. + * @deprecated Use broadcastRoutes instead for more granular control */ broadcastChannels?: string[]; + /** + * Optional username to display as the sender of the notification + */ + username?: string; + /** + * Routes for broadcast notifications based on origin and/or topic. + * Routes are evaluated in order, first match wins. + * Origin+topic matches take precedence over origin-only matches. + */ + broadcastRoutes?: Array<{ + /** + * The origin to match (e.g., 'plugin:catalog', 'external:my-service') + */ + origin?: string; + /** + * The topic to match (e.g., 'entity-updated', 'alerts') + */ + topic?: string; + /** + * The Slack channel(s) to send to. Can be channel IDs, channel names, or user IDs. + */ + channel: string | string[]; + }>; + /** + * Concurrency limit for Slack notifications per backend instance of the notifications plugin, defaults to 10. + */ + concurrencyLimit?: number; + /** + * Throttle duration between Slack notifications per backend instance of the notifications plugin, defaults to 1 minute. + */ + throttleInterval?: HumanDuration | string; }>; }; }; diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 8ee2eed30b..0e35577006 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.2.1-next.0", + "version": "0.3.0", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", @@ -54,7 +54,7 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/test-utils": "workspace:^", - "@faker-js/faker": "^8.4.1", + "@faker-js/faker": "^10.0.0", "msw": "^2.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index ad4bf01e9f..9490ff7dde 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -19,6 +19,22 @@ import { SlackNotificationProcessor } from './SlackNotificationProcessor'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { WebClient } from '@slack/web-api'; import { Entity } from '@backstage/catalog-model'; +import pThrottle from 'p-throttle'; +import { durationToMilliseconds } from '@backstage/types'; + +const throttleConfigs: Array<{ limit: number; interval: number }> = []; + +jest.mock('p-throttle', () => ({ + __esModule: true, + default: jest.fn((config: { limit: number; interval: number }) => { + throttleConfigs.push(config); + return Promise | any>(fn: T) => + (...args: Parameters) => + Promise.resolve(fn(...args)); + }), +})); + +const mockedPThrottle = pThrottle as jest.MockedFunction; jest.mock('@slack/web-api', () => { const mockSlack = { @@ -128,6 +144,8 @@ describe('SlackNotificationProcessor', () => { beforeEach(() => { jest.clearAllMocks(); + throttleConfigs.length = 0; + mockedPThrottle.mockClear(); }); it('should send a notification to a group', async () => { @@ -393,6 +411,462 @@ describe('SlackNotificationProcessor', () => { }); }); + describe('when broadcast routes are configured', () => { + it('should route by origin and topic (highest priority)', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastChannels: ['default-channel'], + broadcastRoutes: [ + { origin: 'plugin:catalog', channel: 'catalog-channel' }, + { topic: 'alerts', channel: 'alerts-channel' }, + { + origin: 'plugin:catalog', + topic: 'alerts', + channel: 'catalog-alerts-channel', + }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:catalog', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'alerts', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'alerts' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'catalog-alerts-channel' }), + ); + }); + + it('should route by origin only when no origin+topic match', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + { origin: 'plugin:catalog', channel: 'catalog-channel' }, + { topic: 'alerts', channel: 'alerts-channel' }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:catalog', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'updates', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'updates' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'catalog-channel' }), + ); + }); + + it('should route by topic only when no origin match', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + { origin: 'plugin:catalog', channel: 'catalog-channel' }, + { topic: 'alerts', channel: 'alerts-channel' }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:unknown', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'alerts', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'alerts' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'alerts-channel' }), + ); + }); + + it('should fall back to broadcastChannels when no route matches', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastChannels: ['default-channel'], + broadcastRoutes: [ + { origin: 'plugin:catalog', channel: 'catalog-channel' }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:unknown', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'default-channel' }), + ); + }); + + it('should support multiple channels in a route', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + { + origin: 'plugin:catalog', + channel: ['channel1', 'channel2', 'channel3'], + }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:catalog', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(3); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'channel1' }), + ); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'channel2' }), + ); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'channel3' }), + ); + }); + + it('should support single channel as string in a route', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + { topic: 'alerts', channel: 'single-alerts-channel' }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:any', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'alerts', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'alerts' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'single-alerts-channel' }), + ); + }); + + it('should not send when no route matches and no broadcastChannels configured', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + { origin: 'plugin:catalog', channel: 'catalog-channel' }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:unknown', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'unknown-topic', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'unknown-topic' }, + }, + ); + + expect(slack.chat.postMessage).not.toHaveBeenCalled(); + }); + + it('should match origin+topic route over origin-only route', async () => { + const slack = new WebClient(); + const routesConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + broadcastRoutes: [ + // Origin-only route defined first + { origin: 'plugin:catalog', channel: 'general-catalog' }, + // More specific origin+topic route defined second + { + origin: 'plugin:catalog', + topic: 'entity-deleted', + channel: 'catalog-deletions', + }, + ], + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig(routesConfig, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.postProcess( + { + origin: 'plugin:catalog', + id: '1234', + user: null, + created: new Date(), + payload: { + title: 'notification', + topic: 'entity-deleted', + }, + }, + { + recipients: { type: 'broadcast' }, + payload: { title: 'notification', topic: 'entity-deleted' }, + }, + ); + + // Should use the origin+topic match, not the origin-only match + expect(slack.chat.postMessage).toHaveBeenCalledTimes(1); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'catalog-deletions' }), + ); + }); + }); + describe('when slack.com/bot-notify annotation is missing', () => { it('should not send notification to a group without annotation', async () => { const slack = new WebClient(); @@ -953,4 +1427,81 @@ describe('SlackNotificationProcessor', () => { ); }); }); + + describe('when throttling is not configured', () => { + it('should use default concurrency limit of 10 per minute', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + expect(slack.chat.postMessage).toHaveBeenCalled(); + expect(throttleConfigs).toEqual([ + { + limit: 10, + interval: durationToMilliseconds({ minutes: 1 }), + }, + ]); + }); + }); + + describe('when throttling is configured', () => { + it('should use custom concurrency limit and interval values', async () => { + const slack = new WebClient(); + const throttlingConfig = mockServices.rootConfig({ + data: { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + slack: [ + { + token: 'mock-token', + concurrencyLimit: 5, + throttleInterval: 'PT30S', + }, + ], + }, + }, + }, + }); + + const processor = SlackNotificationProcessor.fromConfig( + throttlingConfig, + { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + }, + )[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + expect(slack.chat.postMessage).toHaveBeenCalled(); + expect(throttleConfigs).toEqual([ + { + limit: 5, + interval: durationToMilliseconds({ seconds: 30 }), + }, + ]); + }); + }); }); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index 96d4b2ff3e..e8f8d9eda4 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -21,7 +21,7 @@ import { parseEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; import { Notification } from '@backstage/plugin-notifications-common'; import { @@ -34,6 +34,7 @@ import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; import DataLoader from 'dataloader'; import pThrottle from 'p-throttle'; import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants'; +import { BroadcastRoute } from './types'; import { ExpiryMap, toChatPostMessageArgs } from './util'; import { CatalogService } from '@backstage/plugin-catalog-node'; @@ -48,8 +49,11 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly messagesSent: Counter; private readonly messagesFailed: Counter; private readonly broadcastChannels?: string[]; + private readonly broadcastRoutes?: BroadcastRoute[]; private readonly entityLoader: DataLoader; private readonly username?: string; + private readonly concurrencyLimit: number; + private readonly throttleInterval: number; static fromConfig( config: Config, @@ -68,10 +72,23 @@ export class SlackNotificationProcessor implements NotificationProcessor { const slack = options.slack ?? new WebClient(token); const broadcastChannels = c.getOptionalStringArray('broadcastChannels'); const username = c.getOptionalString('username'); + const broadcastRoutesConfig = c.getOptionalConfigArray('broadcastRoutes'); + const broadcastRoutes = broadcastRoutesConfig?.map(route => + this.parseBroadcastRoute(route), + ); + const concurrencyLimit = c.getOptionalNumber('concurrencyLimit') ?? 10; + const throttleInterval = c.has('throttleInterval') + ? durationToMilliseconds( + readDurationFromConfig(c, { key: 'throttleInterval' }), + ) + : durationToMilliseconds({ minutes: 1 }); return new SlackNotificationProcessor({ slack, broadcastChannels, + broadcastRoutes, username, + concurrencyLimit, + throttleInterval, ...options, }); }); @@ -83,16 +100,32 @@ export class SlackNotificationProcessor implements NotificationProcessor { logger: LoggerService; catalog: CatalogService; broadcastChannels?: string[]; + broadcastRoutes?: BroadcastRoute[]; username?: string; + concurrencyLimit?: number; + throttleInterval?: number; }) { - const { auth, catalog, logger, slack, broadcastChannels, username } = - options; + const { + auth, + catalog, + logger, + slack, + broadcastChannels, + broadcastRoutes, + username, + concurrencyLimit, + throttleInterval, + } = options; this.logger = logger; this.catalog = catalog; this.auth = auth; this.slack = slack; this.broadcastChannels = broadcastChannels; + this.broadcastRoutes = broadcastRoutes; this.username = username; + this.concurrencyLimit = concurrencyLimit ?? 10; + this.throttleInterval = + throttleInterval ?? durationToMilliseconds({ minutes: 1 }); this.entityLoader = new DataLoader( async entityRefs => { @@ -134,8 +167,8 @@ export class SlackNotificationProcessor implements NotificationProcessor { ); const throttle = pThrottle({ - limit: 10, - interval: durationToMilliseconds({ minutes: 1 }), + limit: this.concurrencyLimit, + interval: this.throttleInterval, }); const throttled = throttle((opts: ChatPostMessageArguments) => this.sendNotification(opts), @@ -148,12 +181,12 @@ export class SlackNotificationProcessor implements NotificationProcessor { let successCount = 0; let failureCount = 0; - results.forEach(result => { + results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; } else { this.logger.error( - `Failed to send Slack channel notification: ${result.reason.message}`, + `Failed to send Slack channel notification to ${opts[index].channel}: ${result.reason.message}`, ); failureCount++; } @@ -235,7 +268,8 @@ export class SlackNotificationProcessor implements NotificationProcessor { // Handle broadcast case if (notification.user === null) { - destinations.push(...(this.broadcastChannels ?? [])); + const routedChannels = this.getBroadcastDestinations(notification); + destinations.push(...routedChannels); } else if (options.recipients.type === 'entity') { // Handle user-specific notification const entityRefs = [options.recipients.entityRef].flat(); @@ -384,4 +418,88 @@ export class SlackNotificationProcessor implements NotificationProcessor { throw new Error(`Failed to send notification: ${response.error}`); } } + + private static parseBroadcastRoute(route: Config): BroadcastRoute { + const channelValue = route.getOptional('channel'); + let channels: string[]; + + if (typeof channelValue === 'string') { + channels = [channelValue]; + } else if (Array.isArray(channelValue)) { + channels = channelValue as string[]; + } else { + throw new Error( + 'broadcastRoutes entry must have a channel property (string or string[])', + ); + } + + return { + origin: route.getOptionalString('origin'), + topic: route.getOptionalString('topic'), + channels, + }; + } + + /** + * Gets the destination channels for a broadcast notification based on + * configured routes. Routes are matched by origin and/or topic. + * + * Matching precedence: + * 1. Routes with both origin AND topic matching (most specific) + * 2. Routes with only origin matching + * 3. Routes with only topic matching + * 4. Default broadcastChannels (least specific fallback) + * + * The first matching route wins within each precedence level. + */ + private getBroadcastDestinations(notification: Notification): string[] { + const { origin } = notification; + const { topic } = notification.payload; + + if (!this.broadcastRoutes || this.broadcastRoutes.length === 0) { + // Fall back to legacy broadcastChannels config + return this.broadcastChannels ?? []; + } + + // Find most specific match + // Priority 1: origin AND topic match + const originAndTopicMatch = this.broadcastRoutes.find( + route => + route.origin !== undefined && + route.topic !== undefined && + route.origin === origin && + route.topic === topic, + ); + + if (originAndTopicMatch) { + return originAndTopicMatch.channels; + } + + // Priority 2: origin-only match (no topic specified in route) + const originOnlyMatch = this.broadcastRoutes.find( + route => + route.origin !== undefined && + route.topic === undefined && + route.origin === origin, + ); + + if (originOnlyMatch) { + return originOnlyMatch.channels; + } + + // Priority 3: topic-only match (no origin specified in route) + const topicOnlyMatch = this.broadcastRoutes.find( + route => + route.topic !== undefined && + route.origin === undefined && + route.topic === topic, + ); + + if (topicOnlyMatch) { + return topicOnlyMatch.channels; + } + + // No match found, fall back to legacy broadcastChannels + return this.broadcastChannels ?? []; + } } diff --git a/plugins/notifications-backend-module-slack/src/lib/types.ts b/plugins/notifications-backend-module-slack/src/lib/types.ts index bd87e9656c..316df03462 100644 --- a/plugins/notifications-backend-module-slack/src/lib/types.ts +++ b/plugins/notifications-backend-module-slack/src/lib/types.ts @@ -18,3 +18,16 @@ export interface SlackNotificationOptions { url: string; payload: string; } + +/** + * Configuration for routing broadcast notifications to specific Slack channels + * based on origin and/or topic. + */ +export type BroadcastRoute = { + /** The origin to match (e.g., 'plugin:catalog', 'external:my-service') */ + origin?: string; + /** The topic to match (e.g., 'entity-updated', 'alerts') */ + topic?: string; + /** The Slack channel(s) to send to */ + channels: string[]; +}; diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index fd5a2d1d48..91da5468ca 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,108 @@ # @backstage/plugin-notifications-backend +## 0.6.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + - @backstage/plugin-catalog-node@1.20.1 + - @backstage/plugin-notifications-node@0.2.22 + +## 0.6.1-next.1 + +### Patch Changes + +- de96a60: chore(deps): bump `express` from 4.21.2 to 4.22.0 +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-node@1.20.1-next.1 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/plugin-notifications-node@0.2.22-next.1 + +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/plugin-catalog-node@1.20.1-next.0 + - @backstage/plugin-notifications-node@0.2.22-next.0 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.6.0 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- 15fb764: Show default settings for notifications even before receiving first notification. + + Previously, it was not possible for the users to see or modify their notification settings until they had received at + least one notification from specific origin or topic. + This update ensures that default settings are displayed from the outset, + allowing users to customize their preferences immediately. + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/plugin-notifications-node@0.2.21 + - @backstage/config@1.3.6 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-signals-node@0.1.26 + +## 0.6.0-next.2 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + - @backstage/plugin-notifications-node@0.2.21-next.2 + +## 0.5.12-next.1 + +### Patch Changes + +- 15fb764: Show default settings for notifications even before receiving first notification. + + Previously, it was not possible for the users to see or modify their notification settings until they had received at + least one notification from specific origin or topic. + This update ensures that default settings are displayed from the outset, + allowing users to customize their preferences immediately. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.20.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-notifications-node@0.2.21-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + ## 0.5.12-next.0 ### Patch Changes diff --git a/plugins/notifications-backend/config.d.ts b/plugins/notifications-backend/config.d.ts index 2b6f874f72..90a2053824 100644 --- a/plugins/notifications-backend/config.d.ts +++ b/plugins/notifications-backend/config.d.ts @@ -33,12 +33,34 @@ export interface Config { */ defaultSettings?: { channels?: { + /** + * Channel identifier (e.g., 'Web', 'Email') + */ id: string; + /** + * Optional flag to enable/disable the channel by default. + * If not set, defaults to true for backwards compatibility. + * When set to false, the channel uses an opt-in strategy where + * origins are disabled by default unless explicitly enabled. + */ + enabled?: boolean; origins?: { + /** + * Origin identifier (e.g., 'plugin:catalog', 'external:jenkins') + */ id: string; + /** + * Whether notifications from this origin are enabled by default + */ enabled: boolean; topics?: { + /** + * Topic identifier (e.g., 'entity-refresh', 'build-failure') + */ id: string; + /** + * Whether notifications for this topic are enabled by default + */ enabled: boolean; }[]; }[]; diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index ccdb2e0dad..7262b65d38 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.12-next.0", + "version": "0.6.1", "backstage": { "role": "backend-plugin", "pluginId": "notifications", @@ -48,7 +48,7 @@ "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "@backstage/types": "workspace:^", - "express": "^4.17.1", + "express": "^4.22.0", "express-promise-router": "^4.1.0", "knex": "^3.0.0", "p-throttle": "^4.1.1", diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index f42a7b4513..62a37acbbb 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -32,7 +32,7 @@ import { import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { v4 as uuid } from 'uuid'; -import { DatabaseNotificationsStore } from '../database'; +import { DatabaseNotificationsStore, generateSettingsHash } from '../database'; const databases = TestDatabases.create(); let store: DatabaseNotificationsStore; @@ -68,6 +68,16 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { id: 'external:test-service2', enabled: false, }, + { + id: 'external:test-service3', + enabled: true, + topics: [ + { + id: 'test-topic3', + enabled: false, + }, + ], + }, ], }, ], @@ -571,6 +581,157 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { expect(response.status).toEqual(400); }); + + it('should not send notification when channel is disabled and user has no settings', async () => { + // Create a new config with channel disabled + const configWithChannelDisabled = mockServices.rootConfig({ + data: { + app: { baseUrl: 'http://localhost' }, + notifications: { + defaultSettings: { + channels: [ + { + id: 'Web', + enabled: false, // Channel disabled by default (opt-in) + }, + ], + }, + }, + }, + }); + + const routerWithChannelDisabled = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config: configWithChannelDisabled, + httpAuth, + auth, + catalog, + }); + const appWithChannelDisabled = express() + .use(routerWithChannelDisabled) + .use(mockErrorHandler()); + + const sendNotificationToDisabledChannel = ( + opts: NotificationSendOptions, + ) => + request(appWithChannelDisabled) + .post('/notifications') + .send(opts) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const response = await sendNotificationToDisabledChannel({ + recipients: { + type: 'entity', + entityRef: ['user:default/mock'], + }, + payload: { + title: 'test notification', + topic: 'test-topic', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); // No notifications sent + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(0); // No notifications created + }); + + it('should send notification when user enabled specific topic even if channel is disabled', async () => { + // Create a new config with channel disabled + const configWithChannelDisabled = mockServices.rootConfig({ + data: { + app: { baseUrl: 'http://localhost' }, + notifications: { + defaultSettings: { + channels: [ + { + id: 'Web', + enabled: false, // Channel disabled by default (opt-in) + }, + ], + }, + }, + }, + }); + + const routerWithChannelDisabled = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config: configWithChannelDisabled, + httpAuth, + auth, + catalog, + }); + const appWithChannelDisabled = express() + .use(routerWithChannelDisabled) + .use(mockErrorHandler()); + + const sendNotificationToDisabledChannel = ( + opts: NotificationSendOptions, + ) => + request(appWithChannelDisabled) + .post('/notifications') + .send(opts) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // User explicitly enables a specific topic + const client = await database.getClient(); + await client('user_settings').insert({ + settings_key_hash: generateSettingsHash( + 'user:default/mock', + 'Web', + 'external:test-service', + 'important-topic', + ), + user: 'user:default/mock', + channel: 'Web', + origin: 'external:test-service', + topic: 'important-topic', + enabled: true, + }); + + const response = await sendNotificationToDisabledChannel({ + recipients: { + type: 'entity', + entityRef: ['user:default/mock'], + }, + payload: { + title: 'important notification', + topic: 'important-topic', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + created: expect.any(String), + id: expect.any(String), + origin: 'external:test-service', + payload: { + severity: 'normal', + title: 'important notification', + topic: 'important-topic', + }, + user: 'user:default/mock', + }, + ]); + + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(1); // Notification created for enabled topic + }); }); describe('POST /notifications with custom receiver resolver', () => { @@ -829,6 +990,16 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { id: 'external:test-service2', topics: [{ enabled: false, id: 'test-topic2' }], }, + { + enabled: true, + id: 'external:test-service3', + topics: [ + { + enabled: false, + id: 'test-topic3', + }, + ], + }, ]), }, ], @@ -863,6 +1034,16 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { id: 'external:test-service2', topics: [{ enabled: false, id: 'test-topic2' }], }, + { + enabled: true, + id: 'external:test-service3', + topics: [ + { + enabled: false, + id: 'test-topic3', + }, + ], + }, ]), }, ], @@ -887,11 +1068,184 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { id: 'external:test-service2', topics: [{ enabled: false, id: 'test-topic2' }], }, + { + enabled: true, + id: 'external:test-service3', + topics: [ + { + enabled: false, + id: 'test-topic3', + }, + ], + }, ]), }, ], }); }); + + it('should respect channel-level enabled flag from config', async () => { + // Create a new config with channel-level enabled flag + const configWithChannelEnabled = mockServices.rootConfig({ + data: { + app: { baseUrl: 'http://localhost' }, + notifications: { + defaultSettings: { + channels: [ + { + id: 'Web', + enabled: false, // Channel disabled by default (opt-in) + }, + ], + }, + }, + }, + }); + + const routerWithChannelDisabled = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config: configWithChannelEnabled, + httpAuth, + auth, + catalog, + }); + const appWithChannelDisabled = express() + .use(routerWithChannelDisabled) + .use(mockErrorHandler()); + + const response = await request(appWithChannelDisabled).get('/settings'); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + channels: [ + { + id: 'Web', + enabled: false, + origins: expect.arrayContaining([ + { + enabled: false, + id: 'external:test-service', + topics: [{ enabled: false, id: 'test-topic' }], + }, + { + enabled: false, + id: 'external:test-service2', + topics: [{ enabled: false, id: 'test-topic2' }], + }, + ]), + }, + ], + }); + }); + + it('should allow user to enable specific topic even when channel is disabled', async () => { + // Create a new config with channel disabled + const configWithChannelDisabled = mockServices.rootConfig({ + data: { + app: { baseUrl: 'http://localhost' }, + notifications: { + defaultSettings: { + channels: [ + { + id: 'Web', + enabled: false, // Channel disabled by default (opt-in) + }, + ], + }, + }, + }, + }); + + const routerWithChannelDisabled = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config: configWithChannelDisabled, + httpAuth, + auth, + catalog, + }); + const appWithChannelDisabled = express() + .use(routerWithChannelDisabled) + .use(mockErrorHandler()); + + const client = await database.getClient(); + + // Clear existing notifications from beforeEach + await client('notification').del(); + + // Create notifications with multiple topics for the same origin + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + topic: 'topic-build-failed', + title: 'Build Failed', + created: new Date(), + severity: 'high', + }); + + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + topic: 'topic-deployment-success', + title: 'Deployment Success', + created: new Date(), + severity: 'normal', + }); + + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + topic: 'topic-security-alert', + title: 'Security Alert', + created: new Date(), + severity: 'critical', + }); + + // User explicitly enables only one specific topic (build failures) + // The other topics are NOT in the database, so they should inherit from channel default (false) + await client('user_settings').insert({ + settings_key_hash: generateSettingsHash( + 'user:default/mock', + 'Web', + 'external:test-service', + 'topic-build-failed', + ), + user: 'user:default/mock', + channel: 'Web', + origin: 'external:test-service', + topic: 'topic-build-failed', + enabled: true, + }); + + const response = await request(appWithChannelDisabled).get('/settings'); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + channels: [ + { + id: 'Web', + enabled: false, + origins: [ + { + enabled: true, // Origin gets enabled when user enables a topic + id: 'external:test-service', + topics: expect.arrayContaining([ + { enabled: true, id: 'topic-build-failed' }, // User explicitly enabled this + { enabled: false, id: 'topic-deployment-success' }, // Inherits from channel default (false) + { enabled: false, id: 'topic-security-alert' }, // Inherits from channel default (false) + ]), + }, + ], + }, + ], + }); + }); }); describe('POST /settings', () => { diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 027d6a1732..9e410cb964 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -122,7 +122,7 @@ export async function createRouter( topic: any, existingOrigin: OriginSetting | undefined, defaultOriginSettings: OriginSetting | undefined, - defaultEnabled: boolean, + channelDefaultEnabled: boolean, ) => { const existingTopic = existingOrigin?.topics?.find( t => t.id.toLowerCase() === topic.topic.toLowerCase(), @@ -131,11 +131,14 @@ export async function createRouter( t => t.id.toLowerCase() === topic.topic.toLowerCase(), ); + // If topic has explicit setting, use it + // Otherwise check default topic settings from config + // Otherwise use channel default (not origin enabled state) return { id: topic.topic, enabled: existingTopic ? existingTopic.enabled - : defaultTopicSettings?.enabled ?? defaultEnabled, + : defaultTopicSettings?.enabled ?? channelDefaultEnabled, }; }; @@ -144,6 +147,8 @@ export async function createRouter( existingChannel: ChannelSetting | undefined, defaultChannelSettings: ChannelSetting | undefined, topics: { origin: string; topic: string }[], + channelDefaultEnabled: boolean, + channelHasExplicitEnabled: boolean, ) => { const existingOrigin = existingChannel?.origins?.find( o => o.id.toLowerCase() === originId.toLowerCase(), @@ -155,7 +160,7 @@ export async function createRouter( const defaultEnabled = existingOrigin ? existingOrigin.enabled - : defaultOriginSettings?.enabled ?? true; + : defaultOriginSettings?.enabled ?? channelDefaultEnabled; return { id: originId, @@ -167,7 +172,7 @@ export async function createRouter( t, existingOrigin, defaultOriginSettings, - defaultEnabled, + channelHasExplicitEnabled ? channelDefaultEnabled : defaultEnabled, ), ), }; @@ -186,14 +191,29 @@ export async function createRouter( c => c.id.toLowerCase() === channelId.toLowerCase(), ); + // Determine channel enabled state + const channelEnabled = + existingChannel?.enabled ?? defaultChannelSettings?.enabled; + + // Use channel's enabled flag as the default for origins if not explicitly set + const defaultEnabledForOrigins = channelEnabled ?? true; + + // Check if channel has explicit enabled flag (either from user settings or config) + const channelHasExplicitEnabled = + existingChannel?.enabled !== undefined || + defaultChannelSettings?.enabled !== undefined; + return { id: channelId, + enabled: channelEnabled, origins: origins.map(originId => getOriginSettings( originId, existingChannel, defaultChannelSettings, topics, + defaultEnabledForOrigins, + channelHasExplicitEnabled, ), ), }; @@ -207,6 +227,27 @@ export async function createRouter( const settings = await store.getNotificationSettings({ user }); const channels = getNotificationChannels(); + // Merge existing channels/origins/topics with configured settings + for (const channel of defaultNotificationSettings?.channels ?? []) { + if (!channels.includes(channel.id)) { + channels.push(channel.id); + } + + for (const origin of channel.origins ?? []) { + if (!origins.includes(origin.id)) { + origins.push(origin.id); + } + + for (const topic of origin.topics ?? []) { + if ( + !topics.some(t => t.origin === origin.id && t.topic === topic.id) + ) { + topics.push({ origin: origin.id, topic: topic.id }); + } + } + } + } + return { channels: channels.map(channelId => getChannelSettings(channelId, settings, origins, topics), @@ -220,7 +261,56 @@ export async function createRouter( origin: string; topic: string | null; }) => { - const settings = await getNotificationSettings(opts.user); + // Get user's explicit settings from database + const userSettings = await store.getNotificationSettings({ + user: opts.user, + }); + + // Build a minimal settings object with user settings and config defaults + const settings: NotificationSettings = { + channels: [ + { + id: opts.channel, + enabled: defaultNotificationSettings?.channels?.find( + c => c.id.toLowerCase() === opts.channel.toLowerCase(), + )?.enabled, + origins: [], + }, + ], + }; + + // Add user's channel if it exists + const userChannel = userSettings.channels.find( + c => c.id.toLowerCase() === opts.channel.toLowerCase(), + ); + if (userChannel) { + settings.channels[0] = { + ...settings.channels[0], + enabled: userChannel.enabled ?? settings.channels[0].enabled, + origins: userChannel.origins ?? [], + }; + } + + // Add config default origins if not in user settings + // Only add origins if the channel is enabled (not explicitly disabled) + const defaultChannelSettings = defaultNotificationSettings?.channels?.find( + c => c.id.toLowerCase() === opts.channel.toLowerCase(), + ); + if ( + defaultChannelSettings?.origins && + settings.channels[0].enabled !== false + ) { + for (const defaultOrigin of defaultChannelSettings.origins) { + if ( + !settings.channels[0].origins.some( + o => o.id.toLowerCase() === defaultOrigin.id.toLowerCase(), + ) + ) { + settings.channels[0].origins.push(defaultOrigin); + } + } + } + return isNotificationsEnabledFor( settings, opts.channel, diff --git a/plugins/notifications-common/CHANGELOG.md b/plugins/notifications-common/CHANGELOG.md index 002b6e75e3..623ced5b1d 100644 --- a/plugins/notifications-common/CHANGELOG.md +++ b/plugins/notifications-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-notifications-common +## 0.2.0 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + +## 0.2.0-next.1 + +### Minor Changes + +- 87e597c: Adds support for default configuration for an entire notification channel. + This setting will also be inherited down to origins and topics while still respecting the users individual choices. + + This will be handy if you want to use a "opt-in" strategy. + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 31043a59db..1fad4544c7 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-common", - "version": "0.1.2-next.0", + "version": "0.2.0", "description": "Common functionalities for the notifications plugin", "backstage": { "role": "common-library", diff --git a/plugins/notifications-common/report.api.md b/plugins/notifications-common/report.api.md index 337c41b7b5..ecc0ec8ef0 100644 --- a/plugins/notifications-common/report.api.md +++ b/plugins/notifications-common/report.api.md @@ -9,6 +9,7 @@ import { JsonValue } from '@backstage/types'; // @public (undocumented) export type ChannelSetting = { id: string; + enabled?: boolean; origins: OriginSetting[]; }; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 4e49de79de..41b335a4c2 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -136,7 +136,13 @@ export type NotificationProcessorFilters = { * @public */ export type TopicSetting = { + /** + * Topic identifier + */ id: string; + /** + * Whether notifications for this topic are enabled + */ enabled: boolean; }; @@ -144,8 +150,17 @@ export type TopicSetting = { * @public */ export type OriginSetting = { + /** + * Origin identifier + */ id: string; + /** + * Whether notifications from this origin are enabled + */ enabled: boolean; + /** + * Optional array of topic-specific settings + */ topics?: TopicSetting[]; }; @@ -153,7 +168,19 @@ export type OriginSetting = { * @public */ export type ChannelSetting = { + /** + * Channel identifier + */ id: string; + /** + * Optional flag to enable/disable the channel by default. + * If not set, defaults to true for backwards compatibility. + * When set to false, the channel uses an opt-in strategy. + */ + enabled?: boolean; + /** + * Array of origin settings for this channel + */ origins: OriginSetting[]; }; @@ -161,5 +188,8 @@ export type ChannelSetting = { * @public */ export type NotificationSettings = { + /** + * Array of channel settings + */ channels: ChannelSetting[]; }; diff --git a/plugins/notifications-common/src/utils.ts b/plugins/notifications-common/src/utils.ts index b95df5bea5..02d840e34f 100644 --- a/plugins/notifications-common/src/utils.ts +++ b/plugins/notifications-common/src/utils.ts @@ -29,14 +29,20 @@ export const isNotificationsEnabledFor = ( const origin = channel.origins.find(o => o.id === originId); if (!origin) { - return true; + // If no origin is found, use channel's enabled flag (defaults to true if not set) + return channel.enabled ?? true; } - if (topicId === null) { + + // If topic is specified, check topic-level setting + if (topicId !== null) { + const topic = origin.topics?.find(t => t.id === topicId); + if (topic) { + return topic.enabled; + } + // No explicit topic setting, check origin return origin.enabled; } - const topic = origin.topics?.find(t => t.id === topicId); - if (!topic) { - return origin.enabled; - } - return topic.enabled; + + // No topic specified, check origin-level setting + return origin.enabled; }; diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 8170ee2b83..98d1b3c84c 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-notifications-node +## 0.2.22 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27 + - @backstage/backend-plugin-api@1.6.0 + +## 0.2.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-signals-node@0.1.27-next.1 + - @backstage/backend-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.1-next.0 + - @backstage/plugin-signals-node@0.1.27-next.0 + - @backstage/catalog-client@1.12.1 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.2.21 + +### Patch Changes + +- 05f60e1: Refactored constructor parameter properties to explicit property declarations for compatibility with TypeScript's `erasableSyntaxOnly` setting. This internal refactoring maintains all existing functionality while ensuring TypeScript compilation compatibility. +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/backend-plugin-api@1.5.0 + - @backstage/catalog-model@1.7.6 + - @backstage/catalog-client@1.12.1 + - @backstage/plugin-signals-node@0.1.26 + +## 0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/backend-plugin-api@1.5.0-next.2 + +## 0.2.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.5.0-next.1 + - @backstage/plugin-signals-node@0.1.26-next.1 + ## 0.2.21-next.0 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 396a12bf05..8007852dd5 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.21-next.0", + "version": "0.2.22", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/.eslintrc.js b/plugins/notifications/.eslintrc.js index a2d8179106..e487f765b2 100644 --- a/plugins/notifications/.eslintrc.js +++ b/plugins/notifications/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - '@backstage/no-top-level-material-ui-4-imports': 'error', - }, + rules: { + '@backstage/no-top-level-material-ui-4-imports': 'error', + }, }); diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index a72cee1cb1..1967a8fcf7 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-notifications +## 0.5.13 + +### Patch Changes + +- 4452d15: Added i18n support. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.3 + - @backstage/core-components@0.18.5 + +## 0.5.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.5-next.0 + +## 0.5.12 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- 53347cc: Move long notification descriptions behind `Show more/less` button. + + This improves readability of the notifications list by preventing long descriptions from taking up too much space + or rendering very small scrollable areas. + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2 + - @backstage/core-components@0.18.4 + - @backstage/core-plugin-api@1.12.1 + - @backstage/theme@0.7.1 + - @backstage/plugin-signals-react@0.0.18 + +## 0.5.12-next.0 + +### Patch Changes + +- d02db50: Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system. +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.2-next.0 + - @backstage/core-plugin-api@1.12.1-next.0 + - @backstage/theme@0.7.1-next.0 + - @backstage/core-components@0.18.4-next.0 + - @backstage/plugin-signals-react@0.0.18-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-notifications-common@0.2.0 + +## 0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.13.0 + - @backstage/core-compat-api@0.5.4 + - @backstage/plugin-notifications-common@0.2.0 + - @backstage/core-components@0.18.3 + - @backstage/core-plugin-api@1.12.0 + - @backstage/plugin-signals-react@0.0.17 + +## 0.5.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-common@0.2.0-next.1 + - @backstage/frontend-plugin-api@0.12.2-next.2 + - @backstage/core-components@0.18.3-next.2 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/notifications/knip-report.md b/plugins/notifications/knip-report.md index 97d5b385fd..28921c328d 100644 --- a/plugins/notifications/knip-report.md +++ b/plugins/notifications/knip-report.md @@ -1,3 +1 @@ # Knip report - - diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 32a270f0b9..519456aee1 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.11-next.0", + "version": "0.5.13", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", @@ -51,7 +51,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/plugins/notifications/report-alpha.api.md b/plugins/notifications/report-alpha.api.md index bec9d5ae13..eb597e1e04 100644 --- a/plugins/notifications/report-alpha.api.md +++ b/plugins/notifications/report-alpha.api.md @@ -8,10 +8,12 @@ import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; -import { RouteRef } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) const _default: OverridableFrontendPlugin< @@ -20,7 +22,7 @@ const _default: OverridableFrontendPlugin< }, {}, { - 'api:notifications': ExtensionDefinition<{ + 'api:notifications': OverridableExtensionDefinition<{ kind: 'api'; name: undefined; config: {}; @@ -35,7 +37,7 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; - 'page:notifications': ExtensionDefinition<{ + 'page:notifications': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; config: { @@ -48,7 +50,7 @@ const _default: OverridableFrontendPlugin< | ExtensionDataRef | ExtensionDataRef | ExtensionDataRef< - RouteRef, + RouteRef_2, 'core.routing.ref', { optional: true; @@ -59,12 +61,77 @@ const _default: OverridableFrontendPlugin< defaultPath?: [Error: `Use the 'path' param instead`]; path: string; loader: () => Promise; - routeRef?: RouteRef; + routeRef?: RouteRef_2; }; }>; } >; export default _default; +// @alpha (undocumented) +export const notificationsTranslationRef: TranslationRef< + 'plugin.notifications', + { + readonly 'table.errors.markAllReadFailed': 'Failed to mark all notifications as read'; + readonly 'table.pagination.firstTooltip': 'First Page'; + readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}'; + readonly 'table.pagination.labelRowsSelect': 'rows'; + readonly 'table.pagination.lastTooltip': 'Last Page'; + readonly 'table.pagination.nextTooltip': 'Next Page'; + readonly 'table.pagination.previousTooltip': 'Previous Page'; + readonly 'table.emptyMessage': 'No records to display'; + readonly 'table.bulkActions.markAllRead': 'Mark all read'; + readonly 'table.bulkActions.markSelectedAsRead': 'Mark selected as read'; + readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread'; + readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later'; + readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected'; + readonly 'table.confirmDialog.title': 'Are you sure?'; + readonly 'table.confirmDialog.markAllReadDescription': 'Mark all notifications as read.'; + readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All'; + readonly 'filters.view.all': 'All'; + readonly 'filters.view.label': 'View'; + readonly 'filters.view.read': 'Read notifications'; + readonly 'filters.view.saved': 'Saved'; + readonly 'filters.view.unread': 'Unread notifications'; + readonly 'filters.title': 'Filters'; + readonly 'filters.severity.normal': 'Normal'; + readonly 'filters.severity.high': 'High'; + readonly 'filters.severity.low': 'Low'; + readonly 'filters.severity.label': 'Min severity'; + readonly 'filters.severity.critical': 'Critical'; + readonly 'filters.topic.label': 'Topic'; + readonly 'filters.topic.anyTopic': 'Any topic'; + readonly 'filters.createdAfter.label': 'Sent out'; + readonly 'filters.createdAfter.placeholder': 'Notifications since'; + readonly 'filters.createdAfter.last24h': 'Last 24h'; + readonly 'filters.createdAfter.lastWeek': 'Last week'; + readonly 'filters.createdAfter.anyTime': 'Any time'; + readonly 'filters.sortBy.origin': 'Origin'; + readonly 'filters.sortBy.label': 'Sort by'; + readonly 'filters.sortBy.placeholder': 'Field to sort by'; + readonly 'filters.sortBy.newest': 'Newest on top'; + readonly 'filters.sortBy.oldest': 'Oldest on top'; + readonly 'filters.sortBy.topic': 'Topic'; + readonly 'settings.table.origin': 'Origin'; + readonly 'settings.table.topic': 'Topic'; + readonly 'settings.title': 'Notification settings'; + readonly 'settings.errors.useNotificationFormat': 'useNotificationFormat must be used within a NotificationFormatProvider'; + readonly 'settings.errorTitle': 'Failed to load settings'; + readonly 'settings.noSettingsAvailable': 'No notification settings available, check back later'; + readonly 'sidebar.title': 'Notifications'; + readonly 'sidebar.errors.markAsReadFailed': 'Failed to mark notification as read'; + readonly 'sidebar.errors.fetchNotificationFailed': 'Failed to fetch notification'; + readonly 'notificationsPage.title': 'Notifications'; + readonly 'notificationsPage.tableTitle.all_one': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.all_other': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_one': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_other': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_one': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_other': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_one': 'Read notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_other': 'Read notifications ({{count}})'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/notifications/report.api.md b/plugins/notifications/report.api.md index a9aeb64cda..0d2912638f 100644 --- a/plugins/notifications/report.api.md +++ b/plugins/notifications/report.api.md @@ -3,7 +3,7 @@ > 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 { ApiRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -13,7 +13,6 @@ import { Notification as Notification_2 } from '@backstage/plugin-notifications- import { NotificationSettings } from '@backstage/plugin-notifications-common'; import { NotificationSeverity } from '@backstage/plugin-notifications-common'; import { NotificationStatus } from '@backstage/plugin-notifications-common'; -import * as React_2 from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableProps } from '@backstage/core-components'; @@ -111,10 +110,10 @@ export type NotificationSnackbarProperties = { }; dense?: boolean; maxSnack?: number; - snackStyle?: React_2.CSSProperties; - iconVariant?: Partial>; + snackStyle?: React.CSSProperties; + iconVariant?: Partial>; Components?: { - [key in NotificationSeverity]: React_2.JSXElementConstructor; + [key in NotificationSeverity]: React.JSXElementConstructor; }; }; diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index ca4be58209..b930373a46 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -22,21 +22,16 @@ import { fetchApiRef, } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from './routes'; -import { - compatWrapper, - convertLegacyRouteRef, - convertLegacyRouteRefs, -} from '@backstage/core-compat-api'; import { NotificationsClient, notificationsApiRef } from './api'; const page = PageBlueprint.make({ params: { path: '/notifications', - routeRef: convertLegacyRouteRef(rootRouteRef), + routeRef: rootRouteRef, loader: () => - import('./components/NotificationsPage').then(m => - compatWrapper(), - ), + import('./components/NotificationsPage').then(m => ( + + )), }, }); @@ -54,9 +49,11 @@ const api = ApiBlueprint.make({ export default createFrontendPlugin({ pluginId: 'notifications', info: { packageJson: () => import('../package.json') }, - routes: convertLegacyRouteRefs({ + routes: { root: rootRouteRef, - }), + }, // TODO(Rugvip): Nav item (i.e. NotificationsSidebarItem) currently needs to be installed manually extensions: [page, api], }); + +export { notificationsTranslationRef } from './translation'; diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index 7e3f519b0c..66aacdb72e 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -14,14 +14,15 @@ * limitations under the License. */ import { ChangeEvent } from 'react'; - -import Divider from '@material-ui/core/Divider'; import FormControl from '@material-ui/core/FormControl'; +import Divider from '@material-ui/core/Divider'; import Grid from '@material-ui/core/Grid'; import InputLabel from '@material-ui/core/InputLabel'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; import Typography from '@material-ui/core/Typography'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { notificationsTranslationRef } from '../../translation'; import { GetNotificationsOptions } from '../../api'; import { NotificationSeverity } from '@backstage/plugin-notifications-common'; @@ -47,58 +48,53 @@ export type NotificationsFiltersProps = { const ALL = '___all___'; -export const CreatedAfterOptions: { - [key: string]: { label: string; getDate: () => Date }; -} = { +type TranslationKey = keyof (typeof notificationsTranslationRef)['T']; + +export const CreatedAfterOptions = { last24h: { - label: 'Last 24h', + labelKey: 'filters.createdAfter.last24h' satisfies TranslationKey, getDate: () => new Date(Date.now() - 24 * 3600 * 1000), }, lastWeek: { - label: 'Last week', + labelKey: 'filters.createdAfter.lastWeek' satisfies TranslationKey, getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), }, all: { - label: 'Any time', + labelKey: 'filters.createdAfter.anyTime' satisfies TranslationKey, getDate: () => new Date(0), }, -}; +} as const; -export const SortByOptions: { - [key: string]: { - label: string; - sortBy: SortBy; - }; -} = { +export const SortByOptions = { newest: { - label: 'Newest on top', + labelKey: 'filters.sortBy.newest' satisfies TranslationKey, sortBy: { - sort: 'created', - sortOrder: 'desc', + sort: 'created' as const, + sortOrder: 'desc' as const, }, }, oldest: { - label: 'Oldest on top', + labelKey: 'filters.sortBy.oldest' satisfies TranslationKey, sortBy: { - sort: 'created', - sortOrder: 'asc', + sort: 'created' as const, + sortOrder: 'asc' as const, }, }, topic: { - label: 'Topic', + labelKey: 'filters.sortBy.topic' satisfies TranslationKey, sortBy: { - sort: 'topic', - sortOrder: 'asc', + sort: 'topic' as const, + sortOrder: 'asc' as const, }, }, origin: { - label: 'Origin', + labelKey: 'filters.sortBy.origin' satisfies TranslationKey, sortBy: { - sort: 'origin', - sortOrder: 'asc', + sort: 'origin' as const, + sortOrder: 'asc' as const, }, }, -}; +} as const; const getSortByText = (sortBy?: SortBy): string => { if (sortBy?.sort === 'created' && sortBy?.sortOrder === 'asc') { @@ -114,13 +110,6 @@ const getSortByText = (sortBy?: SortBy): string => { return 'newest'; }; -const AllSeverityOptions: { [key in NotificationSeverity]: string } = { - critical: 'Critical', - high: 'High', - normal: 'Normal', - low: 'Low', -}; - export const NotificationsFilters = ({ sorting, onSortingChanged, @@ -136,6 +125,7 @@ export const NotificationsFilters = ({ onTopicChanged, allTopics, }: NotificationsFiltersProps) => { + const { t } = useTranslationRef(notificationsTranslationRef); const sortByText = getSortByText(sorting); const handleOnCreatedAfterChanged = ( @@ -166,7 +156,8 @@ export const NotificationsFilters = ({ const handleOnSortByChanged = ( event: ChangeEvent<{ name?: string; value: unknown }>, ) => { - const idx = (event.target.value as string) || 'newest'; + const idx = ((event.target.value as string) || + 'newest') as keyof typeof SortByOptions; const option = SortByOptions[idx]; onSortingChanged({ ...option.sortBy }); }; @@ -201,41 +192,48 @@ export const NotificationsFilters = ({ <> - Filters + {t('filters.title')} - View + + {t('filters.view.label')} + - Sent out + + {t('filters.createdAfter.label')} + @@ -244,18 +242,20 @@ export const NotificationsFilters = ({ - Sort by + + {t('filters.sortBy.label')} + @@ -265,18 +265,20 @@ export const NotificationsFilters = ({ - Min severity + {t('filters.severity.label')} @@ -285,16 +287,18 @@ export const NotificationsFilters = ({ - Topic + + {t('filters.topic.label')} +