Merge branch 'backstage:master' into feature/catalog-export

This commit is contained in:
1337
2026-05-20 20:32:27 +02:00
committed by GitHub
1172 changed files with 67648 additions and 10161 deletions
@@ -1,8 +0,0 @@
---
'@backstage/core-components': patch
'@backstage/ui': patch
'@backstage/plugin-notifications': patch
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Added missing dependencies that were previously only available transitively.
@@ -1,5 +0,0 @@
---
'@backstage/errors': patch
---
Added explicit `name` property to `ServiceUnavailableError` for consistency with all other error classes, making it resilient to minification.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/ui': patch
---
Added `isPending` prop to Alert, Button, ButtonIcon, Table, and TableRoot as a replacement for the `loading` prop, aligning with React Aria naming conventions. The `loading` prop is now deprecated but still supported as an alias. CSS selectors now use `data-ispending` instead of `data-loading` for styling pending states; `data-loading` is still emitted for backward compatibility but will be removed alongside the `loading` prop.
**Affected components:** Alert, Button, ButtonIcon, Table, TableRoot
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-app': patch
---
Migrated React Aria imports from individual packages (`@react-aria/toast`, `@react-aria/button`, `@react-stately/toast`) to the monopackages (`react-aria`, `react-stately`).
-8
View File
@@ -1,8 +0,0 @@
---
'@backstage/ui': patch
'@backstage/plugin-app': patch
'@backstage/plugin-app-visualizer': patch
'@backstage/plugin-notifications': patch
---
Tightened React Aria dependency version ranges from `^` to `~` to prevent unintended minor version upgrades.
@@ -1,5 +0,0 @@
---
'@backstage/frontend-app-api': patch
---
Internal cleanup of routing utilities.
@@ -1,5 +0,0 @@
---
'@backstage/frontend-test-utils': patch
---
Removed internal `mockWithApiFactory` helper in favor of using `attachMockApiFactory` directly.
@@ -1,5 +0,0 @@
---
'@backstage/backend-app-api': minor
---
Added `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper to reimplement extension point outputs at backend creation time.
@@ -1,5 +0,0 @@
---
'@backstage/backend-defaults': patch
---
Exported `defaultServiceFactories` to allow use with `createSpecializedBackend` for advanced configuration like `extensionPointFactoryMiddleware`.
@@ -1,11 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed several database migration `down` functions that were not properly reversible, causing the SQL report to show warnings:
- `20241003170511_alter_target_in_locations.js`: both `up` and `down` now include `.notNullable()` when altering the `locations.target` column, preventing the `NOT NULL` constraint from being accidentally dropped when widening the column type from `varchar(255)` to `text`.
- `20220116144621_remove_legacy.js`: the `down` function now properly recreates the three dropped legacy tables (`entities`, `entities_search`, `entities_relations`) with correct columns and indices.
- `20210302150147_refresh_state.js`: the `down` function now drops dependent tables in the correct order (avoiding a FK constraint violation) and fixes a typo where the table was referred to as `references` instead of `refresh_state_references`.
- `20201005122705_add_entity_full_name.js`: the `down` function now drops the `full_name` column from `entities` (not `entities_search`), and restores the `entities_unique_name` index with the correct column order `(kind, name, namespace)`.
- `20200702153613_entities.js`: the `down` function now uses `table.integer('generation')` instead of `table.string('generation')`, restoring the correct column type.
@@ -1,7 +0,0 @@
---
'@backstage/ui': patch
---
Fixed dark mode background for Dialog component by correcting the theme attribute selector from `data-theme` to `data-theme-mode`.
**Affected components:** Dialog
@@ -1,5 +0,0 @@
---
'@backstage/cli-module-build': patch
---
Fixed config path resolution for the embedded-postgres database client detection to resolve paths relative to the target package directory rather than the workspace root.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed a performance regression in the `/entity-facets` endpoint when filters or permission conditions are applied, by routing the EXISTS-based filter through `final_entities` instead of correlating against the much larger `search` table.
@@ -1,5 +0,0 @@
---
'@backstage/frontend-test-utils': patch
---
Added explicit type annotations to `.map()` callback parameters in `renderInTestApp` to avoid implicit `any` errors with newer TypeScript versions.
@@ -1,5 +0,0 @@
---
'@backstage/backend-defaults': patch
---
Fixed scheduler `sleep` firing immediately for durations longer than ~24.8 days, caused by Node.js `setTimeout` overflowing its 32-bit millisecond limit.
@@ -1,7 +0,0 @@
---
'@backstage/ui': patch
---
Fixed an issue where the active tab indicator would disappear shortly after page load for uncontrolled Tabs.
**Affected components:** Tabs
@@ -1,5 +0,0 @@
---
'@backstage/plugin-home': patch
---
Fixed widgets not being movable or resizable after saved edits. Previously, entering edit mode didn't restore `isDraggable` and `isResizable`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Made the TechDocs sidebar positioning at tablet breakpoints configurable via CSS custom properties, allowing apps with custom sidebar widths to override the defaults. The available properties are `--techdocs-sidebar-closed-offset-pinned`, `--techdocs-sidebar-closed-offset-collapsed`, and `--techdocs-sidebar-open-translate`.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/ui': minor
---
Add support for flex item props (`grow`, `shrink`, and `basis`) to `Box`, `Card`, `Grid`, and `Flex` itself.
**Affected components:** Box, Card, Grid, Flex
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/ui': patch
---
Updated React Aria dependencies to v1.17.0 and migrated imports from individual `@react-aria/*` and `@react-stately/*` packages to the monopackages (`react-aria`, `react-stately`). This fixes a type resolution error for `@react-types/table` that occurred in new app installations.
@@ -1,5 +0,0 @@
---
'@backstage/integration': patch
---
Moved `registerMswTestHooks` to test files.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
---
Simplified the `OwnerEntityColumn` in the task list to rely on `EntityRefLink` and the entity presentation API instead of manually fetching entities from the catalog.
+214 -246
View File
@@ -2,262 +2,230 @@
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.0.34",
"@backstage/app-defaults": "1.7.7",
"app-example-plugin": "0.0.34",
"example-app-legacy": "0.2.120",
"example-backend": "0.0.49",
"@backstage/backend-app-api": "1.6.1",
"@backstage/backend-defaults": "0.17.0",
"example-app": "0.0.35",
"@backstage/app-defaults": "1.7.8",
"app-example-plugin": "0.0.35",
"example-app-legacy": "0.2.121",
"example-backend": "0.0.50",
"@backstage/backend-app-api": "1.7.0",
"@backstage/backend-defaults": "0.17.1",
"@backstage/backend-dev-utils": "0.1.7",
"@backstage/backend-dynamic-feature-service": "0.8.1",
"@backstage/backend-dynamic-feature-service": "0.8.2",
"@internal/backend": "0.0.1",
"@backstage/backend-openapi-utils": "0.6.8",
"@backstage/backend-plugin-api": "1.9.0",
"@backstage/backend-test-utils": "1.11.2",
"@backstage/catalog-client": "1.15.0",
"@backstage/catalog-model": "1.8.0",
"@backstage/cli": "0.36.1",
"@backstage/cli-common": "0.2.1",
"@backstage/cli-defaults": "0.1.1",
"@internal/cli": "0.0.3",
"@backstage/cli-module-actions": "0.1.0",
"@backstage/cli-module-auth": "0.1.1",
"@backstage/cli-module-build": "0.1.1",
"@backstage/cli-module-config": "0.1.1",
"@backstage/cli-module-github": "0.1.1",
"@backstage/cli-module-info": "0.1.1",
"@backstage/cli-module-lint": "0.1.1",
"@backstage/cli-module-maintenance": "0.1.1",
"@backstage/cli-module-migrate": "0.1.1",
"@backstage/cli-module-new": "0.1.2",
"@backstage/cli-module-test-jest": "0.1.1",
"@backstage/cli-module-translations": "0.1.1",
"@backstage/cli-node": "0.3.1",
"@backstage/codemods": "0.1.56",
"@backstage/config": "1.3.7",
"@backstage/config-loader": "1.10.10",
"@backstage/core-app-api": "1.20.0",
"@backstage/core-compat-api": "0.5.10",
"@backstage/core-components": "0.18.9",
"@backstage/core-plugin-api": "1.12.5",
"@backstage/create-app": "0.8.2",
"@backstage/dev-utils": "1.1.22",
"e2e-test": "0.2.39",
"@backstage/backend-openapi-utils": "0.6.9",
"@backstage/backend-plugin-api": "1.9.1",
"@backstage/backend-test-utils": "1.11.3",
"@backstage/catalog-client": "1.15.1",
"@backstage/catalog-model": "1.9.0",
"@backstage/cli": "0.36.2",
"@backstage/cli-common": "0.2.2",
"@backstage/cli-defaults": "0.1.2",
"@internal/cli": "0.0.4",
"@backstage/cli-module-actions": "0.1.1",
"@backstage/cli-module-auth": "0.1.2",
"@backstage/cli-module-build": "0.1.3",
"@backstage/cli-module-config": "0.1.2",
"@backstage/cli-module-github": "0.1.2",
"@backstage/cli-module-info": "0.1.2",
"@backstage/cli-module-lint": "0.1.2",
"@backstage/cli-module-maintenance": "0.1.2",
"@backstage/cli-module-migrate": "0.1.2",
"@backstage/cli-module-new": "0.1.3",
"@backstage/cli-module-test-jest": "0.1.2",
"@backstage/cli-module-translations": "0.1.2",
"@backstage/cli-node": "0.3.2",
"@backstage/codemods": "0.1.57",
"@backstage/config": "1.3.8",
"@backstage/config-loader": "1.10.11",
"@backstage/core-app-api": "1.20.1",
"@backstage/core-compat-api": "0.5.11",
"@backstage/core-components": "0.18.10",
"@backstage/core-plugin-api": "1.12.6",
"@backstage/create-app": "0.8.3",
"@backstage/dev-utils": "1.1.23",
"e2e-test": "0.2.40",
"@backstage/e2e-test-utils": "0.1.2",
"@backstage/errors": "1.3.0",
"@backstage/eslint-plugin": "0.2.3",
"@backstage/filter-predicates": "0.1.2",
"@backstage/frontend-app-api": "0.16.2",
"@backstage/frontend-defaults": "0.5.1",
"@backstage/frontend-dev-utils": "0.1.1",
"@backstage/frontend-dynamic-feature-loader": "0.1.11",
"@internal/frontend": "0.0.19",
"@backstage/frontend-plugin-api": "0.16.0",
"@backstage/frontend-test-utils": "0.5.2",
"@backstage/integration": "2.0.1",
"@backstage/integration-aws-node": "0.1.21",
"@backstage/integration-react": "1.2.17",
"@backstage/module-federation-common": "0.1.3",
"@backstage/errors": "1.3.1",
"@backstage/eslint-plugin": "0.3.0",
"@backstage/filter-predicates": "0.1.3",
"@backstage/frontend-app-api": "0.16.3",
"@backstage/frontend-defaults": "0.5.2",
"@backstage/frontend-dev-utils": "0.1.2",
"@backstage/frontend-dynamic-feature-loader": "0.1.12",
"@internal/frontend": "0.0.20",
"@backstage/frontend-plugin-api": "0.17.0",
"@backstage/frontend-test-utils": "0.6.0",
"@backstage/integration": "2.0.2",
"@backstage/integration-aws-node": "0.2.0",
"@backstage/integration-react": "1.2.18",
"@backstage/module-federation-common": "0.1.4",
"@internal/opaque": "0.0.1",
"@backstage/release-manifests": "0.0.13",
"@backstage/repo-tools": "0.17.1",
"@internal/scaffolder": "0.0.20",
"@techdocs/cli": "1.10.7",
"techdocs-cli-embedded-app": "0.2.119",
"@backstage/test-utils": "1.7.17",
"@backstage/repo-tools": "0.17.2",
"@internal/scaffolder": "0.0.21",
"@techdocs/cli": "1.11.0",
"techdocs-cli-embedded-app": "0.2.120",
"@backstage/test-utils": "1.7.18",
"@backstage/theme": "0.7.3",
"@backstage/types": "1.2.2",
"@backstage/ui": "0.14.0",
"@backstage/ui": "0.15.0",
"@backstage/version-bridge": "1.0.12",
"yarn-plugin-backstage": "0.0.11",
"@backstage/plugin-api-docs": "0.14.0",
"yarn-plugin-backstage": "0.0.12",
"@backstage/plugin-api-docs": "0.14.1",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.11",
"@backstage/plugin-app": "0.4.3",
"@backstage/plugin-app-backend": "0.5.13",
"@backstage/plugin-app-node": "0.1.44",
"@backstage/plugin-app-react": "0.2.2",
"@backstage/plugin-app-visualizer": "0.2.2",
"@backstage/plugin-auth": "0.1.7",
"@backstage/plugin-auth-backend": "0.28.0",
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.14",
"@backstage/plugin-auth-backend-module-auth0-provider": "0.4.0",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.19",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.14",
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.14",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.14",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.14",
"@backstage/plugin-auth-backend-module-github-provider": "0.5.2",
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.4.2",
"@backstage/plugin-auth-backend-module-google-provider": "0.3.14",
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.18",
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.14",
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.14",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.19",
"@backstage/plugin-auth-backend-module-oidc-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-okta-provider": "0.2.14",
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.14",
"@backstage/plugin-auth-backend-module-openshift-provider": "0.1.6",
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.13",
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.13",
"@backstage/plugin-auth-node": "0.7.0",
"@backstage/plugin-auth-react": "0.1.26",
"@backstage/plugin-bitbucket-cloud-common": "0.3.9",
"@backstage/plugin-catalog": "2.0.2",
"@backstage/plugin-catalog-backend": "3.6.0",
"@backstage/plugin-catalog-backend-module-aws": "0.4.22",
"@backstage/plugin-catalog-backend-module-azure": "0.3.16",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.13",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.10",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.10",
"@backstage/plugin-catalog-backend-module-gcp": "0.3.18",
"@backstage/plugin-catalog-backend-module-gerrit": "0.3.13",
"@backstage/plugin-catalog-backend-module-gitea": "0.1.11",
"@backstage/plugin-catalog-backend-module-github": "0.13.1",
"@backstage/plugin-catalog-backend-module-github-org": "0.3.21",
"@backstage/plugin-catalog-backend-module-gitlab": "0.8.2",
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.20",
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.11",
"@backstage/plugin-catalog-backend-module-ldap": "0.12.4",
"@backstage/plugin-catalog-backend-module-logs": "0.1.21",
"@backstage/plugin-catalog-backend-module-msgraph": "0.9.2",
"@backstage/plugin-catalog-backend-module-openapi": "0.2.21",
"@backstage/plugin-catalog-backend-module-puppetdb": "0.2.21",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.19",
"@backstage/plugin-catalog-backend-module-unprocessed": "0.6.10",
"@backstage/plugin-catalog-common": "1.1.9",
"@backstage/plugin-catalog-graph": "0.6.1",
"@backstage/plugin-catalog-import": "0.13.12",
"@backstage/plugin-catalog-node": "2.2.0",
"@backstage/plugin-catalog-react": "2.1.2",
"@backstage/plugin-catalog-unprocessed-entities": "0.2.29",
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.14",
"@backstage/plugin-config-schema": "0.1.79",
"@backstage/plugin-devtools": "0.1.38",
"@backstage/plugin-devtools-backend": "0.5.16",
"@backstage/plugin-devtools-common": "0.1.24",
"@backstage/plugin-devtools-react": "0.2.1",
"@backstage/plugin-events-backend": "0.6.1",
"@backstage/plugin-events-backend-module-aws-sqs": "0.4.21",
"@backstage/plugin-events-backend-module-azure": "0.2.30",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.30",
"@backstage/plugin-events-backend-module-bitbucket-server": "0.1.11",
"@backstage/plugin-events-backend-module-gerrit": "0.2.30",
"@backstage/plugin-events-backend-module-github": "0.4.11",
"@backstage/plugin-events-backend-module-gitlab": "0.3.11",
"@backstage/plugin-events-backend-module-google-pubsub": "0.2.2",
"@backstage/plugin-events-backend-module-kafka": "0.3.3",
"@backstage/plugin-events-backend-test-utils": "0.1.54",
"@backstage/plugin-events-node": "0.4.21",
"@internal/plugin-todo-list": "1.0.50",
"@internal/plugin-todo-list-backend": "1.0.49",
"@internal/plugin-todo-list-common": "1.0.30",
"@backstage/plugin-gateway-backend": "1.1.4",
"@backstage/plugin-home": "0.9.4",
"@backstage/plugin-home-react": "0.1.37",
"@backstage/plugin-kubernetes": "0.12.18",
"@backstage/plugin-kubernetes-backend": "0.21.3",
"@backstage/plugin-kubernetes-cluster": "0.0.36",
"@backstage/plugin-kubernetes-common": "0.9.11",
"@backstage/plugin-kubernetes-node": "0.4.3",
"@backstage/plugin-kubernetes-react": "0.5.18",
"@backstage/plugin-mcp-actions-backend": "0.1.12",
"@backstage/plugin-mui-to-bui": "0.2.6",
"@backstage/plugin-notifications": "0.5.16",
"@backstage/plugin-notifications-backend": "0.6.4",
"@backstage/plugin-notifications-backend-module-email": "0.3.20",
"@backstage/plugin-notifications-backend-module-slack": "0.4.1",
"@backstage/plugin-notifications-common": "0.2.2",
"@backstage/plugin-notifications-node": "0.2.25",
"@backstage/plugin-org": "0.7.1",
"@backstage/plugin-org-react": "0.1.49",
"@backstage/plugin-permission-backend": "0.7.11",
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.18",
"@backstage/plugin-permission-common": "0.9.8",
"@backstage/plugin-permission-node": "0.10.12",
"@backstage/plugin-permission-react": "0.5.0",
"@backstage/plugin-proxy-backend": "0.6.12",
"@backstage/plugin-proxy-node": "0.1.14",
"@backstage/plugin-scaffolder": "1.36.2",
"@backstage/plugin-scaffolder-backend": "3.4.0",
"@backstage/plugin-scaffolder-backend-module-azure": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.3.5",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.20",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.22",
"@backstage/plugin-scaffolder-backend-module-gcp": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-gitea": "0.2.20",
"@backstage/plugin-scaffolder-backend-module-github": "0.9.8",
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.11.5",
"@backstage/plugin-scaffolder-backend-module-notifications": "0.1.21",
"@backstage/plugin-scaffolder-backend-module-rails": "0.5.20",
"@backstage/plugin-scaffolder-backend-module-sentry": "0.3.3",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.21",
"@backstage/plugin-scaffolder-common": "2.1.0",
"@backstage/plugin-scaffolder-node": "0.13.2",
"@backstage/plugin-scaffolder-node-test-utils": "0.3.10",
"@backstage/plugin-scaffolder-react": "1.20.1",
"@backstage/plugin-search": "1.7.1",
"@backstage/plugin-search-backend": "2.1.1",
"@backstage/plugin-search-backend-module-catalog": "0.3.14",
"@backstage/plugin-search-backend-module-elasticsearch": "1.8.2",
"@backstage/plugin-search-backend-module-explore": "0.3.13",
"@backstage/plugin-search-backend-module-pg": "0.5.54",
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.19",
"@backstage/plugin-search-backend-module-techdocs": "0.4.13",
"@backstage/plugin-search-backend-node": "1.4.3",
"@backstage/plugin-search-common": "1.2.23",
"@backstage/plugin-search-react": "1.11.1",
"@backstage/plugin-signals": "0.0.30",
"@backstage/plugin-signals-backend": "0.3.14",
"@backstage/plugin-signals-node": "0.2.0",
"@backstage/plugin-signals-react": "0.0.21",
"@backstage/plugin-techdocs": "1.17.3",
"@backstage/plugin-techdocs-addons-test-utils": "2.0.4",
"@backstage/plugin-techdocs-backend": "2.1.7",
"@backstage/plugin-app": "0.4.6",
"@backstage/plugin-app-backend": "0.5.14",
"@backstage/plugin-app-node": "0.1.45",
"@backstage/plugin-app-react": "0.2.3",
"@backstage/plugin-app-visualizer": "0.2.4",
"@backstage/plugin-auth": "0.1.8",
"@backstage/plugin-auth-backend": "0.29.0",
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-auth0-provider": "0.4.1",
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.16",
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.20",
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.15",
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.15",
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-github-provider": "0.5.3",
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.4.3",
"@backstage/plugin-auth-backend-module-google-provider": "0.3.15",
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.19",
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.15",
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.15",
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.20",
"@backstage/plugin-auth-backend-module-oidc-provider": "0.4.16",
"@backstage/plugin-auth-backend-module-okta-provider": "0.2.15",
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.15",
"@backstage/plugin-auth-backend-module-openshift-provider": "0.1.7",
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.14",
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.14",
"@backstage/plugin-auth-node": "0.7.1",
"@backstage/plugin-auth-react": "0.1.27",
"@backstage/plugin-bitbucket-cloud-common": "0.3.10",
"@backstage/plugin-catalog": "2.0.5",
"@backstage/plugin-catalog-backend": "3.7.0",
"@backstage/plugin-catalog-backend-module-ai-model": "0.1.0",
"@backstage/plugin-catalog-backend-module-aws": "0.4.23",
"@backstage/plugin-catalog-backend-module-azure": "0.3.17",
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.14",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.11",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.11",
"@backstage/plugin-catalog-backend-module-gcp": "0.3.19",
"@backstage/plugin-catalog-backend-module-gerrit": "0.3.14",
"@backstage/plugin-catalog-backend-module-gitea": "0.1.12",
"@backstage/plugin-catalog-backend-module-github": "0.13.2",
"@backstage/plugin-catalog-backend-module-github-org": "0.3.22",
"@backstage/plugin-catalog-backend-module-gitlab": "0.8.3",
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.21",
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.12",
"@backstage/plugin-catalog-backend-module-ldap": "0.12.5",
"@backstage/plugin-catalog-backend-module-logs": "0.1.22",
"@backstage/plugin-catalog-backend-module-msgraph": "0.10.0",
"@backstage/plugin-catalog-backend-module-msgraph-incremental": "0.1.0",
"@backstage/plugin-catalog-backend-module-openapi": "0.2.22",
"@backstage/plugin-catalog-backend-module-puppetdb": "0.2.22",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.20",
"@backstage/plugin-catalog-backend-module-unprocessed": "0.6.12",
"@backstage/plugin-catalog-common": "1.1.10",
"@backstage/plugin-catalog-graph": "0.6.4",
"@backstage/plugin-catalog-import": "0.13.13",
"@backstage/plugin-catalog-node": "2.2.1",
"@backstage/plugin-catalog-react": "3.0.0",
"@backstage/plugin-catalog-unprocessed-entities": "0.2.31",
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.16",
"@backstage/plugin-config-schema": "0.1.80",
"@backstage/plugin-devtools": "0.1.39",
"@backstage/plugin-devtools-backend": "0.5.17",
"@backstage/plugin-devtools-common": "0.1.25",
"@backstage/plugin-devtools-react": "0.2.2",
"@backstage/plugin-events-backend": "0.6.2",
"@backstage/plugin-events-backend-module-aws-sqs": "0.4.22",
"@backstage/plugin-events-backend-module-azure": "0.2.31",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.31",
"@backstage/plugin-events-backend-module-bitbucket-server": "0.1.12",
"@backstage/plugin-events-backend-module-gerrit": "0.2.31",
"@backstage/plugin-events-backend-module-github": "0.4.12",
"@backstage/plugin-events-backend-module-gitlab": "0.3.12",
"@backstage/plugin-events-backend-module-google-pubsub": "0.2.3",
"@backstage/plugin-events-backend-module-kafka": "0.3.4",
"@backstage/plugin-events-backend-test-utils": "0.1.55",
"@backstage/plugin-events-node": "0.4.22",
"@internal/plugin-todo-list": "1.0.51",
"@internal/plugin-todo-list-backend": "1.0.50",
"@internal/plugin-todo-list-common": "1.0.31",
"@backstage/plugin-gateway-backend": "1.1.5",
"@backstage/plugin-home": "0.9.6",
"@backstage/plugin-home-react": "0.1.38",
"@backstage/plugin-kubernetes": "0.12.19",
"@backstage/plugin-kubernetes-backend": "0.21.4",
"@backstage/plugin-kubernetes-cluster": "0.0.37",
"@backstage/plugin-kubernetes-common": "0.9.12",
"@backstage/plugin-kubernetes-node": "0.4.4",
"@backstage/plugin-kubernetes-react": "0.5.19",
"@backstage/plugin-mcp-actions-backend": "0.1.13",
"@backstage/plugin-mui-to-bui": "0.2.7",
"@backstage/plugin-notifications": "0.5.17",
"@backstage/plugin-notifications-backend": "0.6.5",
"@backstage/plugin-notifications-backend-module-email": "0.3.21",
"@backstage/plugin-notifications-backend-module-slack": "0.4.2",
"@backstage/plugin-notifications-common": "0.2.3",
"@backstage/plugin-notifications-node": "0.2.26",
"@backstage/plugin-org": "0.7.4",
"@backstage/plugin-org-react": "0.1.50",
"@backstage/plugin-permission-backend": "0.7.12",
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.19",
"@backstage/plugin-permission-common": "0.9.9",
"@backstage/plugin-permission-node": "0.11.0",
"@backstage/plugin-permission-react": "0.5.1",
"@backstage/plugin-proxy-backend": "0.6.13",
"@backstage/plugin-proxy-node": "0.1.15",
"@backstage/plugin-scaffolder": "1.37.0",
"@backstage/plugin-scaffolder-backend": "4.0.0",
"@backstage/plugin-scaffolder-backend-module-azure": "0.2.21",
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.3.6",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.21",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.21",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.23",
"@backstage/plugin-scaffolder-backend-module-gcp": "0.2.21",
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.21",
"@backstage/plugin-scaffolder-backend-module-gitea": "0.2.21",
"@backstage/plugin-scaffolder-backend-module-github": "0.9.9",
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.11.6",
"@backstage/plugin-scaffolder-backend-module-notifications": "0.1.22",
"@backstage/plugin-scaffolder-backend-module-rails": "0.5.21",
"@backstage/plugin-scaffolder-backend-module-sentry": "0.3.4",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.22",
"@backstage/plugin-scaffolder-common": "2.2.0",
"@backstage/plugin-scaffolder-node": "0.13.3",
"@backstage/plugin-scaffolder-node-test-utils": "0.3.11",
"@backstage/plugin-scaffolder-react": "2.0.0",
"@backstage/plugin-search": "1.7.4",
"@backstage/plugin-search-backend": "2.1.2",
"@backstage/plugin-search-backend-module-catalog": "0.3.15",
"@backstage/plugin-search-backend-module-elasticsearch": "1.8.3",
"@backstage/plugin-search-backend-module-explore": "0.3.14",
"@backstage/plugin-search-backend-module-pg": "0.5.55",
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.20",
"@backstage/plugin-search-backend-module-techdocs": "0.4.14",
"@backstage/plugin-search-backend-node": "1.4.4",
"@backstage/plugin-search-common": "1.2.24",
"@backstage/plugin-search-react": "1.11.4",
"@backstage/plugin-signals": "0.0.31",
"@backstage/plugin-signals-backend": "0.3.15",
"@backstage/plugin-signals-node": "0.2.1",
"@backstage/plugin-signals-react": "0.0.22",
"@backstage/plugin-techdocs": "1.17.6",
"@backstage/plugin-techdocs-addons-test-utils": "2.0.5",
"@backstage/plugin-techdocs-backend": "2.2.0",
"@backstage/plugin-techdocs-common": "0.1.1",
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.35",
"@backstage/plugin-techdocs-node": "1.14.5",
"@backstage/plugin-techdocs-react": "1.3.10",
"@backstage/plugin-user-settings": "0.9.2",
"@backstage/plugin-user-settings-backend": "0.4.2",
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.36",
"@backstage/plugin-techdocs-node": "1.15.0",
"@backstage/plugin-techdocs-react": "1.3.11",
"@backstage/plugin-user-settings": "0.9.3",
"@backstage/plugin-user-settings-backend": "0.4.3",
"@backstage/plugin-user-settings-common": "0.1.0"
},
"changesets": [
"add-missing-transitive-deps",
"add-service-unavailable-error-name",
"brave-groups-learn",
"chubby-candies-cry",
"clamp-react-aria-deps",
"deduplicate-joinpaths-routing",
"delegate-attach-mock-api-factory",
"extension-point-middleware-backend-app-api",
"extension-point-middleware-backend-defaults",
"fix-alter-target-nullability",
"fix-dialog-dark-theme-selector",
"fix-embedded-postgres-config-paths",
"fix-facets-perf-regression",
"fix-implicit-any-renderInTestApp",
"fix-scheduler-sleep-overflow",
"fix-tabs-active-indicator-disappearing",
"fix-widget-resize-after-edit",
"free-ways-flow",
"funny-areas-rescue",
"gold-drinks-poke",
"move-registermswtesthooks-to-test-utils",
"owner-column-cleanup",
"remove-duplicate-deps",
"remove-portable-schema-deprecated-prop",
"remove-unused-deps",
"remove-unused-getgithubintegrationconfig",
"replace-duplicate-error-utilities",
"shy-ways-lay",
"slack-scope-message-updates",
"ui-date-range-picker",
"upgrade-module-federation-v2",
"zod-v3-config-schema-docs",
"zod-v4-dep-bump"
]
"changesets": []
}
-13
View File
@@ -1,13 +0,0 @@
---
'@backstage/backend-defaults': patch
'@backstage/cli': patch
'@backstage/core-compat-api': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-auth-backend-module-oidc-provider': patch
'@backstage/plugin-auth-node': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-notifications-backend-module-slack': patch
---
Removed duplicated entries that appeared in both `dependencies` and `devDependencies`.
@@ -1,5 +0,0 @@
---
'@backstage/frontend-plugin-api': minor
---
**BREAKING**: Removed the deprecated property form of `PortableSchema.schema`. The `schema` member is now a plain method that must be called as `schema()` — direct property access like `schema.type` or `schema.properties` is no longer supported.
-22
View File
@@ -1,22 +0,0 @@
---
'@backstage/core-components': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-backend-module-gitlab': patch
'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-devtools-backend': patch
'@backstage/plugin-kubernetes-node': patch
'@backstage/plugin-notifications-common': patch
'@backstage/plugin-notifications-node': patch
'@backstage/plugin-permission-backend': patch
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
'@backstage/plugin-search-backend': patch
'@backstage/plugin-signals-node': patch
'@backstage/plugin-techdocs-react': patch
'@backstage/plugin-user-settings-backend': patch
'@techdocs/cli': patch
---
Removed unused dependencies that had no imports in source code.
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-import': patch
---
Internal refactor
@@ -1,6 +0,0 @@
---
'@backstage/repo-tools': patch
'@backstage/create-app': patch
---
Replaced internal error utilities with shared ones from `@backstage/cli-common`.
-13
View File
@@ -1,13 +0,0 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-search-react': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-search': patch
'@backstage/plugin-app': patch
'@backstage/plugin-org': patch
---
Replaced old config schema values from existing extensions and blueprints.
@@ -1,5 +0,0 @@
---
'@backstage/plugin-notifications-backend-module-slack': patch
---
Added scope-based message update support. When a notification is re-sent with the same `scope` and `notification.updated` is set, the processor now calls `chat.update()` on the existing Slack message instead of sending a duplicate via `chat.postMessage()`. Message timestamps are persisted in a new `slack_message_timestamps` database table with automatic daily cleanup. The processor gracefully degrades to the previous behavior when no database is provided.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed a race condition in the stitch queue and entity processing claim logic where `SELECT FOR UPDATE SKIP LOCKED` row locks were released before the subsequent timestamp bump, allowing multiple workers to claim the same rows. Both the select and update now run inside a single transaction for MySQL and PostgreSQL.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/ui': patch
---
Added new `DateRangePicker` component — combines two date fields and a calendar popover for selecting a date range, built on React Aria with full keyboard and screen reader accessibility. Uses BUI design tokens throughout, including auto-incremented backgrounds via the bg consumer pattern.
@@ -1,8 +0,0 @@
---
'@backstage/cli-module-build': patch
'@backstage/frontend-dynamic-feature-loader': patch
'@backstage/module-federation-common': patch
'@backstage/backend-dynamic-feature-service': patch
---
Upgraded `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` from `^0.21.6` to `^2.3.3` to address known vulnerabilities.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/frontend-plugin-api': patch
---
Updated error messages and deprecation warnings to clarify that the `zod/v4` subpath export from the Zod v3 package is not supported by `configSchema`, since it does not include JSON Schema conversion. The `zod` dependency has been bumped to `^4.0.0`.
-12
View File
@@ -1,12 +0,0 @@
---
'@backstage/plugin-app': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-search': patch
'@backstage/plugin-search-react': patch
'@backstage/plugin-org': patch
---
The `zod` dependency has been bumped from `^3.25.76 || ^4.0.0` to `^4.0.0`, since `configSchema` requires the full Zod v4 package for JSON Schema support.
+1
View File
@@ -0,0 +1 @@
../../AGENTS.md
+1
View File
@@ -8,6 +8,7 @@
**/public/**
**/microsite/**
**/docs-ui/**
**/workspaces/**
**/templates/**
**/sample-templates/**
playwright.config.ts
+2
View File
@@ -58,6 +58,7 @@ yarn.lock @backstage/maintainers @backst
/plugins/catalog-backend-module-backstage-openapi @backstage/maintainers @backstage/openapi-tooling-maintainers
/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/catalog-maintainers
/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann
/plugins/catalog-backend-module-msgraph-incremental @backstage/maintainers @backstage/catalog-maintainers
/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers
/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers
/plugins/devtools @backstage/maintainers @awanlin
@@ -85,6 +86,7 @@ yarn.lock @backstage/maintainers @backst
/plugins/user-settings-common @backstage/maintainers @backstage/sda-se-reviewers
/scripts @backstage/operations-maintainers
/workspaces/ui @backstage/design-system-maintainers
/packages/backend-plugin-api/src/services/definitions/AuditorService.ts @backstage/maintainers @backstage/auditor-maintainers
/packages/backend-defaults/src/entrypoints/auditor @backstage/maintainers @backstage/auditor-maintainers
@@ -75,6 +75,7 @@ codemod
codemods
codeowners
codescene
Combobox
composability
composable
config
@@ -102,6 +103,7 @@ dayjs
debounce
debounced
debounces
debouncing
debuggability
declaratively
deduplicate
@@ -256,6 +258,7 @@ LocalStack
lockdown
lockfile
lockfiles
loopback
lookbehind
lookup
lookups
@@ -345,6 +348,7 @@ params
parseable
passthrough
passwordless
patcher
Patrik
pattison
Peloton
@@ -74,7 +74,7 @@ jobs:
- name: Cache Comment
if: ${{ steps.event.outputs.ACTION != 'closed' }}
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: comment.md
key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }}
@@ -103,7 +103,7 @@ jobs:
- name: Fetch cached Manifests File
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: comment.md
key: ${{ needs.setup.outputs.comment-cache-key }}
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
cat ${{ github.event_path }} > event.json
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: preview-spec
path: |
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
sync-labels: true
+3 -3
View File
@@ -40,7 +40,7 @@ jobs:
run: |
mkdir -p ./context
echo "${{ github.event.pull_request.number }}" > ./context/pr-number
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.node-version == '22.x'
with:
name: pr-context
@@ -257,7 +257,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/backstage-cli
key: ${{ runner.os }}-v${{ matrix.node-version }}-backstage-cli-${{ github.run_id }}
@@ -281,7 +281,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: always()
with:
path: .cache/backstage-cli
+9 -9
View File
@@ -72,7 +72,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
@@ -83,7 +83,7 @@ jobs:
run: yarn backstage-repo-tools package-docs
- name: upload API reference
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stable-reference
path: type-docs/
@@ -92,7 +92,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: always()
with:
path: .cache/package-docs
@@ -107,7 +107,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stable-openapi-docs
path: docs/**/api/**/*
@@ -145,7 +145,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-${{ github.run_id }}
@@ -156,7 +156,7 @@ jobs:
run: yarn backstage-repo-tools package-docs
- name: upload API reference
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: next-reference
path: type-docs/
@@ -165,7 +165,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: always()
with:
path: .cache/package-docs
@@ -175,7 +175,7 @@ jobs:
run: yarn build-storybook
- name: Upload Storybook
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: storybook
path: dist-storybook
@@ -191,7 +191,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: next-openapi-docs
path: docs/**/api/**/*
+3 -3
View File
@@ -94,7 +94,7 @@ jobs:
run: yarn backstage-cli config:check --lax
- name: backstage-cli cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/backstage-cli
key: ${{ runner.os }}-v${{ matrix.node-version }}-backstage-cli-${{ github.run_id }}
@@ -118,8 +118,8 @@ jobs:
yarn backstage-cli repo test --maxWorkers=3 --workerIdleMemoryLimit=1300M --coverage --success-cache --success-cache-dir .cache/backstage-cli
env:
BACKSTAGE_TEST_DISABLE_DOCKER: 1
BACKSTAGE_TEST_DATABASE_postgres18_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres18.ports[5432] }}
BACKSTAGE_TEST_DATABASE_postgres14_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres14.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES18_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres18.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres14.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }}
+1 -1
View File
@@ -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@10dcc54158ba4c137713d9d69d70a2da63b6bda3 # v3
uses: stefanbuck/github-issue-parser@cb6e97157cbf851e3a393ff8d57c93a484cc323f # v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/.common.yaml
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: 'Upload artifact'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
sarif_file: results.sarif
@@ -5,6 +5,10 @@ on:
- '.github/workflows/sync_dependabot-changesets.yml'
- '**/yarn.lock'
permissions:
contents: write
pull-requests: write
jobs:
generate-changeset:
runs-on: ubuntu-latest
@@ -16,6 +16,11 @@ on:
issue_comment:
types: [created]
permissions:
actions: none
contents: none
pull-requests: none
jobs:
trigger:
if: >
@@ -44,7 +49,7 @@ jobs:
echo "$ACTOR" > ./context/actor
echo "$ACTION" > ./context/action
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr-context
path: context/
@@ -5,6 +5,10 @@ on:
- '.github/workflows/sync_renovate-changesets.yml'
- '**/yarn.lock'
permissions:
contents: write
pull-requests: write
jobs:
generate-changeset:
runs-on: ubuntu-latest
@@ -29,7 +29,7 @@ jobs:
cache-prefix: ${{ runner.os }}-v22.x
- name: Create Snyk report
uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master
uses: snyk/actions/node@9cf6ca713d71123d2d229cc3d7f145b96ea3c518 # master
continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found
with:
args: >
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Monitor and Synchronize Snyk Policies
uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master
uses: snyk/actions/node@9cf6ca713d71123d2d229cc3d7f145b96ea3c518 # 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@9adf32b1121593767fc3c057af55b55db032dc04 # master
uses: snyk/actions/node@9cf6ca713d71123d2d229cc3d7f145b96ea3c518 # 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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
sarif_file: snyk.sarif
+2 -2
View File
@@ -49,7 +49,7 @@ jobs:
- name: Run Chromatic
id: chromatic
uses: chromaui/action@f191a0224b10e1a38b2091cefb7b7a2337009116 # latest
uses: chromaui/action@e3eb8ec36101d8f0253c7c3ae66e5a2b4e2197ba # latest
with:
token: ${{ secrets.GITHUB_TOKEN }}
# projectToken intentionally shared to allow collaborators to run Chromatic on forks
@@ -82,7 +82,7 @@ jobs:
- name: Post Chromatic Link in PR Comment
if: github.event_name == 'pull_request' && steps.chromatic.outputs.url && github.event.pull_request.head.repo.full_name == github.repository
uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3
with:
message: |
## 🎨 Visual Testing with Chromatic
+3 -3
View File
@@ -55,7 +55,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
# ️ 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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
run: |
mkdir -p ./context
echo "${{ github.event.pull_request.number }}" > ./context/pr-number
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.node-version == '22.x' && github.event_name == 'pull_request'
with:
name: pr-context
+9 -9
View File
@@ -77,7 +77,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
@@ -88,14 +88,14 @@ jobs:
run: yarn backstage-repo-tools package-docs
- name: upload API reference
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stable-reference
path: type-docs/
# 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: always()
with:
path: .cache/package-docs
@@ -110,7 +110,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stable-openapi-docs
path: docs/**/api/**/*
@@ -147,7 +147,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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-${{ github.run_id }}
@@ -158,14 +158,14 @@ jobs:
run: yarn backstage-repo-tools package-docs
- name: upload API reference
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: next-reference
path: type-docs/
# 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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: always()
with:
path: .cache/package-docs
@@ -175,7 +175,7 @@ jobs:
run: yarn build-storybook
- name: Upload Storybook
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: storybook
path: dist-storybook
@@ -191,7 +191,7 @@ jobs:
run: yarn docusaurus gen-api-docs all
- name: upload OpenAPI API docs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: next-openapi-docs
path: docs/**/api/**/*
-1
View File
@@ -1 +0,0 @@
Make TechDocs sidebar positioning configurable via CSS custom properties
-1
View File
@@ -1 +0,0 @@
Bump zod dependency to v4 for packages using configSchema and clarify that zod/v4 subpath from v3 is not supported
-1
View File
@@ -1 +0,0 @@
Clamp React Aria dependency ranges to patch-only updates to prevent unintended minor version upgrades
-1
View File
@@ -1 +0,0 @@
Fix active tab indicator disappearing on uncontrolled Tabs in @backstage/ui
+1
View File
@@ -0,0 +1 @@
Preserve external hrefs in BUI link components under non-root base path
+1
View File
@@ -3,6 +3,7 @@
.yarn
dist
microsite
workspaces
docs-ui/.next
docs-ui/public
docs-ui/out
+18 -31
View File
@@ -50,26 +50,11 @@ export default definePreview({
dynamicTitle: true,
},
},
background: {
name: 'Background',
description: 'Global background for components',
defaultValue: 'app',
toolbar: {
icon: 'contrast',
items: [
{ value: 'app', title: 'App Background' },
{ value: 'neutral-1', title: 'Neutral 1 Background' },
{ value: 'neutral-2', title: 'Neutral 2 Background' },
{ value: 'neutral-3', title: 'Neutral 3 Background' },
],
},
},
},
initialGlobals: {
themeMode: 'light',
themeName: 'backstage',
background: 'app',
},
parameters: {
@@ -143,7 +128,6 @@ export default definePreview({
globals.themeMode === 'light' ? themes.light : themes.dark;
const selectedThemeMode = globals.themeMode || 'light';
const selectedThemeName = globals.themeName || 'backstage';
const selectedBackground = globals.background || 'app';
const isFullscreen = context.parameters.layout === 'fullscreen';
useEffect(() => {
@@ -155,15 +139,13 @@ export default definePreview({
document.body.removeAttribute('data-theme-mode');
document.body.removeAttribute('data-theme-name');
};
}, [selectedTheme, selectedThemeName]);
}, [selectedThemeMode, selectedThemeName]);
useEffect(() => {
appThemeApi.setActiveThemeId(selectedThemeMode);
}, [selectedThemeMode]);
document.body.style.backgroundColor = 'var(--bui-bg-app)';
document.body.style.padding =
isFullscreen && selectedBackground !== 'app' ? '1rem' : '';
const docsStoryElements = document.getElementsByClassName('docs-story');
Array.from(docsStoryElements).forEach(element => {
(element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)';
@@ -174,18 +156,23 @@ export default definePreview({
{/* @ts-ignore */}
<TestApiProvider apis={apis}>
<AlertDisplay />
{Array.from({
length:
selectedBackground === 'app'
? 0
: parseInt(selectedBackground.split('-')[1], 10),
}).reduce<React.ReactNode>(
children => (
<Box bg="neutral" p="4">
{children}
</Box>
),
<Story />,
{selectedThemeName === 'spotify' ? (
<Box
bg="neutral"
m={isFullscreen ? '4' : undefined}
style={{
borderRadius: 'var(--bui-radius-3)',
height: isFullscreen
? 'calc(100vh - (var(--bui-space-4) * 2))'
: undefined,
overflow: 'auto',
overscrollBehavior: 'none',
}}
>
<Story />
</Box>
) : (
<Story />
)}
</TestApiProvider>
</UnifiedThemeProvider>
-25
View File
@@ -190,10 +190,6 @@
.bui-Tag {
border-radius: var(--bui-radius-full);
}
.bui-Container {
padding-inline: 0;
}
}
[data-theme-mode='light'][data-theme-name='spotify'] {
@@ -243,24 +239,3 @@
--bui-ring: rgba(255, 255, 255, 0.2);
}
/*
* Plugin header (@backstage/ui) and story shell header — kept at the bottom of
* this file for easier scanning alongside other component overrides above.
*/
[data-theme-name='spotify'] {
.bui-PluginHeaderToolbar {
padding: 0;
height: 32px;
border: none;
background: none;
margin-bottom: var(--bui-space-2);
}
.bui-PluginHeaderTabsWrapper {
padding: 0;
border: none;
background: none;
margin-left: -8px;
}
}
+4
View File
@@ -1,3 +1,7 @@
---
alwaysApply: true
---
Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
## Key Directories
+16 -13
View File
@@ -63,12 +63,10 @@ app:
# - apis.plugin.graphiql.browse.gitlab: true
# - graphiql-endpoint:graphiql/gitlab: true
- nav-item:search: false
- nav-item:user-settings: false
- nav-item:catalog
- nav-item:api-docs
- nav-item:scaffolder
- nav-item:app-visualizer
# Opt in to the experimental BUI scaffolder form theme
- sub-page:scaffolder/templates:
config:
enableBackstageUi: true
# Pages
- page:catalog:
@@ -99,6 +97,16 @@ app:
- custom:
title: Custom
- sub-page:scaffolder/templates:
config:
groups:
- title: Recommended Services
filter:
spec.type: service
- title: Documentation
filter:
spec.type: documentation
# Entity page cards
- entity-card:catalog/about:
config:
@@ -195,6 +203,7 @@ backend:
pluginSources:
- catalog
- scaffolder
- search
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
endpoints:
@@ -275,6 +284,7 @@ catalog:
pullRequestBranchName: backstage-integration
rules:
- allow:
- AiResource
- Component
- API
- Resource
@@ -343,15 +353,8 @@ scaffolder:
auth:
experimentalDynamicClientRegistration:
enabled: true
allowedRedirectUriPatterns:
- cursor://*
- http://localhost:*
- http://127.0.0.1:*
experimentalClientIdMetadataDocuments:
enabled: true
allowedRedirectUriPatterns:
- http://127.0.0.1:*
- http://localhost:*
### Add auth.keyStore.provider to more granularly control how to store JWK data when running
# the auth-backend.
@@ -1,6 +1,6 @@
<#
.DESCRIPTION
Cleanes up orphaned entities for the provided Backstage URL, defaults to the local backend
Cleans up orphaned entities for the provided Backstage URL, defaults to the local backend
#>
param(
[string]$backstageUrl = "http://localhost:7007"
@@ -2,7 +2,7 @@
set -euo pipefail
# Cleanes up orphaned entities for the provided Backstage URL, defaults to the local backend
# Cleans up orphaned entities for the provided Backstage URL, defaults to the local backend
BACKSTAGE_URL=${1:-'http://localhost:7007'}
echo $BACKSTAGE_URL
+4 -3
View File
@@ -11,6 +11,7 @@
"sync:changelog:force": "node scripts/sync-changelog.mjs --force"
},
"resolutions": {
"@remixicon/react": ">=4.6.0 <4.9.0",
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3"
},
@@ -22,13 +23,13 @@
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "16.2.1",
"@remixicon/react": "^4.6.0",
"@remixicon/react": ">=4.6.0 <4.9.0",
"@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": "16.2.1",
"next": "16.2.3",
"next-mdx-remote-client": "^2.1.2",
"prop-types": "^15.8.1",
"react": "19.2.4",
@@ -44,7 +45,7 @@
"@types/react-dom": "19.2.3",
"eslint": "^9",
"eslint-config-next": "16.2.1",
"postcss": "^8.5.6",
"postcss": "^8.5.10",
"postcss-import": "^16.1.1",
"typescript": "^5",
"unified": "^11.0.4"
+1 -1
View File
@@ -29,7 +29,7 @@ import {
} from './components';
export const reactAriaUrls = {
button: 'https://react-spectrum.adobe.com/react-aria/Button.html',
button: 'https://react-aria.adobe.com/Button',
};
<PageTitle
@@ -0,0 +1,151 @@
'use client';
import { Combobox } from '../../../../../packages/ui/src/components/Combobox/Combobox';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiCloudLine } from '@remixicon/react';
const fontOptions = [
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
];
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'mx', label: 'Mexico' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'it', label: 'Italy' },
{ value: 'es', label: 'Spain' },
{ value: 'jp', label: 'Japan' },
{ value: 'cn', label: 'China' },
{ value: 'in', label: 'India' },
{ value: 'br', label: 'Brazil' },
{ value: 'au', label: 'Australia' },
];
const sectionedFonts = [
{
title: 'Serif Fonts',
options: [
{ value: 'times', label: 'Times New Roman' },
{ value: 'georgia', label: 'Georgia' },
{ value: 'garamond', label: 'Garamond' },
],
},
{
title: 'Sans-Serif Fonts',
options: [
{ value: 'arial', label: 'Arial' },
{ value: 'helvetica', label: 'Helvetica' },
{ value: 'verdana', label: 'Verdana' },
],
},
{
title: 'Monospace Fonts',
options: [
{ value: 'courier', label: 'Courier New' },
{ value: 'consolas', label: 'Consolas' },
{ value: 'fira', label: 'Fira Code' },
],
},
];
export const Preview = () => (
<Combobox
label="Font Family"
options={fontOptions}
placeholder="Pick a font"
name="font"
style={{ maxWidth: 260 }}
/>
);
export const WithLabelAndDescription = () => (
<Combobox
label="Font Family"
description="Choose a font family for your document"
options={fontOptions}
placeholder="Pick a font"
name="font"
style={{ width: 300 }}
/>
);
export const Sizes = () => (
<Flex direction="row" gap="2">
<Combobox
label="Small"
size="small"
options={fontOptions}
name="font-small"
placeholder="Pick a font"
style={{ maxWidth: 260 }}
/>
<Combobox
label="Medium"
size="medium"
options={fontOptions}
name="font-medium"
placeholder="Pick a font"
style={{ maxWidth: 260 }}
/>
</Flex>
);
export const WithIcon = () => (
<Combobox
label="Font Family"
options={fontOptions}
placeholder="Pick a font"
name="font"
icon={<RiCloudLine />}
style={{ width: 300 }}
/>
);
export const Disabled = () => (
<Combobox
label="Font Family"
options={fontOptions}
placeholder="Pick a font"
name="font"
isDisabled
style={{ width: 300 }}
/>
);
export const AllowsCustomValue = () => (
<Combobox
label="Country"
options={countries}
placeholder="Type any country"
allowsCustomValue
name="country"
style={{ width: 300 }}
/>
);
export const DisabledOption = () => (
<Combobox
label="Font Family"
options={fontOptions}
placeholder="Pick a font"
name="font"
disabledKeys={['serif']}
style={{ width: 300 }}
/>
);
export const WithSections = () => (
<Combobox
label="Font Family"
options={sectionedFonts}
placeholder="Pick a font"
name="font"
style={{ width: 300 }}
/>
);
@@ -0,0 +1,153 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import {
Preview,
WithLabelAndDescription,
Sizes,
WithIcon,
Disabled,
DisabledOption,
AllowsCustomValue,
WithSections,
} from './components';
import { comboboxPropDefs } from './props-definition';
import {
optionPropDefs,
optionSectionPropDefs,
} from '../select/props-definition';
import {
comboboxUsageSnippet,
comboboxDefaultSnippet,
comboboxDescriptionSnippet,
comboboxSizesSnippet,
comboboxDisabledSnippet,
comboboxResponsiveSnippet,
comboboxIconSnippet,
comboboxDisabledOptionsSnippet,
comboboxCustomValueSnippet,
comboboxSectionsSnippet,
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ComboboxDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
combobox: 'https://react-aria.adobe.com/ComboBox',
};
<PageTitle
title="Combobox"
description="A text input paired with a filterable dropdown for selecting or typing a value."
/>
<Snippet
align="center"
py={4}
preview={<Preview />}
code={comboboxDefaultSnippet}
/>
## Usage
<CodeBlock code={comboboxUsageSnippet} />
## API reference
<PropsTable data={comboboxPropDefs} />
<ReactAriaLink component="ComboBox" href={reactAriaUrls.combobox} />
### Option types
The `options` prop accepts an array containing either of the following shapes.
#### `Option`
<PropsTable data={optionPropDefs} />
#### `OptionSection`
<PropsTable data={optionSectionPropDefs} />
## Examples
### Label and description
<Snippet
layout="side-by-side"
open
preview={<WithLabelAndDescription />}
code={comboboxDescriptionSnippet}
/>
### Sizes
<Snippet
layout="side-by-side"
open
preview={<Sizes />}
code={comboboxSizesSnippet}
/>
### With icon
<Snippet
layout="side-by-side"
open
preview={<WithIcon />}
code={comboboxIconSnippet}
/>
### Disabled
<Snippet
layout="side-by-side"
open
preview={<Disabled />}
code={comboboxDisabledSnippet}
/>
### Disabled options
<Snippet
layout="side-by-side"
open
preview={<DisabledOption />}
code={comboboxDisabledOptionsSnippet}
/>
### Custom values
Allow the user to type a value that is not in the option list by setting `allowsCustomValue`.
<Snippet
layout="side-by-side"
open
preview={<AllowsCustomValue />}
code={comboboxCustomValueSnippet}
/>
### With sections
Group options under section headings by passing objects with a `title` and a
nested `options` array.
<Snippet
layout="side-by-side"
open
preview={<WithSections />}
code={comboboxSectionsSnippet}
/>
### Responsive
Size can change at different breakpoints.
<CodeBlock code={comboboxResponsiveSnippet} />
<Theming definition={ComboboxDefinition} />
<ChangelogComponent component="combobox" />
@@ -0,0 +1,113 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const comboboxPropDefs: Record<string, PropDef> = {
options: {
type: 'enum',
values: ['(Option | OptionSection)[]'],
description: (
<>
Options to display in the dropdown. Pass <Chip>Option</Chip> objects
directly, or <Chip>OptionSection</Chip> objects to render grouped
options under section headings.
</>
),
},
allowsCustomValue: {
type: 'boolean',
default: 'false',
description:
'When true, the typed text is accepted as the value on blur or Enter even if it does not match any option.',
},
value: {
type: 'string',
description: 'Controlled selected value.',
},
defaultValue: {
type: 'string',
description: 'Initial value for uncontrolled usage.',
},
onChange: {
type: 'enum',
values: ['(value: Key | null) => void'],
description: 'Called when the selected option changes.',
},
inputValue: {
type: 'string',
description: 'Controlled input text.',
},
defaultInputValue: {
type: 'string',
description: 'Initial input text for uncontrolled usage.',
},
onInputChange: {
type: 'enum',
values: ['(value: string) => void'],
description: 'Called when the input text changes.',
},
label: {
type: 'string',
description: 'Visible label above the combobox.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary text shown next to the label. If not provided and isRequired
is true, displays <Chip>Required</Chip>.
</>
),
},
description: {
type: 'string',
description: 'Helper text displayed below the label.',
},
placeholder: {
type: 'string',
description: 'Text shown when the input is empty.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: 'Visual size of the combobox field.',
},
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the input.',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description: 'Called when the dropdown opens or closes.',
},
isDisabled: {
type: 'boolean',
description: 'Prevents user interaction when true.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of options that should be disabled.',
},
isRequired: {
type: 'boolean',
description: 'Marks the field as required for form validation.',
},
isInvalid: {
type: 'boolean',
description: 'Displays the combobox in an error state.',
},
name: {
type: 'string',
description: 'Form field name for form submission.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,114 @@
export const comboboxUsageSnippet = `import { Combobox } from '@backstage/ui';
<Combobox
name="font"
label="Font Family"
options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
export const comboboxDefaultSnippet = `<Combobox
name="font"
label="Font Family"
placeholder="Pick a font"
options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
export const comboboxDescriptionSnippet = `<Combobox
name="font"
label="Font Family"
description="Choose a font family for your document"
options={[ ... ]}
/>`;
export const comboboxIconSnippet = `<Combobox
name="font"
label="Font Family"
icon={<RiCloudLine />}
options={[ ... ]}
/>`;
export const comboboxSizesSnippet = `<Flex>
<Combobox
size="small"
label="Font family"
options={[ ... ]}
/>
<Combobox
size="medium"
label="Font family"
options={[ ... ]}
/>
</Flex>`;
export const comboboxDisabledSnippet = `<Combobox
isDisabled
label="Font family"
options={[ ... ]}
/>`;
export const comboboxResponsiveSnippet = `<Combobox
size={{ initial: 'small', lg: 'medium' }}
label="Font family"
options={[ ... ]}
/>`;
export const comboboxDisabledOptionsSnippet = `<Combobox
name="font"
label="Font Family"
placeholder="Pick a font"
disabledKeys={['cursive', 'serif']}
options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
export const comboboxCustomValueSnippet = `<Combobox
name="country"
label="Country"
allowsCustomValue
placeholder="Type any country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
// ... more options
]}
/>`;
export const comboboxSectionsSnippet = `<Combobox
name="font"
label="Font Family"
options={[
{
title: 'Serif Fonts',
options: [
{ value: 'times', label: 'Times New Roman' },
{ value: 'georgia', label: 'Georgia' },
{ value: 'garamond', label: 'Garamond' },
],
},
{
title: 'Sans-Serif Fonts',
options: [
{ value: 'arial', label: 'Arial' },
{ value: 'helvetica', label: 'Helvetica' },
{ value: 'verdana', label: 'Verdana' },
],
},
]}
/>`;
@@ -0,0 +1,36 @@
'use client';
import { DatePicker } from '../../../../../packages/ui/src/components/DatePicker/DatePicker';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { parseDate } from '@internationalized/date';
export const WithLabel = () => {
return <DatePicker label="Date" style={{ maxWidth: '220px' }} />;
};
export const Sizes = () => {
return (
<Flex
direction="column"
gap="4"
style={{ width: '100%', maxWidth: '280px' }}
>
<DatePicker label="Small" size="small" />
<DatePicker label="Medium" size="medium" />
</Flex>
);
};
export const WithDefaultValue = () => {
return (
<DatePicker
label="Booking date"
defaultValue={parseDate('2025-02-03')}
style={{ maxWidth: '280px' }}
/>
);
};
export const Disabled = () => {
return <DatePicker label="Date" isDisabled style={{ maxWidth: '280px' }} />;
};
@@ -0,0 +1,82 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { datePickerPropDefs } from './props-definition';
import {
datePickerUsageSnippet,
withLabelSnippet,
sizesSnippet,
withDefaultValueSnippet,
disabledSnippet,
} from './snippets';
import { WithLabel, Sizes, WithDefaultValue, Disabled } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { DatePickerDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
datePicker: 'https://react-aria.adobe.com/DatePicker',
};
<PageTitle
title="DatePicker"
description="A date picker that combines a date field and a calendar popover for selecting a date."
/>
<Snippet
align="center"
py={4}
preview={<WithLabel />}
code={withLabelSnippet}
/>
## Usage
<CodeBlock code={datePickerUsageSnippet} />
## API reference
<PropsTable data={datePickerPropDefs} />
<ReactAriaLink component="DatePicker" href={reactAriaUrls.datePicker} />
## Examples
### Sizes
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizesSnippet}
layout="side-by-side"
/>
### With default value
<Snippet
align="center"
py={4}
open
preview={<WithDefaultValue />}
code={withDefaultValueSnippet}
layout="side-by-side"
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabledSnippet}
layout="side-by-side"
/>
<Theming definition={DatePickerDefinition} />
<ChangelogComponent component="date-picker" />
@@ -0,0 +1,94 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const datePickerPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Visual size of the picker. Use <Chip>small</Chip> for dense layouts,{' '}
<Chip>medium</Chip> for prominent fields.
</>
),
},
label: {
type: 'string',
description: 'Visible label displayed above the picker.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary text shown next to the label. If not provided and isRequired
is true, displays <Chip>Required</Chip>.
</>
),
},
description: {
type: 'string',
description: 'Help text displayed below the label.',
},
value: {
type: 'enum',
values: ['DateValue'],
description: 'Controlled value of the date.',
},
defaultValue: {
type: 'enum',
values: ['DateValue'],
description: 'Default value for uncontrolled usage.',
},
onChange: {
type: 'enum',
values: ['(value: DateValue | null) => void'],
description: 'Handler called when the selected date changes.',
},
granularity: {
type: 'enum',
values: ['day', 'hour', 'minute', 'second'],
default: 'day',
description:
'Smallest unit displayed. Defaults to "day" for dates and "minute" for times.',
},
minValue: {
type: 'enum',
values: ['DateValue'],
description: 'Minimum allowed date. Dates before this are disabled.',
},
maxValue: {
type: 'enum',
values: ['DateValue'],
description: 'Maximum allowed date. Dates after this are disabled.',
},
isDateUnavailable: {
type: 'enum',
values: ['(date: DateValue) => boolean'],
description:
'Callback invoked for each calendar date. Return true to mark a date as unavailable.',
},
name: {
type: 'string',
description: 'Form field name for the date, submitted as ISO 8601.',
},
isRequired: {
type: 'boolean',
description: 'Whether the field is required for form submission.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the picker is disabled.',
},
isReadOnly: {
type: 'boolean',
description: 'Whether the picker is read-only.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,25 @@
export const datePickerUsageSnippet = `import { DatePicker } from '@backstage/ui';
<DatePicker label="Date" />`;
export const withLabelSnippet = `<DatePicker
label="Date"
description="Select the date of your event."
/>`;
export const sizesSnippet = `<Flex direction="column" gap="4">
<DatePicker label="Small" size="small" />
<DatePicker label="Medium" size="medium" />
</Flex>`;
export const withDefaultValueSnippet = `import { parseDate } from '@internationalized/date';
<DatePicker
label="Booking date"
defaultValue={parseDate('2025-02-03')}
/>`;
export const disabledSnippet = `<DatePicker
label="Date"
isDisabled
/>`;
@@ -1,6 +1,8 @@
'use client';
import { Header } from '../../../../../packages/ui/src/components/Header/Header';
import { HeaderMetadataUsers } from '../../../../../packages/ui/src/components/Header/HeaderMetadataUsers';
import { HeaderMetadataStatus } from '../../../../../packages/ui/src/components/Header/HeaderMetadataStatus';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import {
@@ -11,6 +13,29 @@ import {
import { MemoryRouter } from 'react-router-dom';
import { RiMore2Line } from '@remixicon/react';
const users = {
giles: {
name: 'Giles Peyton-Nicoll',
src: 'https://i.pravatar.cc/150?u=giles',
href: '/users/giles',
},
alice: {
name: 'Alice Johnson',
src: 'https://i.pravatar.cc/150?u=alice42',
href: '/users/alice',
},
bob: {
name: 'Bob Smith',
src: 'https://i.pravatar.cc/150?u=bob',
href: '/users/bob',
},
carol: {
name: 'Carol Williams',
src: 'https://i.pravatar.cc/150?u=carol',
href: '/users/carol',
},
};
const tabs = [
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
@@ -29,12 +54,37 @@ const breadcrumbs = [
},
];
const tags = [
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
];
const metadataUsers = [
{ label: 'Type', value: 'website' },
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Owner',
value: <HeaderMetadataUsers users={[users.giles]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers users={[users.alice, users.bob, users.carol]} />
),
},
];
export const WithEverything = () => (
<MemoryRouter initialEntries={['/overview']}>
<Header
title="Page Title"
tags={tags}
description="A short description of this page. Supports [inline links](https://backstage.io)."
metadata={metadataUsers}
tabs={tabs.slice(0, 2)}
breadcrumbs={breadcrumbs.slice(0, 2)}
customActions={
<>
<Button variant="secondary">Secondary</Button>
@@ -45,6 +95,84 @@ export const WithEverything = () => (
</MemoryRouter>
);
export const WithMetadataUsers = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Owner',
value: <HeaderMetadataUsers users={[users.giles]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[users.alice, users.bob, users.carol]}
/>
),
},
]}
/>
</MemoryRouter>
);
export const WithTags = () => (
<MemoryRouter>
<Header title="Page Title" tags={tags} />
</MemoryRouter>
);
export const WithDescription = () => (
<MemoryRouter>
<Header
title="Page Title"
description="A short description of this page. Supports [inline links](https://backstage.io)."
/>
</MemoryRouter>
);
export const WithMetadata = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{ label: 'Owner', value: 'platform-team' },
{ label: 'Type', value: 'website' },
]}
/>
</MemoryRouter>
);
export const WithMetadataStatus = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Build',
value: (
<HeaderMetadataStatus
label="Failed"
color="danger"
href="/builds/123"
/>
),
},
{
label: 'Coverage',
value: <HeaderMetadataStatus label="Warning" color="warning" />,
},
]}
/>
</MemoryRouter>
);
export const WithLongBreadcrumbs = () => (
<MemoryRouter>
<Header title="Page Title" breadcrumbs={breadcrumbs.slice(0, 2)} />
+44 -7
View File
@@ -3,17 +3,28 @@ import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import {
WithEverything,
WithLongBreadcrumbs,
WithTabs,
WithTags,
WithDescription,
WithMetadata,
WithMetadataUsers,
WithMetadataStatus,
WithCustomActions,
WithMenu,
} from './components';
import { headerPagePropDefs } from './props-definition';
import {
headerPagePropDefs,
headerMetadataUsersPropDefs,
} from './props-definition';
import {
usage,
defaultSnippet,
withTabs,
withBreadcrumbs,
withTags,
withDescription,
withMetadata,
withMetadataUsers,
withMetadataStatus,
withCustomActions,
withMenu,
} from './snippets';
@@ -24,7 +35,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Header"
description="A secondary header with title, breadcrumbs, tabs, and actions."
description="A secondary header with title, tags, description, metadata, tabs, and actions."
/>
<Snippet py={4} preview={<WithEverything />} code={defaultSnippet} />
@@ -39,11 +50,37 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## Examples
### Breadcrumbs
### Tags
Labels are truncated at 240px.
Tags are rendered above the title. Each tag with an `href` renders as a link; tags without `href` render as plain text. Tags are separated by a small circle divider.
<Snippet open preview={<WithLongBreadcrumbs />} code={withBreadcrumbs} />
<Snippet open preview={<WithTags />} code={withTags} />
### Description
The description accepts a markdown string with support for inline links. Bold, italic, and block-level markdown are not rendered.
<Snippet open preview={<WithDescription />} code={withDescription} />
### Metadata
Key-value pairs displayed below the description.
<Snippet open preview={<WithMetadata />} code={withMetadata} />
### Metadata with users
Use `HeaderMetadataUsers` as the metadata value to display users as avatars. A single user shows the avatar with their name beside it. Multiple users show a row of avatars — hover to reveal each name via tooltip. When a user has an `href`, the avatar and name become links.
<Snippet open preview={<WithMetadataUsers />} code={withMetadataUsers} />
<PropsTable data={headerMetadataUsersPropDefs} />
### Metadata with status
Use `HeaderMetadataStatus` as the metadata value to display a status indicator. The dot colour is driven by the `color` prop which maps to BUI status tokens. Pass an `href` to make the label a link.
<Snippet open preview={<WithMetadataStatus />} code={withMetadataStatus} />
### Tabs
@@ -5,6 +5,51 @@ export const headerPagePropDefs: Record<string, PropDef> = {
type: 'string',
description: 'Page heading displayed in the header.',
},
tags: {
type: 'complex',
description:
'Items displayed above the title. Each tag renders as a link when href is provided, or as plain text otherwise. Tags are separated by a small circle divider.',
complexType: {
name: 'HeaderTag[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the tag.',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when the tag is clicked.',
},
},
},
},
description: {
type: 'string',
description:
'Markdown string rendered below the title. Only inline links are supported. Bold, italic, and block-level markdown are not rendered.',
},
metadata: {
type: 'complex',
description: 'Key-value pairs displayed below the description.',
complexType: {
name: 'HeaderMetadataItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'The key label, displayed in secondary color.',
},
value: {
type: 'string | ReactNode',
required: true,
description:
'The value to display alongside the label. Pass a string for plain text or a ReactNode for custom content such as HeaderMetadataUsers.',
},
},
},
},
customActions: {
type: 'enum',
values: ['ReactNode'],
@@ -49,6 +94,7 @@ export const headerPagePropDefs: Record<string, PropDef> = {
},
breadcrumbs: {
type: 'complex',
deprecated: true,
description: 'Breadcrumb trail displayed above the title.',
complexType: {
name: 'HeaderBreadcrumb[]',
@@ -68,3 +114,33 @@ export const headerPagePropDefs: Record<string, PropDef> = {
},
...classNamePropDefs,
};
export const headerMetadataUsersPropDefs: Record<string, PropDef> = {
users: {
type: 'complex',
description:
'List of users to display. A single user shows the avatar with their name beside it. Multiple users show a row of avatars with names revealed on hover via tooltip.',
complexType: {
name: 'HeaderMetadataUser[]',
properties: {
name: {
type: 'string',
required: true,
description:
'Display name shown beside the avatar (single) or in the tooltip (multiple).',
},
src: {
type: 'string',
required: false,
description: 'URL for the avatar image.',
},
href: {
type: 'string',
required: false,
description:
'When provided, the avatar becomes a link and the name is rendered as a Link component.',
},
},
},
},
};
+98 -5
View File
@@ -2,15 +2,41 @@ export const usage = `import { Header } from '@backstage/ui';
<Header title="Page Title" />`;
export const defaultSnippet = `<Header
export const defaultSnippet = `import { Header, HeaderMetadataUsers, HeaderMetadataStatus } from '@backstage/ui';
<Header
title="Page Title"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
tags={[
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
]}
description="A short description. Supports [inline links](https://backstage.io)."
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Owner',
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
]}
/>
),
},
]}
tabs={[
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'settings', label: 'Settings', href: '/settings' },
{ id: 'checks', label: 'Checks', href: '/checks' },
]}
customActions={
<>
@@ -54,3 +80,70 @@ export const withMenu = `<Header
</MenuTrigger>
}
/>`;
export const withTags = `<Header
title="Page Title"
tags={[
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
{ label: 'Gold' },
]}
/>`;
export const withDescription = `<Header
title="Page Title"
description="A short description. Supports [inline links](https://backstage.io)."
/>`;
export const withMetadata = `<Header
title="Page Title"
metadata={[
{ label: 'Owner', value: 'platform-team' },
{ label: 'Type', value: 'website' },
]}
/>`;
export const withMetadataStatus = `import { Header, HeaderMetadataStatus } from '@backstage/ui';
<Header
title="Page Title"
metadata={[
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Build',
value: <HeaderMetadataStatus label="Failed" color="danger" href="/builds/123" />,
},
{
label: 'Coverage',
value: <HeaderMetadataStatus label="Warning" color="warning" />,
},
]}
/>`;
export const withMetadataUsers = `import { Header, HeaderMetadataUsers } from '@backstage/ui';
<Header
title="Page Title"
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Owner',
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
]}
/>
),
},
]}
/>`;
@@ -40,6 +40,33 @@ const skills = [
{ value: 'swift', label: 'Swift' },
];
const sectionedFonts = [
{
title: 'Serif Fonts',
options: [
{ value: 'times', label: 'Times New Roman' },
{ value: 'georgia', label: 'Georgia' },
{ value: 'garamond', label: 'Garamond' },
],
},
{
title: 'Sans-Serif Fonts',
options: [
{ value: 'arial', label: 'Arial' },
{ value: 'helvetica', label: 'Helvetica' },
{ value: 'verdana', label: 'Verdana' },
],
},
{
title: 'Monospace Fonts',
options: [
{ value: 'courier', label: 'Courier New' },
{ value: 'consolas', label: 'Consolas' },
{ value: 'fira', label: 'Fira Code' },
],
},
];
export const Preview = () => (
<Select
label="Font Family"
@@ -148,3 +175,23 @@ export const SearchableMultiple = () => (
style={{ width: 300 }}
/>
);
export const WithSections = () => (
<Select
label="Font Family"
options={sectionedFonts}
name="font"
style={{ width: 300 }}
/>
);
export const SearchableWithSections = () => (
<Select
label="Font Family"
searchable
searchPlaceholder="Search fonts..."
options={sectionedFonts}
name="font"
style={{ width: 300 }}
/>
);
+44 -1
View File
@@ -12,8 +12,14 @@ import {
Searchable,
MultipleSelection,
SearchableMultiple,
WithSections,
SearchableWithSections,
} from './components';
import { selectPropDefs } from './props-definition';
import {
selectPropDefs,
optionPropDefs,
optionSectionPropDefs,
} from './props-definition';
import {
selectUsageSnippet,
selectDefaultSnippet,
@@ -26,6 +32,8 @@ import {
selectMultipleSnippet,
selectSearchableMultipleSnippet,
selectDisabledOptionsSnippet,
selectSectionsSnippet,
selectSearchableSectionsSnippet,
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -58,6 +66,18 @@ export const reactAriaUrls = {
<ReactAriaLink component="Select" href={reactAriaUrls.select} />
### Option types
The `options` prop accepts an array containing either of the following shapes.
#### `Option`
<PropsTable data={optionPropDefs} />
#### `OptionSection`
<PropsTable data={optionSectionPropDefs} />
## Examples
### Label and description
@@ -136,6 +156,29 @@ Combine search and multiple selection.
code={selectSearchableMultipleSnippet}
/>
### With sections
Group options under section headings by passing objects with a `title` and a
nested `options` array.
<Snippet
layout="side-by-side"
open
preview={<WithSections />}
code={selectSectionsSnippet}
/>
### Searchable with sections
Sections are preserved when filtering with `searchable`.
<Snippet
layout="side-by-side"
open
preview={<SearchableWithSections />}
code={selectSearchableSectionsSnippet}
/>
### Responsive
Size can change at different breakpoints.
@@ -5,30 +5,48 @@ import {
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const optionPropDefs: Record<string, PropDef> = {
value: {
type: 'string',
required: true,
description: 'Unique value for the option.',
},
label: {
type: 'string',
required: true,
description: 'Display text for the option.',
},
disabled: {
type: 'boolean',
description: 'Whether the option is disabled.',
},
};
export const optionSectionPropDefs: Record<string, PropDef> = {
title: {
type: 'string',
required: true,
description: 'Heading displayed above the grouped options.',
},
options: {
type: 'enum',
values: ['Option[]'],
required: true,
description: 'Options nested inside the section.',
},
};
export const selectPropDefs: Record<string, PropDef> = {
options: {
type: 'complex',
description: 'Array of options to display in the dropdown.',
complexType: {
name: 'SelectOption[]',
properties: {
value: {
type: 'string',
required: true,
description: 'Unique value for the option.',
},
label: {
type: 'string',
required: true,
description: 'Display text for the option.',
},
disabled: {
type: 'boolean',
required: false,
description: 'Whether the option is disabled.',
},
},
},
type: 'enum',
values: ['(Option | OptionSection)[]'],
description: (
<>
Options to display in the dropdown. Pass <Chip>Option</Chip> objects
directly, or <Chip>OptionSection</Chip> objects to render grouped
options under section headings.
</>
),
},
selectionMode: {
type: 'enum',
@@ -110,3 +110,49 @@ export const selectDisabledOptionsSnippet = `<Select
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
export const selectSectionsSnippet = `<Select
name="font"
label="Font Family"
options={[
{
title: 'Serif Fonts',
options: [
{ value: 'times', label: 'Times New Roman' },
{ value: 'georgia', label: 'Georgia' },
{ value: 'garamond', label: 'Garamond' },
],
},
{
title: 'Sans-Serif Fonts',
options: [
{ value: 'arial', label: 'Arial' },
{ value: 'helvetica', label: 'Helvetica' },
{ value: 'verdana', label: 'Verdana' },
],
},
]}
/>`;
export const selectSearchableSectionsSnippet = `<Select
name="font"
label="Font Family"
searchable
searchPlaceholder="Search fonts..."
options={[
{
title: 'Serif Fonts',
options: [
{ value: 'times', label: 'Times New Roman' },
{ value: 'georgia', label: 'Georgia' },
],
},
{
title: 'Sans-Serif Fonts',
options: [
{ value: 'arial', label: 'Arial' },
{ value: 'helvetica', label: 'Helvetica' },
],
},
]}
/>`;
+1 -1
View File
@@ -28,7 +28,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SliderDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
slider: 'https://react-spectrum.adobe.com/react-aria/Slider.html',
slider: 'https://react-aria.adobe.com/Slider',
};
<PageTitle
+2 -2
View File
@@ -126,9 +126,9 @@ Configure page size and available options through `paginationOptions`. The table
### Search
The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query.
The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query. When the search query changes, pagination resets to the first page automatically.
The search state is debounced internally, so rapid typing doesn't trigger excessive re-filtering. When the search query changes, pagination resets to the first page automatically.
In `complete` mode, set `searchDebounceMs` (and/or `filterDebounceMs`) to defer the filtering pipeline until typing settles — useful for large datasets. Both default to `0` (no debounce). The controlled `search` / `onSearchChange` (and `filter` / `onFilterChange`) surface continues to fire on every change. For `offset` and `cursor` modes, requests are already debounced internally, so these options don't apply.
For server-side search with `offset` or `cursor` modes, the search query is passed to your `getData` function. See [Server-Side Data](#server-side-data).
@@ -157,6 +157,28 @@ export const useTableOptionsPropDefs: Record<string, PropDef> = {
</>
),
},
searchDebounceMs: {
type: 'number',
description: (
<>
Trailing-edge debounce delay (ms) applied to the search value before it
reaches <Chip>searchFn</Chip>. Defaults to <Chip>0</Chip> (no debounce).
Does not affect the controlled <Chip>onSearchChange</Chip> callback.
Only used with <Chip>complete</Chip> mode.
</>
),
},
filterDebounceMs: {
type: 'number',
description: (
<>
Trailing-edge debounce delay (ms) applied to the filter value before it
reaches <Chip>filterFn</Chip>. Defaults to <Chip>0</Chip> (no debounce).
Does not affect the controlled <Chip>onFilterChange</Chip> callback.
Only used with <Chip>complete</Chip> mode.
</>
),
},
};
export const useTableReturnPropDefs: Record<string, PropDef> = {
+36
View File
@@ -264,6 +264,42 @@ pressed, and disabled variants for interactive states.
</Table.Body>
</Table.Root>
## Inherited background
`--bui-bg-inherit` resolves to the bg color of the nearest enclosing element with a
`data-bg` attribute (set by `Box`, `Flex`, `Grid`, `Card`, `Accordion`, or any
element that explicitly sets `data-bg`). When no such ancestor exists it falls
back to `--bui-bg-app`. Use it from CSS when a sticky, fixed, or otherwise
overlapping element needs to match its surrounding bg without hardcoding a level.
<CodeBlock
code={`.searchBarContainer {
position: sticky;
top: 0;
background-color: var(--bui-bg-inherit);
}`}
/>
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>--bui-bg-inherit</Chip>
</Table.Cell>
<Table.Cell>
Resolves to the bg color of the nearest enclosing `data-bg` ancestor,
falling back to `--bui-bg-app`.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
## Solid background colors
<Table.Root>
+7 -1
View File
@@ -4,12 +4,18 @@ import styles from './styles.module.css';
export const Chip = ({
children,
head = false,
deprecated = false,
}: {
children: ReactNode;
head?: boolean;
deprecated?: boolean;
}) => {
return (
<span className={`${styles.chip} ${head ? styles.head : ''}`}>
<span
className={`${styles.chip} ${head ? styles.head : ''} ${
deprecated ? styles.deprecated : ''
}`}
>
{children}
</span>
);
@@ -14,6 +14,11 @@
color: #2563eb;
}
.deprecated {
background-color: #fff4e5;
color: #b45309;
}
[data-theme-mode='dark'] .chip {
background-color: #2c2c2c;
color: #fff;
@@ -22,3 +27,8 @@
[data-theme-mode='dark'] .chip.head {
background-color: #33405b;
}
[data-theme-mode='dark'] .chip.deprecated {
background-color: #3d2a10;
color: #fbbf24;
}
@@ -52,7 +52,12 @@ export const PropsTable = <T extends Record<string, PropData>>({
switch (column) {
case 'prop':
return <Chip head>{propName}</Chip>;
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}>
<Chip head>{propName}</Chip>
{propData.deprecated && <Chip deprecated>deprecated</Chip>}
</div>
);
case 'type':
return (
+9
View File
@@ -49,10 +49,19 @@ export const components: Page[] = [
title: 'CheckboxGroup',
slug: 'checkbox-group',
},
{
title: 'Combobox',
slug: 'combobox',
status: 'new',
},
{
title: 'Container',
slug: 'container',
},
{
title: 'DatePicker',
slug: 'date-picker',
},
{
title: 'DateRangePicker',
slug: 'date-range-picker',
+1
View File
@@ -44,6 +44,7 @@ export type PropDef = {
required?: boolean;
responsive?: boolean;
description?: ReactNode;
deprecated?: boolean;
};
export { breakpoints };
+96 -96
View File
@@ -314,12 +314,12 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.1, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0":
version: 6.5.2
resolution: "@codemirror/state@npm:6.5.2"
"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.1, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.6.0":
version: 6.6.0
resolution: "@codemirror/state@npm:6.6.0"
dependencies:
"@marijn/find-cluster-break": "npm:^1.0.0"
checksum: 10/5ccd3acb0c0a5b88e83fb91be39099fceb9f44a5047cc41a75d53f160e736851f65c8de40950b90c6519e6d2828e12f468db0af658dde30e938896f1c39eec91
checksum: 10/5d624f3c8832b287d76ebb5f57c01327641875c12c58ada2a97f958dc4df8c3bb0a1ad08ed370300a4a929ee8d5c7f14a397449a0d075ac3129d60d85f077441
languageName: node
linkType: hard
@@ -336,14 +336,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.39.16
resolution: "@codemirror/view@npm:6.39.16"
version: 6.41.1
resolution: "@codemirror/view@npm:6.41.1"
dependencies:
"@codemirror/state": "npm:^6.5.0"
"@codemirror/state": "npm:^6.6.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/199576febda2a91fe7676b8708627ed2e38d7e964ec8258331422fe7c7f89003eee2de7dec828e09c046de005742fd476cae6ceebc7bd994744f771253bfcbf3
checksum: 10/ab8156db1012f94ac39d603da4c397ab1de8877f941d0cf67f79cd09fffe9f39d5de8a10611a788ce9d5a88cad2633445880955fd0dc1ad67813cbf9be97774a
languageName: node
linkType: hard
@@ -928,10 +928,10 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:16.2.1":
version: 16.2.1
resolution: "@next/env@npm:16.2.1"
checksum: 10/c4f19f1767d7a1e8e9ff93cdee7e3b6a923d26d9d71f44124a797f03703ab9a18508b5ede997cc99d0307f2e0d0d1c426e9673a6c11ea10e170b87462a572236
"@next/env@npm:16.2.3":
version: 16.2.3
resolution: "@next/env@npm:16.2.3"
checksum: 10/30ed128d8ffae47e58732ee134b78da36e2d6942da7479ec5e640d205b7822224daf2f07d7a69352dc362908eb260fc9fa7eaba1ce5e6311abeacc6ffb0fe90a
languageName: node
linkType: hard
@@ -961,58 +961,58 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-darwin-arm64@npm:16.2.1"
"@next/swc-darwin-arm64@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-darwin-arm64@npm:16.2.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-darwin-x64@npm:16.2.1"
"@next/swc-darwin-x64@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-darwin-x64@npm:16.2.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-linux-arm64-gnu@npm:16.2.1"
"@next/swc-linux-arm64-gnu@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-linux-arm64-gnu@npm:16.2.3"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-linux-arm64-musl@npm:16.2.1"
"@next/swc-linux-arm64-musl@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-linux-arm64-musl@npm:16.2.3"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-linux-x64-gnu@npm:16.2.1"
"@next/swc-linux-x64-gnu@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-linux-x64-gnu@npm:16.2.3"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-linux-x64-musl@npm:16.2.1"
"@next/swc-linux-x64-musl@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-linux-x64-musl@npm:16.2.3"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-win32-arm64-msvc@npm:16.2.1"
"@next/swc-win32-arm64-msvc@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-win32-arm64-msvc@npm:16.2.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:16.2.1":
version: 16.2.1
resolution: "@next/swc-win32-x64-msvc@npm:16.2.1"
"@next/swc-win32-x64-msvc@npm:16.2.3":
version: 16.2.3
resolution: "@next/swc-win32-x64-msvc@npm:16.2.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -1175,12 +1175,12 @@ __metadata:
languageName: node
linkType: hard
"@remixicon/react@npm:^4.6.0":
version: 4.9.0
resolution: "@remixicon/react@npm:4.9.0"
"@remixicon/react@npm:>=4.6.0 <4.9.0":
version: 4.8.0
resolution: "@remixicon/react@npm:4.8.0"
peerDependencies:
react: ">=18.2.0"
checksum: 10/3d8f1d86b2bb20ab5e44d15f18811e928b0886f7710eb7a1516afb9913ba72e46facec5dfee382825139d800bcbb6704c15d0c760d0f977c12257d4af8db3295
checksum: 10/10241f2e07826ce7d595cf9687a35c96cc9cf9eb68a9431d7dfa1b9df479dbef302874d28c0502cee2a536cf34722de7c49ed12d10a2b071e4e42ba4a278c516
languageName: node
linkType: hard
@@ -1534,9 +1534,9 @@ __metadata:
languageName: node
linkType: hard
"@uiw/codemirror-extensions-basic-setup@npm:4.25.8":
version: 4.25.8
resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.8"
"@uiw/codemirror-extensions-basic-setup@npm:4.25.9":
version: 4.25.9
resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.9"
dependencies:
"@codemirror/autocomplete": "npm:^6.0.0"
"@codemirror/commands": "npm:^6.0.0"
@@ -1553,13 +1553,13 @@ __metadata:
"@codemirror/search": ">=6.0.0"
"@codemirror/state": ">=6.0.0"
"@codemirror/view": ">=6.0.0"
checksum: 10/a8d83465f9f3393b6e95d98ae7f3616ad57f819bce64224831d3db19647524538fc013973074a63551afa69daad9a8ab05f2e5c7441db7e30e722495d7e991d3
checksum: 10/bab06a40bdd8fb99a0af5115511cdb812c93aac2b93ccd8a02bdf8ea06098d6be2ce1302efc560a3b577171a5cd87d34c3215e523f21450ba100b147dfcb975c
languageName: node
linkType: hard
"@uiw/codemirror-themes@npm:^4.23.7":
version: 4.25.8
resolution: "@uiw/codemirror-themes@npm:4.25.8"
version: 4.25.9
resolution: "@uiw/codemirror-themes@npm:4.25.9"
dependencies:
"@codemirror/language": "npm:^6.0.0"
"@codemirror/state": "npm:^6.0.0"
@@ -1568,19 +1568,19 @@ __metadata:
"@codemirror/language": ">=6.0.0"
"@codemirror/state": ">=6.0.0"
"@codemirror/view": ">=6.0.0"
checksum: 10/e9983b0f6e663ca200d36437b6b52b4061ce5ccefece6f738b15370a8a7ac6774e7139a82e9e28ae273692e25d0c0804693587ea0967e163a1c7ac8cf3859cd1
checksum: 10/4a4fe7ae5f6c2bd37170b46c75ccabb67b47b7d3cee0de45c63fafe4f2b7569461b009e0ff5386480574e651627ed03c077833d53b6d3391102b79107ae39d15
languageName: node
linkType: hard
"@uiw/react-codemirror@npm:^4.23.7":
version: 4.25.8
resolution: "@uiw/react-codemirror@npm:4.25.8"
version: 4.25.9
resolution: "@uiw/react-codemirror@npm:4.25.9"
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.8"
"@uiw/codemirror-extensions-basic-setup": "npm:4.25.9"
codemirror: "npm:^6.0.0"
peerDependencies:
"@babel/runtime": ">=7.11.0"
@@ -1590,7 +1590,7 @@ __metadata:
codemirror: ">=6.0.0"
react: ">=17.0.0"
react-dom: ">=17.0.0"
checksum: 10/8c974e22dad1ad6231f33f7db42cd5b68caaf9bea545539b06b8a89dda3427eebadf47c8f48ee0d74cdf5a25000a8fcc02bac9fe560b624955eedf1f9bb47a85
checksum: 10/02c6ababa9307cf10aee5b32db1a0e5885485960819d708cd154bc218cf4d8b182b5fb49fa7386014401d71c8391ce211b29b2de201ad1fdece2c1716d09a74d
languageName: node
linkType: hard
@@ -2338,7 +2338,7 @@ __metadata:
"@mdx-js/react": "npm:^3.1.0"
"@next/mdx": "npm:16.2.1"
"@octokit/rest": "npm:^22.0.1"
"@remixicon/react": "npm:^4.6.0"
"@remixicon/react": "npm:>=4.6.0 <4.9.0"
"@shikijs/transformers": "npm:^3.13.0"
"@types/mdx": "npm:^2.0.13"
"@types/node": "npm:^22.13.14"
@@ -2351,9 +2351,9 @@ __metadata:
eslint-config-next: "npm:16.2.1"
html-react-parser: "npm:^5.2.5"
motion: "npm:^12.4.1"
next: "npm:16.2.1"
next: "npm:16.2.3"
next-mdx-remote-client: "npm:^2.1.2"
postcss: "npm:^8.5.6"
postcss: "npm:^8.5.10"
postcss-import: "npm:^16.1.1"
prop-types: "npm:^15.8.1"
react: "npm:19.2.4"
@@ -3103,12 +3103,12 @@ __metadata:
languageName: node
linkType: hard
"framer-motion@npm:^12.35.2":
version: 12.35.2
resolution: "framer-motion@npm:12.35.2"
"framer-motion@npm:^12.38.0":
version: 12.38.0
resolution: "framer-motion@npm:12.38.0"
dependencies:
motion-dom: "npm:^12.35.2"
motion-utils: "npm:^12.29.2"
motion-dom: "npm:^12.38.0"
motion-utils: "npm:^12.36.0"
tslib: "npm:^2.4.0"
peerDependencies:
"@emotion/is-prop-valid": "*"
@@ -3121,7 +3121,7 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 10/10af699ff1e35a166ef60ceab464479b81624ef74de3ec9e11b427f86bd7bf2c8c8a9f24fb0646288b2d4b0c1b219203da351821fc568c7b91c6821594af4a3f
checksum: 10/4d529d1648a8e31ec9859e7ff1296b7e4ef0028eb09cbc7d626068766ab53e486038b431fac33b1438a1cc076a244e6843c5a8c0f38442885832308452b4b25e
languageName: node
linkType: hard
@@ -4500,27 +4500,27 @@ __metadata:
languageName: node
linkType: hard
"motion-dom@npm:^12.35.2":
version: 12.35.2
resolution: "motion-dom@npm:12.35.2"
"motion-dom@npm:^12.38.0":
version: 12.38.0
resolution: "motion-dom@npm:12.38.0"
dependencies:
motion-utils: "npm:^12.29.2"
checksum: 10/dd009e58b178dd80b123a86199ae78ecd6b2fc6c8e03464b2daf43b4218dfcc36042ec0af8fad2c6c157198f56849f90dc033b58f46478b45fbaeaefcc2710ad
motion-utils: "npm:^12.36.0"
checksum: 10/78c040b46d93273932cf80c70e39845be5a442dcaf18d4345b45a9193de9dfa87c885b609943cb652115e4eac5d46ef40b452185073dd43fc328b134f9975e90
languageName: node
linkType: hard
"motion-utils@npm:^12.29.2":
version: 12.29.2
resolution: "motion-utils@npm:12.29.2"
checksum: 10/ae5f9be58c07939af72334894ed1a18653d724946182a718dc3d11268ef26e63804c3f16dee5a6110596d4406b539c4513822b74f86adebef9488601c34b18b7
"motion-utils@npm:^12.36.0":
version: 12.36.0
resolution: "motion-utils@npm:12.36.0"
checksum: 10/c4a2a7ffac48ca44082d6d31b115f245025060a7e69d70dac062646d8f96c39e5662a7c8a51f255566fdf8e719ef1269a8e9aa3a04fc263bb65b5a7b61331901
languageName: node
linkType: hard
"motion@npm:^12.4.1":
version: 12.35.2
resolution: "motion@npm:12.35.2"
version: 12.38.0
resolution: "motion@npm:12.38.0"
dependencies:
framer-motion: "npm:^12.35.2"
framer-motion: "npm:^12.38.0"
tslib: "npm:^2.4.0"
peerDependencies:
"@emotion/is-prop-valid": "*"
@@ -4533,7 +4533,7 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 10/3d99a53816634cbee1b38ed8a9a5d88bafbd29eb3bc02e78fc741c604972b4b88d317cf374bba30a1486f727bb1657ef8826f83e669a3b04fd1ec3ef75bfb62d
checksum: 10/d7ae2ba3cc112c4467822956b92065239640b9c62204d3bee1780da9fc0147185373534138d39975e82bf73b5f1b28d3fb3581031e4e7e0cfb230472767bd10d
languageName: node
linkType: hard
@@ -4587,19 +4587,19 @@ __metadata:
languageName: node
linkType: hard
"next@npm:16.2.1":
version: 16.2.1
resolution: "next@npm:16.2.1"
"next@npm:16.2.3":
version: 16.2.3
resolution: "next@npm:16.2.3"
dependencies:
"@next/env": "npm:16.2.1"
"@next/swc-darwin-arm64": "npm:16.2.1"
"@next/swc-darwin-x64": "npm:16.2.1"
"@next/swc-linux-arm64-gnu": "npm:16.2.1"
"@next/swc-linux-arm64-musl": "npm:16.2.1"
"@next/swc-linux-x64-gnu": "npm:16.2.1"
"@next/swc-linux-x64-musl": "npm:16.2.1"
"@next/swc-win32-arm64-msvc": "npm:16.2.1"
"@next/swc-win32-x64-msvc": "npm:16.2.1"
"@next/env": "npm:16.2.3"
"@next/swc-darwin-arm64": "npm:16.2.3"
"@next/swc-darwin-x64": "npm:16.2.3"
"@next/swc-linux-arm64-gnu": "npm:16.2.3"
"@next/swc-linux-arm64-musl": "npm:16.2.3"
"@next/swc-linux-x64-gnu": "npm:16.2.3"
"@next/swc-linux-x64-musl": "npm:16.2.3"
"@next/swc-win32-arm64-msvc": "npm:16.2.3"
"@next/swc-win32-x64-msvc": "npm:16.2.3"
"@swc/helpers": "npm:0.5.15"
baseline-browser-mapping: "npm:^2.9.19"
caniuse-lite: "npm:^1.0.30001579"
@@ -4643,7 +4643,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: 10/319c0b18173a90e53b5e5ffafa8a8fecb7cc340b77728796743edd996c7ee7652201892bff60c32f6a3b75abdff1449b77f13f6fab8fd56d4f9da47cf0fb9299
checksum: 10/5164885daacbb36a771380e1b5efba524863e1bdf2b5a6c80413cbf1e3ab4e8ddab5716cd91ff94ca5c5c5deb2a12d1312d6d6ae994e16ebfa985fdda6134bc6
languageName: node
linkType: hard
@@ -4857,9 +4857,9 @@ __metadata:
linkType: hard
"picomatch@npm:^2.3.1":
version: 2.3.1
resolution: "picomatch@npm:2.3.1"
checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc
version: 2.3.2
resolution: "picomatch@npm:2.3.2"
checksum: 10/b788ef8148a2415b9dec12f0bb350ae6a5830f8f1950e472abc2f5225494debf7d1b75eb031df0ceaea9e8ec3e7bad599e8dbf3c60d61b42be429ba41bff4426
languageName: node
linkType: hard
@@ -4915,14 +4915,14 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:^8.5.6":
version: 8.5.8
resolution: "postcss@npm:8.5.8"
"postcss@npm:^8.5.10":
version: 8.5.10
resolution: "postcss@npm:8.5.10"
dependencies:
nanoid: "npm:^3.3.11"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459
checksum: 10/7eac6169e535b63c8412e94d4f6047fc23efa3e9dde804b541940043c831b25f1cd867d83cd2c4371ad2450c8abcb42c208aa25668c1f0f3650d7f72faf711a8
languageName: node
linkType: hard
@@ -6241,11 +6241,11 @@ __metadata:
linkType: hard
"yaml@npm:^2.0.0":
version: 2.8.1
resolution: "yaml@npm:2.8.1"
version: 2.8.3
resolution: "yaml@npm:2.8.3"
bin:
yaml: bin.mjs
checksum: 10/eae07b3947d405012672ec17ce27348aea7d1fa0534143355d24a43a58f5e05652157ea2182c4fe0604f0540be71f99f1173f9d61018379404507790dff17665
checksum: 10/ecad41d39d34fae5cc17ea2d4b7f7f55faacd45cbce8983ba22d48d1ed1a92ed242ea49ea813a79ac39a69f75f9c5a03e7b5395fd954d55476f25e21a47c141d
languageName: node
linkType: hard
@@ -60,7 +60,7 @@ Even with feature discovery enabled, you can disable specific extensions via con
app:
extensions:
- page:techdocs: false
- nav-item:search: false
- page:search: false
```
### How Discovery Works with Manual Imports
+5
View File
@@ -19,6 +19,11 @@
"name": "plugin-full-frontend-system-migration",
"description": "Fully migrate a Backstage plugin to the new frontend system, dropping all old system support. Use this skill for internal plugins that only need to run in a single app, or when you are ready to remove backward compatibility entirely.",
"files": ["SKILL.md"]
},
{
"name": "plugin-analytics-instrumentation",
"description": "Instrument a Backstage frontend plugin with analytics events using the Backstage Analytics API. Use this skill when adding, reviewing, or extending event capture (captureEvent, AnalyticsContext) in plugin components, deciding whether an interaction warrants an event, or writing tests for analytics behavior.",
"files": ["SKILL.md"]
}
]
}
@@ -5,8 +5,8 @@ description: Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (B
# MUI to BUI Migration Skill
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to Backstage UI (
@backstage/ui).
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to
Backstage UI (@backstage/ui).
## Prerequisites
@@ -19,6 +19,7 @@ Before starting migration:
```
2. Add the CSS import to your root file (typically `src/index.ts` or app entry point):
```typescript
import '@backstage/ui/css/styles.css';
```
@@ -38,11 +39,14 @@ Before starting migration:
- `Accordion` - Collapsible content panels (`Accordion`, `AccordionTrigger`, `AccordionPanel`, `AccordionGroup`)
- `Alert` - Alert/notification banners (`status`, `title`, `description`)
- `Avatar` - User/entity avatars
- `Badge` - Inline badge/label with optional icon (`size`, `icon`)
- `Button` - Action buttons (`variant="primary"`, `variant="secondary"`, `variant="tertiary"`, `isDisabled`, `destructive`, `loading`)
- `ButtonIcon` - Icon-only buttons (`icon`, `onPress`, `variant`)
- `ButtonLink` - Link styled as button
- `Card` - Content cards (`Card`, `CardHeader`, `CardBody`, `CardFooter`)
- `Checkbox` - Checkbox input
- `CheckboxGroup` - Grouped checkboxes with shared label (`label`, `orientation`, `isRequired`)
- `DateRangePicker` - Date range input field (`label`, `value`, `onChange`)
- `Dialog` - Modal dialogs (`DialogTrigger`, `Dialog`, `DialogHeader`, `DialogBody`, `DialogFooter`)
- `FieldLabel` - Form field label with description and secondary label
- `Header` - Page headers with breadcrumbs and tabs
@@ -57,6 +61,7 @@ Before starting migration:
- `SearchField` - Search input
- `Select` - Dropdown select (single and multiple selection modes)
- `Skeleton` - Loading skeleton
- `Slider` - Range slider input (`label`, `minValue`, `maxValue`, `step`)
- `Switch` - Toggle switch
- `Table` - Data tables (with `useTable` hook for data management)
- `TablePagination` - Standalone pagination component
@@ -103,9 +108,9 @@ Create a `.module.css` file alongside your component using BUI CSS variables.
**Before (MUI `makeStyles`):**
```typescript
```tsx
// MyComponent.tsx
import {makeStyles, Theme} from '@material-ui/core/styles';
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
container: {
@@ -130,18 +135,16 @@ const useStyles = makeStyles((theme: Theme) => ({
function MyComponent() {
const classes = useStyles();
return (
<div className = {classes.container} >
<Typography className = {classes.title} > Title < /Typography>
< div
className = {classes.listItem} >
<div className = {classes.icon} >
<SomeIcon / >
<div className={classes.container}>
<Typography className={classes.title}>Title</Typography>
<div className={classes.listItem}>
<div className={classes.icon}>
<SomeIcon />
</div>
<span>Content</span>
</div>
</div>
< span > Content < /span>
< /div>
< /div>
)
;
);
}
```
@@ -177,27 +180,24 @@ function MyComponent() {
}
```
```typescript
```tsx
// MyComponent.tsx
import {Box, Text} from '@backstage/ui';
import {RiSomeIcon} from '@remixicon/react';
import { Box, Text } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
import styles from './MyComponent.module.css';
function MyComponent() {
return (
<Box className = {styles.container} >
<Text className = {styles.title} > Title < /Text>
< div
className = {styles.listItem} >
<div className = {styles.icon} >
<RiSomeIcon size = {24}
/>
< /div>
< span > Content < /span>
< /div>
< /Box>
)
;
<Box className={styles.container}>
<Text className={styles.title}>Title</Text>
<div className={styles.listItem}>
<div className={styles.icon}>
<RiSomeIcon size={24} />
</div>
<span>Content</span>
</div>
</Box>
);
}
```
@@ -205,38 +205,27 @@ function MyComponent() {
**Before (MUI Box with display prop):**
```typescript
```tsx
<Box
display = "flex"
flexDirection = "column"
alignItems = "center"
justifyContent = "space-between"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="space-between"
>
<Box display = "flex"
flexDirection = "row"
gap = {2} >
{children}
< /Box>
< /Box>
<Box display="flex" flexDirection="row" gap={2}>
{children}
</Box>
</Box>
```
**After (BUI `Flex` component):**
```typescript
<Flex direction = "column"
align = "center"
justify = "between" >
<Flex direction = "row"
style = {
{
gap: 'var(--bui-space-4)'
}
}>
{
children
}
```tsx
<Flex direction="column" align="center" justify="between">
<Flex direction="row" style={{ gap: 'var(--bui-space-4)' }}>
{children}
</Flex>
</Flex>
< /Flex>
```
Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
@@ -245,70 +234,40 @@ Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
**Before (MUI Grid):**
```typescript
<Grid container
spacing = {3} >
<Grid item
xs = {12}
md = {6} >
{content}
< /Grid>
< /Grid>
```tsx
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
{content}
</Grid>
</Grid>
```
**After (BUI Grid):**
```typescript
<Grid.Root columns = {
{
sm: '12'
}
}
gap = "6" >
<Grid.Item colSpan = {
{
sm: '12', md
:
'6'
}
}>
{
content
}
</Grid.Item>
< /Grid.Root>
```tsx
<Grid.Root columns={{ sm: '12' }} gap="6">
<Grid.Item colSpan={{ sm: '12', md: '6' }}>{content}</Grid.Item>
</Grid.Root>
```
### 5. Typography to Text
**Before (MUI Typography):**
```typescript
<Typography variant = "h1" > Heading < /Typography>
< Typography
variant = "h6" > Subheading < /Typography>
< Typography
variant = "body1" > Body
text < /Typography>
< Typography
variant = "body2"
color = "textSecondary" > Secondary
text < /Typography>
```tsx
<Typography variant="h1">Heading</Typography>
<Typography variant="h6">Subheading</Typography>
<Typography variant="body1">Body text</Typography>
<Typography variant="body2" color="textSecondary">Secondary text</Typography>
```
**After (BUI Text):**
```typescript
<Text variant = "title-large" > Heading < /Text>
< Text
variant = "title-small" > Subheading < /Text>
< Text
variant = "body-medium" > Body
text < /Text>
< Text
variant = "body-small"
color = "secondary" > Secondary
text < /Text>
```tsx
<Text variant="title-large">Heading</Text>
<Text variant="title-small">Subheading</Text>
<Text variant="body-medium">Body text</Text>
<Text variant="body-small" color="secondary">Secondary text</Text>
```
Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-small`, `body-large`, `body-medium`,
@@ -318,19 +277,17 @@ Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-smal
**Before (MUI Tooltip):**
```typescript
import {Tooltip, Typography} from '@material-ui/core';
```tsx
import { Tooltip, Typography } from '@material-ui/core';
<Tooltip title = { < Typography > Tooltip
content < /Typography>}>
< span > Hover
me < /span>
< /Tooltip>;
<Tooltip title={<Typography>Tooltip content</Typography>}>
<span>Hover me</span>
</Tooltip>;
```
**After (BUI TooltipTrigger pattern):**
```typescript
```tsx
import { Tooltip, TooltipTrigger, Text } from '@backstage/ui';
<TooltipTrigger>
@@ -343,26 +300,23 @@ import { Tooltip, TooltipTrigger, Text } from '@backstage/ui';
**Before (MUI Dialog):**
```typescript
import {Dialog, DialogTitle, DialogActions, Button} from '@material-ui/core';
```tsx
import { Dialog, DialogTitle, DialogActions, Button } from '@material-ui/core';
<Dialog open = {isOpen}
onClose = {onClose} >
<DialogTitle>Title < /DialogTitle>
< DialogActions >
<Button onClick = {onClose} > Cancel < /Button>
< Button
onClick = {onConfirm}
color = "primary" >
Confirm
< /Button>
< /DialogActions>
< /Dialog>;
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>Title</DialogTitle>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onConfirm} color="primary">
Confirm
</Button>
</DialogActions>
</Dialog>;
```
**After (BUI Dialog):**
```typescript
```tsx
import {
Dialog,
DialogTrigger,
@@ -373,78 +327,58 @@ import {
<DialogTrigger>
<Dialog
isOpen = {isOpen}
isDismissable
onOpenChange = {open
=>
{
if (!open) onClose();
}
}
>
<DialogHeader>Title < /DialogHeader>
< DialogFooter >
<Button onClick = {onConfirm}
variant = "primary" >
Confirm
< /Button>
< Button
onClick = {onClose}
variant = "secondary"
slot = "close" >
Cancel
< /Button>
< /DialogFooter>
< /Dialog>
< /DialogTrigger>;
isOpen={isOpen}
isDismissable
onOpenChange={open => {
if (!open) onClose();
}}
>
<DialogHeader>Title</DialogHeader>
<DialogFooter>
<Button onClick={onConfirm} variant="primary">
Confirm
</Button>
<Button onClick={onClose} variant="secondary" slot="close">
Cancel
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>;
```
### 8. Button Changes
**Before (MUI Button):**
```typescript
<Button variant = "contained"
color = "primary"
disabled = {loading}
onClick = {handleClick} >
```tsx
<Button variant="contained" color="primary" disabled={loading} onClick={handleClick}>
Submit
< /Button>
< IconButton
onClick = {handleDelete}
disabled = {!
canDelete
}>
<DeleteIcon / >
</Button>
<IconButton onClick={handleDelete} disabled={!canDelete}>
<DeleteIcon />
</IconButton>
```
**After (BUI Button):**
```typescript
<Button variant = "primary"
isDisabled = {loading}
onClick = {handleClick} >
```tsx
<Button variant="primary" isDisabled={loading} onClick={handleClick}>
Submit
< /Button>
< ButtonIcon
aria - label = "delete"
isDisabled = {!
canDelete
}
onPress = {handleDelete}
icon = { < RiDeleteBinLine
size = {16}
/>}
variant = "secondary"
/ >
</Button>
<ButtonIcon
aria-label="delete"
isDisabled={!canDelete}
onPress={handleDelete}
icon={<RiDeleteBinLine size={16} />}
variant="secondary"
/>
```
### 9. TextField Changes
**Before (MUI TextField):**
```typescript
```tsx
<TextField
required
name="title"
@@ -457,7 +391,7 @@ variant = "secondary"
**After (BUI TextField):**
```typescript
```tsx
<TextField
isRequired
id="title"
@@ -473,89 +407,72 @@ Note: BUI TextField `onChange` receives the string value directly, not an event
**Before (MUI Tabs):**
```typescript
import {Tab} from '@material-ui/core';
import {TabContext, TabList, TabPanel} from '@material-ui/lab';
```tsx
import { Tab } from '@material-ui/core';
import { TabContext, TabList, TabPanel } from '@material-ui/lab';
<TabContext value = {tab} >
<TabList onChange = {handleChange} >
<Tab label = "Tab 1"
value = "tab1" / >
<Tab label = "Tab 2"
value = "tab2" / >
<TabContext value={tab}>
<TabList onChange={handleChange}>
<Tab label="Tab 1" value="tab1" />
<Tab label="Tab 2" value="tab2" />
</TabList>
< TabPanel
value = "tab1" > Content
1 < /TabPanel>
< TabPanel
value = "tab2" > Content
2 < /TabPanel>
< /TabContext>;
<TabPanel value="tab1">Content 1</TabPanel>
<TabPanel value="tab2">Content 2</TabPanel>
</TabContext>;
```
**After (BUI Tabs):**
```typescript
import {Tabs, TabList, Tab, TabPanel} from '@backstage/ui';
```tsx
import { Tabs, TabList, Tab, TabPanel } from '@backstage/ui';
<Tabs defaultSelectedKey = "tab1" >
<TabList>
<Tab id = "tab1" > Tab
1 < /Tab>
< Tab
id = "tab2" > Tab
2 < /Tab>
< /TabList>
< TabPanel
id = "tab1" > Content
1 < /TabPanel>
< TabPanel
id = "tab2" > Content
2 < /TabPanel>
< /Tabs>;
<Tabs defaultSelectedKey="tab1">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
</TabList>
<TabPanel id="tab1">Content 1</TabPanel>
<TabPanel id="tab2">Content 2</TabPanel>
</Tabs>;
```
### 11. Menu Pattern
**Before (MUI Menu):**
```typescript
```tsx
import {IconButton, Popover, MenuList, MenuItem} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
<IconButton onClick = {handleOpen} > <MoreVertIcon / > </IconButton>
< Popover
open = {open}
anchorEl = {anchorEl}
onClose = {handleClose} >
<MenuList>
<MenuItem onClick = {handleAction} > Action < /MenuItem>
< /MenuList>
< /Popover>
<IconButton onClick={handleOpen}>
<MoreVertIcon />
</IconButton>
<Popover open={open} anchorEl={anchorEl} onClose={handleClose}>
<MenuList>
<MenuItem onClick={handleAction}>Action</MenuItem>
</MenuList>
</Popover>
```
**After (BUI Menu):**
```typescript
import {ButtonIcon, Menu, MenuItem, MenuTrigger} from '@backstage/ui';
import {RiMore2Line} from '@remixicon/react';
```tsx
import { ButtonIcon, Menu, MenuItem, MenuTrigger } from '@backstage/ui';
import { RiMore2Line } from '@remixicon/react';
<MenuTrigger>
<ButtonIcon aria - label = "more"
icon = { < RiMore2Line / >
}
variant = "secondary" / >
<Menu>
<MenuItem onAction = {handleAction} > Action < /MenuItem>
< /Menu>
< /MenuTrigger>;
<ButtonIcon aria-label="more" icon={<RiMore2Line />} variant="secondary" />
<Menu>
<MenuItem onAction={handleAction}>Action</MenuItem>
</Menu>
</MenuTrigger>;
```
### 12. List to BUI List
**Before (MUI List):**
```typescript
```tsx
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
<List>
@@ -570,7 +487,7 @@ import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
**After (BUI List):**
```typescript
```tsx
import { List, ListRow } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
@@ -587,7 +504,7 @@ Note: `ListRow` supports `icon`, `description`, `menuItems`, and `customActions`
**Before (MUI Chip):**
```typescript
```tsx
import { Chip } from '@material-ui/core';
<Chip label="Category" size="small" />;
@@ -595,17 +512,17 @@ import { Chip } from '@material-ui/core';
**After (BUI Tag):**
```typescript
import {Tag} from '@backstage/ui';
```tsx
import { Tag } from '@backstage/ui';
<Tag size = "small" > Category < /Tag>;
<Tag size="small">Category</Tag>;
```
### 14. Alert Pattern
**Before (MUI Alert):**
```typescript
```tsx
import { Alert, AlertTitle } from '@material-ui/lab';
<Alert severity="error">
@@ -616,7 +533,7 @@ import { Alert, AlertTitle } from '@material-ui/lab';
**After (BUI Alert):**
```typescript
```tsx
import { Alert } from '@backstage/ui';
<Alert
@@ -637,22 +554,21 @@ Use `loading` for a loading spinner, and `customActions` for action buttons.
**Before (MUI Icons):**
```typescript
```tsx
import CloseIcon from '@material-ui/icons/Close';
import SearchIcon from '@material-ui/icons/Search';
<CloseIcon / >
<SearchIcon fontSize = "small" / >
<CloseIcon />
<SearchIcon fontSize="small" />
```
**After (Remix Icons):**
```typescript
```tsx
import {RiCloseLine, RiSearchLine} from '@remixicon/react';
<RiCloseLine / >
<RiSearchLine size = {16}
/>
<RiCloseLine />
<RiSearchLine size={16} />
```
Common icon mappings:
@@ -683,6 +599,239 @@ Common icon mappings:
Find more icons at: https://remixicon.com/
### 16. Paper to Card
**Before (MUI Paper):**
```tsx
import { Paper, Typography } from '@material-ui/core';
<Paper elevation={2}>
<Typography variant="h6">Title</Typography>
<Typography>Body content</Typography>
</Paper>;
```
**After (BUI Card):**
```tsx
import { Card, CardHeader, CardBody, Text } from '@backstage/ui';
<Card>
<CardHeader>Title</CardHeader>
<CardBody>
<Text>Body content</Text>
</CardBody>
</Card>;
```
### 17. Select
**Before (MUI Select):**
```tsx
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
<FormControl fullWidth>
<InputLabel>Framework</InputLabel>
<Select value={value} onChange={e => setValue(e.target.value as string)}>
<MenuItem value="react">React</MenuItem>
<MenuItem value="angular">Angular</MenuItem>
</Select>
</FormControl>;
```
**After (BUI Select):**
```tsx
import { Select } from '@backstage/ui';
<Select
label="Framework"
selectedKey={value}
onSelectionChange={key => setValue(key as string)}
options={[
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' },
]}
/>;
```
Note: BUI `Select` accepts flat `options` arrays or grouped `OptionSection` arrays. Pass `multiple` for multi-select.
### 18. Accordion
**Before (MUI Accordion):**
```tsx
import {
Accordion,
AccordionSummary,
AccordionDetails,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
Section title
</AccordionSummary>
<AccordionDetails>Content goes here</AccordionDetails>
</Accordion>;
```
**After (BUI Accordion):**
```tsx
import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Section title" />
<AccordionPanel>Content goes here</AccordionPanel>
</Accordion>;
```
Use `AccordionGroup` to wrap multiple `Accordion` items and control whether multiple panels can be open simultaneously.
### 19. RadioGroup
**Before (MUI RadioGroup):**
```tsx
import {
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio,
} from '@material-ui/core';
<FormControl>
<FormLabel>Frequency</FormLabel>
<RadioGroup value={value} onChange={e => setValue(e.target.value)}>
<FormControlLabel value="daily" control={<Radio />} label="Daily" />
<FormControlLabel value="weekly" control={<Radio />} label="Weekly" />
</RadioGroup>
</FormControl>;
```
**After (BUI RadioGroup):**
```tsx
import { RadioGroup, Radio } from '@backstage/ui';
<RadioGroup label="Frequency" value={value} onChange={setValue}>
<Radio value="daily">Daily</Radio>
<Radio value="weekly">Weekly</Radio>
</RadioGroup>;
```
### 20. Badge
**Before (MUI Badge):**
```tsx
import { Badge } from '@material-ui/core';
<Badge badgeContent={4} color="primary">
<MailIcon />
</Badge>;
```
**After (BUI Badge):**
```tsx
import { Badge } from '@backstage/ui';
import { RiMailLine } from 'react-icons/ri';
<Badge>New</Badge>
<Badge size="small" icon={<RiMailLine size={12} />}>4</Badge>
```
Note: BUI `Badge` is a label-style badge (inline text with optional icon), not a notification counter overlay.
For notification counters overlaid on icons, use CSS positioning.
### 21. Slider
**Before (MUI Slider):**
```tsx
import { Slider } from '@material-ui/core';
<Slider
value={value}
onChange={(_, newValue) => setValue(newValue as number)}
min={0}
max={100}
step={10}
/>;
```
**After (BUI Slider):**
```tsx
import { Slider } from '@backstage/ui';
<Slider
label="Volume"
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={10}
/>;
```
Note: BUI `Slider` `onChange` receives the new value directly. Use `minValue`/`maxValue` instead of `min`/`max`.
### 22. CheckboxGroup
**Before (MUI FormGroup with Checkboxes):**
```tsx
import {
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from '@material-ui/core';
<FormControl>
<FormLabel>Options</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={values.a}
onChange={e => handleChange('a', e.target.checked)}
/>
}
label="Option A"
/>
<FormControlLabel
control={
<Checkbox
checked={values.b}
onChange={e => handleChange('b', e.target.checked)}
/>
}
label="Option B"
/>
</FormGroup>
</FormControl>;
```
**After (BUI CheckboxGroup):**
```tsx
import { CheckboxGroup, Checkbox } from '@backstage/ui';
<CheckboxGroup label="Options" value={selected} onChange={setSelected}>
<Checkbox value="a">Option A</Checkbox>
<Checkbox value="b">Option B</Checkbox>
</CheckboxGroup>;
```
## CSS Variable Reference
### Spacing
@@ -733,8 +882,7 @@ Find more icons at: https://remixicon.com/
Some Backstage APIs still require MUI-compatible icon types:
- **NavItemBlueprint** (`@backstage/frontend-plugin-api`): The `icon` prop expects MUI `IconComponent` type. Remix icons
are not type-compatible.
- **PageBlueprint** (`@backstage/frontend-plugin-api`): The `icon` param on page extensions expects an `IconElement`. MUI icon components can still be used via `<Icon fontSize="inherit" />`.
- **Timeline** (`@material-ui/lab`): No BUI equivalent exists.
For these cases, keep using MUI components.
@@ -758,18 +906,23 @@ When migrating a plugin:
13. [ ] Replace MUI `Dialog` with BUI `DialogTrigger` pattern
14. [ ] Replace MUI `Tooltip` with BUI `TooltipTrigger` pattern (both from `@backstage/ui`)
15. [ ] Replace MUI `Tabs` with BUI `Tabs`
16. [ ] Replace MUI `Menu` with BUI `MenuTrigger` pattern
16. [ ] Replace MUI `Menu`/`Popover` with BUI `MenuTrigger` pattern
17. [ ] Replace `Chip` with `Tag`
18. [ ] Replace `IconButton` with `ButtonIcon`
19. [ ] Replace MUI `Alert` with BUI `Alert`
20. [ ] Replace MUI `List` with BUI `List` and `ListRow`
21. [ ] Update `Button` props (`disabled``isDisabled`, `variant="contained"``variant="primary"`)
22. [ ] Update `TextField` props (`required``isRequired`, `onChange` signature)
23. [ ] Replace MUI icons with Remix icons
24. [ ] Run `yarn tsc` to check for type errors
25. [ ] Run `yarn build` to verify build
26. [ ] Run `yarn lint` to check for missing dependencies
27. [ ] Test component rendering and functionality
21. [ ] Replace MUI `Select`/`FormControl` with BUI `Select`
22. [ ] Replace MUI `Accordion` with BUI `Accordion`/`AccordionTrigger`/`AccordionPanel`
23. [ ] Replace MUI `RadioGroup`/`FormControlLabel` with BUI `RadioGroup`/`Radio`
24. [ ] Replace MUI `FormGroup` with BUI `CheckboxGroup`
25. [ ] Replace MUI `Slider` with BUI `Slider`
26. [ ] Update `Button` props (`disabled``isDisabled`, `variant="contained"``variant="primary"`)
27. [ ] Update `TextField` props (`required``isRequired`, `onChange` signature)
28. [ ] Replace MUI icons with Remix icons
29. [ ] Run `yarn tsc` to check for type errors
30. [ ] Run the project's build command (e.g. `yarn build`, `yarn build:all`, or `yarn workspace <pkg> build`) to verify build
31. [ ] Run `yarn lint` to check for missing dependencies
32. [ ] Test component rendering and functionality
## Reference
@@ -0,0 +1,185 @@
---
name: plugin-analytics-instrumentation
description: Instrument a Backstage frontend plugin with analytics events using the Backstage Analytics API. Use this skill when adding, reviewing, or extending event capture (`captureEvent`, `AnalyticsContext`) in plugin components, deciding whether an interaction warrants an event, or writing tests for analytics behavior.
---
# Plugin Analytics Instrumentation Skill
This skill helps you add analytics instrumentation to a Backstage frontend plugin so that app integrators can measure how the plugin is used.
## Guiding principles
Follow these before writing a single `captureEvent` call.
### 1. Less is more — instrument semantic events, not every interaction
Capture events that represent things **your plugin is semantically responsible for** — the domain actions only your plugin knows how to describe. Events should reflect **user intent** (something a person chose to do), not the lifecycle of your UI. Avoid instrumenting generic UI noise that the framework or design system already handles.
Good candidates for plugin-owned events:
- A domain verb only your plugin performs (`deploy`, `create`, `merge`, `approve`, `trigger`, `refresh`, `rerun`).
- An outcome you uniquely know about (a search returning N results, a scaffolder template saving Y minutes, a task transitioning to a terminal state).
- A context-carrying interaction where the attributes matter (clicking a search result with its `rank` and `to` target).
Poor candidates — avoid these:
- Routine clicks on navigation links, buttons, tabs, menu items — these are covered by the `navigate` event and by built-in instrumentation in `@backstage/ui` (see next principle).
- Low-value UI state toggles (expanding a panel, opening a tooltip, hovering).
- Every field edit in a form — usually one `submit`-style event at the end captures the intent.
- Component lifecycle signals — mounts, unmounts, re-renders, effect firings, data fetches. These describe the machinery of the UI, not the user, and will fire in plenty of contexts the user never initiated (route prefetches, Suspense boundaries, tab switches). Narrow exceptions exist for terminal states the user _lands on_ (e.g. `not-found`).
- Events whose `action` and `subject` duplicate what is already captured upstream.
If you can't answer the question _"what question does this event help someone answer?"_ in one sentence, it's probably best not to add the event.
### 2. Prefer `@backstage/ui` components — they already instrument clicks
Components from `@backstage/ui` (BUI) have built-in click instrumentation wired to the Analytics API. As of today this includes at least `Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, and `Table` row clicks. When these components are used for navigation (i.e. rendered with an `href`), a `click` event is fired with the destination included as a `to` attribute. For most of them the `subject` is a best-effort human-readable label — the `aria-label`, the visible text, or the `href` as a fallback. `Table` rows are the exception: their `subject` is the `href` string itself, not derived from visible row content.
Consequences:
- If you render a `Link`/`ButtonLink` from `@backstage/ui`, you do **not** need to add a `click` event by hand. Doing so would produce duplicate events.
- If a plain `<a>` or a MUI button handles a navigation or action that you care about analytically, migrate it to the BUI equivalent first (see the `mui-to-bui-migration` skill). You'll get the click event for free and can focus your manual instrumentation on plugin-specific actions.
- Manual `captureEvent('click', ...)` calls are reserved for cases where **no** BUI component fits — for example, clicks on a canvas, a custom widget, or a non-link element whose interaction needs tracking.
#### Overriding the default event with `noTrack`
Occasionally a BUI component is the right UI primitive but the default event it fires isn't the one you want — for example, the interaction has a domain-specific verb (`approve`, `rerun`) rather than a generic `click`, or the subject should be a stable identifier rather than the visible link text. In that case, pass `noTrack` to suppress the built-in event and fire your own from the click handler:
```tsx
import { Link } from '@backstage/ui';
import { useAnalytics } from '@backstage/frontend-plugin-api';
function ApproveLink({ requestId, href }: Props) {
const analytics = useAnalytics();
return (
<Link
noTrack
href={href}
onClick={() => analytics.captureEvent('approve', requestId)}
>
Approve
</Link>
);
}
```
Reach for `noTrack` only when you're **replacing** the default event, not layering a second event on top of it. If both the default `click` and your custom event are useful, the custom one probably belongs on a different component or in a different handler. `noTrack` is available on all BUI components with built-in instrumentation (`Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, and `Table` rows).
### 3. Split events so analysis stays flexible
An `AnalyticsEvent` has an `action`, a `subject`, and surrounding `context` (which is filled in with `pluginId` and `extension` automatically). Keep each dimension disaggregated so questions can be answered at any level of granularity.
- **Action** is the verb — kept generic and reused across plugins (`click`, `search`, `filter`, `create`, `discover`). Avoid squashing what belongs in context into the action (e.g. don't use `filterEntityTable` — use `filter` and let the `extension` / `AnalyticsContext` identify the table).
- **Subject** is the noun — the specific thing acted upon (a PR name, a template name, a search term, a result title).
- **Attributes** are optional key/value dimensions available at capture time (`to`, `org`, `repo`, `entityRef`).
- **Context** is for metadata coming from further up the React tree, or shared across many events in a region.
When in doubt about attribute naming, reuse what existing events in the repo use (e.g. `entityRef` for catalog entities, `to` for destinations, `searchTypes` for search). Consistency across plugins makes aggregation possible.
## How to capture an event
Get a tracker with `useAnalytics()` and call `captureEvent(action, subject, options?)`.
```tsx
import { useAnalytics } from '@backstage/frontend-plugin-api';
function DeployButton({ serviceName }: { serviceName: string }) {
const analytics = useAnalytics();
const handleDeploy = () => {
// ...perform the deploy
analytics.captureEvent('deploy', serviceName);
};
return <Button onClick={handleDeploy}>Deploy</Button>;
}
```
For old-system plugins, the same hook is re-exported from `@backstage/core-plugin-api`; the behavior is identical. New plugins targeting the new frontend system should import from `@backstage/frontend-plugin-api`.
### Adding `value` and `attributes`
`value` is a single numeric metric associated with the event (duration, rank, count). `attributes` are dimensional string/number/boolean pairs.
```tsx
analytics.captureEvent('merge', pullRequestName, {
value: pullRequestAgeInMinutes,
attributes: { org, repo },
});
```
Keep attributes flat and serializable. Don't stuff large objects or PII in here.
### Using `AnalyticsContext` for ambient metadata
When the same attribute applies to many events under a subtree — or when the metadata lives further up the tree than the component firing the event — wrap the subtree in an `<AnalyticsContext>` instead of passing props down:
```tsx
import { AnalyticsContext } from '@backstage/frontend-plugin-api';
function TaskPage({ taskId, entityRef }: Props) {
return (
<AnalyticsContext attributes={{ taskId, entityRef }}>
<TaskToolbar />
<TaskTimeline />
</AnalyticsContext>
);
}
```
Every `captureEvent` fired inside that subtree will have `taskId` and `entityRef` merged into its `context`. Contexts nest and merge; inner values override outer ones.
Good uses of `AnalyticsContext`:
- Page- or route-level attributes that apply to every interaction on that page (`entityRef`, `taskId`, a tab selection).
- Cross-cutting aggregation keys that let app integrators group events (`segment`, `workspace`).
Don't wrap every small component in its own context — prefer to set context once at the boundary where the metadata first becomes available.
## Unit testing event capture
Use `mockApis.analytics()` from `@backstage/frontend-test-utils` — it returns a mock `AnalyticsApi` implementation with a `getEvents()` helper for assertions. Prefer one thorough test with multiple assertions over many small ones.
```tsx
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { analyticsApiRef } from '@backstage/frontend-plugin-api';
import {
mockApis,
TestApiProvider,
wrapInTestApp,
} from '@backstage/frontend-test-utils';
it('captures a deploy event with the service name', async () => {
const analytics = mockApis.analytics();
render(
wrapInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<DeployButton serviceName="payments-api" />
</TestApiProvider>,
),
);
fireEvent.click(await screen.findByRole('button', { name: /deploy/i }));
await waitFor(() => {
expect(analytics.getEvents()[0]).toMatchObject({
action: 'deploy',
subject: 'payments-api',
});
});
});
```
Assert on `action`, `subject`, and any `attributes`/`value` you explicitly set. Don't assert on auto-populated context keys like `pluginId` — those are the framework's responsibility.
## Review checklist
Before submitting instrumentation changes:
1. [ ] Every new `captureEvent` call represents a **plugin-semantic, user-initiated** action (not a click already covered by BUI, a navigation, or a component-lifecycle trigger).
2. [ ] Route to a BUI component (`Link`, `ButtonLink`, `Tab`, `MenuItem`, `Tag`, `Table`) wherever one fits, rather than instrumenting a plain element by hand.
3. [ ] `action` is a short generic verb; plugin/extension identity is left to the auto-populated `context`.
4. [ ] Attribute keys reuse established conventions where applicable (`entityRef`, `to`, `searchTypes`, etc.).
5. [ ] Shared attributes are set via a single `<AnalyticsContext>` at a boundary, not duplicated across events.
6. [ ] `value` is numeric and meaningful (duration, rank, count) — not a stand-in for a string dimension.
7. [ ] No PII, secrets, tokens, or large serialized payloads in attributes.
8. [ ] At least one unit test covers each new event using `MockAnalyticsApi`.

Some files were not shown because too many files have changed in this diff Show More