Merge branch 'master' of https://github.com/backstage/backstage into fix/scaffolder-after-zero-query-param

This commit is contained in:
suhr25
2026-04-09 10:50:16 +05:30
842 changed files with 21947 additions and 6976 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added new `Badge` component for non-interactive labeling and categorization of content. It shares the visual appearance of `Tag` but renders as a plain DOM element with no interactive states.
**Affected components:** Badge
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/errors': minor
---
A new `toError` utility function is now available for converting unknown values to `ErrorLike` objects. If the value is already error-like it is returned as-is, strings are used directly as the error message, and all other values are wrapped as `unknown error '<stringified>'`. Non-error causes passed to `CustomErrorBase` are now converted and stored using `toError` rather than discarded.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-module-new': patch
---
Rename the legacy `frontend-plugin` to `frontend-plugin-legacy`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Refactored auditor severity log level mappings to use `zod/v4` with schema-driven defaults and type inference.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-node': patch
---
Added `OAuthAuthenticatorLogoutResult` type. The `logout` method on `OAuthAuthenticator` can now optionally return `{ logoutUrl }` to trigger a browser redirect after sign-out. This allows providers like Auth0 to clear their session cookies by redirecting to their logout endpoint.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-auth0-provider': patch
---
Added `createAuth0Authenticator` factory function that accepts a `CacheService` to cache Auth0 profile API responses for 1 minute during token refreshes. This avoids hitting Auth0 rate limits on repeated page refreshes. The module now uses the cached variant by default. The existing `auth0Authenticator` export remains available for use without caching.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend-module-auth0-provider': minor
---
Added federated logout support. Set `federatedLogout: true` in the Auth0 provider config to clear both the Auth0 session and any upstream IdP session on sign-out. The authenticator returns a logout URL that redirects the browser to Auth0's `/v2/logout?federated` endpoint, ensuring users must fully re-authenticate after signing out.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-azure': patch
---
Add Azure DevOps SCM event translation layer for instant catalog reprocessing.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
---
Added Bitbucket Cloud SCM event translation layer for the catalog backend module. The module now subscribes to Bitbucket Cloud webhook events and translates them into generic catalog SCM events, enabling instant catalog reprocessing when repositories are pushed to, renamed, transferred, or deleted. The `analyzeBitbucketCloudWebhookEvent` function is exported from the alpha entry point for custom integrations.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-mcp-actions-backend': patch
---
Fix OAuth 2.0 Protected Resource Metadata endpoint returning internal plugin URL, preventing some MCP clients like Claude Code from authenticating
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/theme': patch
---
Fixes occasional duplication of v5 class name prefix for MUI 5 components.
Documentation added to explain how to resolve missing v5 prefix in class names when using MUI 5 components in main app.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-catalog-backend': patch
'@backstage/cli-module-auth': patch
'@backstage/cli-module-build': patch
'@backstage/cli-module-test-jest': patch
'@backstage/cli': patch
'@backstage/ui': patch
'@backstage/repo-tools': patch
---
Bumped `glob` dependency from v7/v8/v11 to v13 to address security vulnerabilities in older versions. Bumped `rollup` from v4.27 to v4.59+ to fix a high severity path traversal vulnerability (GHSA-mw96-cpmx-2vgc).
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Added a `location_entity_ref` column to the `locations` database table that stores the full entity ref of the corresponding `kind: Location` catalog entity for each registered location row. The value is pre-computed and persisted so that it no longer needs to be recomputed from the location's type and target on every read.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Removed deprecated `PermissionAuthorizer` support and the `createPermissionIntegrationRouter` fallback path from `CatalogBuilder`. The `permissionsRegistry` service is now required, and `permissions` is always a `PermissionsService`.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Removed the internal `addPermissions` and `addPermissionRules` methods from `CatalogBuilder`, and removed the `catalogPermissionExtensionPoint` wiring from `CatalogPlugin`. Custom permission rules and permissions should be registered via `coreServices.permissionsRegistry` directly.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Exported the NFS variant of the catalog index page as `CatalogIndexPage` from the `./alpha` entry point, along with supporting types `CatalogIndexPageProps`, `CatalogTableRow`, and `CatalogTableColumnsFunc`. This allows adopters to use and customize the catalog index page within a `PageBlueprint` in the new frontend system.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-node': minor
---
**BREAKING ALPHA**: Removed the deprecated `CatalogPermissionRuleInput`, `CatalogPermissionExtensionPoint`, and `catalogPermissionExtensionPoint` exports. Use `coreServices.permissionsRegistry` directly to register catalog entity permission rules and permissions.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added automatic active tab detection to the Header component. When `activeTabId` is omitted, the active tab is now auto-detected from the current route using `matchRoutes`. Pass an explicit `activeTabId` to override, or `null` for no active tab.
**Affected components:** Header
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-events-backend-module-google-pubsub': patch
---
Migrated internal metrics in `GooglePubSubConsumingEventPublisher` and `EventConsumingGooglePubSubPublisher` to use the new alpha `MetricsService`
@@ -0,0 +1,6 @@
---
'@backstage/core-app-api': patch
'@backstage/plugin-app': patch
---
The default auth implementation now checks for a `logoutUrl` in the logout response body. If the auth provider returns one (e.g. Auth0 federated logout), the browser is redirected to that URL to clear the provider's session cookies. This is backward compatible — providers that return an empty response are unaffected.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Bumped create-app version.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Bumped create-app version.
+30
View File
@@ -0,0 +1,30 @@
---
'@backstage/backend-app-api': patch
'@backstage/backend-defaults': patch
'@backstage/cli': patch
'@backstage/cli-common': patch
'@backstage/cli-module-migrate': patch
'@backstage/cli-module-new': patch
'@backstage/cli-node': patch
'@backstage/config-loader': patch
'@backstage/core-components': patch
'@backstage/repo-tools': patch
'@backstage/plugin-auth': patch
'@backstage/plugin-auth-backend': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog-unprocessed-entities': patch
'@backstage/plugin-devtools-backend': patch
'@backstage/plugin-mcp-actions-backend': patch
'@backstage/plugin-notifications-backend-module-slack': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-backend-module-github': patch
'@backstage/plugin-search-backend-node': patch
'@backstage/plugin-techdocs-backend': patch
'@backstage/plugin-techdocs-node': patch
---
Migrated from `assertError` to `toError` for error handling.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Migrated OpenTelemetry metrics to use the `MetricsService` from `@backstage/backend-plugin-api/alpha` instead of the raw `@opentelemetry/api` meter.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Exported the `TableBodySkeleton` component as a public API for use outside of the built-in `Table` component. The component now accepts any column array whose items have an `id` property, making it compatible with custom column types.
@@ -0,0 +1,5 @@
---
'@backstage/cli-module-build': patch
---
Fixed the embedded-postgres PID file being written before database initialization, which prevented the database from initializing successfully.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-gitlab': patch
---
Add GitLab SCM event translation layer for instant catalog reprocessing.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/ui': minor
---
**BREAKING**: Tab `href` values in the Header component are now resolved through the router context instead of being passed raw to the `<a>` tag. This means relative `href` values (e.g. `sub3`, `./sub4`, `../catalog`) are now resolved against the current route, and absolute `href` values may be affected by the router's `basename` configuration.
**Migration:**
Tab navigation should work the same for absolute `href` values in most setups. If you use relative `href` values in tabs, verify they resolve as expected. If your app configures a router `basename`, check that absolute tab `href` values still navigate correctly.
**Affected components:** Header
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
`HostDiscovery` now logs a warning when `backend.baseUrl` is set to a localhost address while `NODE_ENV` is `production`, and when `backend.baseUrl` is not a valid URL.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Added Kind field to the About Card. Tags moved before Type and Lifecycle, Kind placed after them. A new `aboutCard.kindField.label` translation key was added.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
DefaultActionsRegistryService: add json middleware to /.backstage/actions/ routes only
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications': patch
---
Migrated notifications plugin to use backstage UI
@@ -0,0 +1,6 @@
---
'@backstage/plugin-search-backend-module-techdocs': patch
'@backstage/plugin-techdocs-backend': patch
---
Migrated internal usage of the deprecated `catalogServiceRef` from `@backstage/plugin-catalog-node/alpha` to the stable `catalogServiceRef` from `@backstage/plugin-catalog-node`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Add an initial CheckboxGroup component implementation and docs coverage.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-graph': patch
---
Support configuring `showArrowHeads` on `page:catalog-graph` and `entity-card:catalog-graph/relations`.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-scaffolder': patch
---
Fix the display of the description in `GitlabRepoPicker`:
- Move `owner.description` helper text outside the `allowedOwners` conditional so it renders for both `Select` and `Autocomplete` modes.
- Update the `Autocomplete` label to use `fields.gitlabRepoPicker.owner.inputTitle` instead of `fields.gitlabRepoPicker.owner.title`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-module-build': patch
---
Suppressed false-positive Module Federation warning for shared dependencies that use secondary entry points (e.g. `@mui/material/styles`). These sub-path `package.json` files lack a `version` field, causing the bundler to emit "No version specified" warnings that fail CI builds.
+88 -1
View File
@@ -225,36 +225,123 @@
"@backstage/plugin-user-settings-common": "0.1.0"
},
"changesets": [
"add-actions-registry-examples",
"add-badge-component",
"add-to-error-utility",
"angry-clouds-tell",
"app-defaults-clarify-failures",
"app-routes-redirect-config",
"auditor-zod-v4-refactor",
"auth-node-logout-result",
"auth0-federated-logout",
"azure-scm-events-layer",
"bitbucket-cloud-scm-events-layer",
"brave-suits-pay",
"breezy-bushes-divide",
"bump-glob-rollup-security",
"catalog-backend-location-entity-ref",
"catalog-backend-permissions-cleanup-step-1",
"catalog-backend-permissions-cleanup-step-2",
"catalog-entity-page-no-header",
"catalog-exists-query-optimization",
"catalog-graph-nfs-page",
"catalog-import-nfs-page",
"catalog-nfs-page-header",
"catalog-node-permissions-cleanup-step-2",
"catalog-search-table-sync",
"chatty-cups-create",
"clarify-fetch-failures",
"cold-bikes-beam",
"cold-ways-double",
"common-plums-chew",
"cool-shoes-hide",
"core-app-api-logout-redirect",
"create-app-1774970958",
"create-app-1775575768",
"deep-ripe-rats",
"dependabot-03b295a",
"dependabot-2a735a0",
"dependabot-cc35625",
"drop-react-17-packages-ui",
"easy-pens-judge",
"evil-seals-smell",
"fix-avatar-flex-shrink",
"fix-aws-s3-url-reader-custom-endpoints",
"fix-bui-relative-href-resolution",
"fix-catalog-filter-flicker",
"fix-catalog-refresh-state-deadlock",
"fix-catalog-table-loading-flash",
"fix-entity-info-card-header-overflow",
"fix-org-profile-card-overflow",
"fix-sidebar-settings-nav",
"fix-translation-ref-ts6",
"fix-zod-generic-auth-node",
"fix-zod-generic-frontend-plugin-api",
"frontend-app-api-permission-error",
"funny-items-wink",
"fuzzy-mangos-design",
"gitlab-scm-events-layer",
"happy-masks-allow",
"header-nav-resolve-href",
"hide-compat-wrapper-headers",
"hip-parents-fall",
"host-discovery-baseurl-warnings",
"hot-yaks-crash",
"kubernetes-nfs-metadata",
"lazy-jars-shake",
"loopback-redirect-uri-port",
"lucky-adults-talk",
"lucky-things-write",
"major-actors-stand",
"major-banks-prove",
"migrate-techdocs-catalog-service-ref",
"nine-parrots-melt",
"nine-signs-end",
"notifications-nfs-metadata",
"olive-peaches-fly",
"olive-ravens-smell",
"orange-friends-march",
"org-nfs-metadata",
"parallelize-cli-reports",
"plugin-header-remove-toolbar-wrapper",
"quiet-streets-laugh",
"remove-cli-module-new-dep",
"remove-deprecated-catalog-node-alpha-exports",
"remove-empty-examples-scaffolder-bridge",
"remove-jest-when-dep",
"remove-legacy-proxy-techdocs-cli",
"remove-plugin-header-action-define-params",
"remove-type-fest-github",
"renovate-9d44e41",
"renovate-ced70c0",
"replace-humanize-entity-ref-catalog-react",
"replace-humanize-entity-ref-plugins",
"repo-tools-peer-deps-react18",
"scaffolder-backend-permissions-registry",
"scaffolder-nfs-page-layout",
"scaffolder-node-permission-refs",
"show-pagination-label",
"shy-doors-repair",
"signals-nfs-metadata",
"simplify-compat-route-ref",
"simplify-route-ref-types",
"six-suits-write",
"smart-cycles-fall",
"soft-beers-bathe",
"solid-pianos-act",
"specialized-app-apis",
"swift-lizards-bathe",
"thin-elephants-joke",
"true-groups-slide",
"ts6-cli-dom-asynciterable",
"ts6-eslint-plugin",
"ts6-plugin-scaffolder",
"two-zoos-teach",
"ui-menu-item-style-refactor",
"ui-react-aria-deps",
"unprocessed-entities-devtools",
"update-app-visualizer-header-action"
"update-app-visualizer-header-action",
"wicked-impalas-fry",
"witty-tools-cough"
]
}
@@ -0,0 +1,10 @@
---
'@backstage/plugin-catalog-node': minor
---
**BREAKING ALPHA**: Removed the following deprecated exports from `@backstage/plugin-catalog-node/alpha`:
- `catalogServiceRef` — use `catalogServiceRef` from `@backstage/plugin-catalog-node` instead
- `CatalogLocationsExtensionPoint` / `catalogLocationsExtensionPoint` — use the non-alpha equivalents from `@backstage/plugin-catalog-node` instead
- `CatalogProcessingExtensionPoint` / `catalogProcessingExtensionPoint` — use the non-alpha equivalents from `@backstage/plugin-catalog-node` instead
- `CatalogAnalysisExtensionPoint` / `catalogAnalysisExtensionPoint` — use the non-alpha equivalents from `@backstage/plugin-catalog-node` instead
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Updated proxy documentation to recommend Node.js built-in proxy support via `NODE_USE_ENV_PROXY` instead of `global-agent`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Updated dependency `@types/use-sync-external-store` to `^1.0.0`.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Deprecated `humanizeEntityRef` and `humanizeEntity` in favor of the Catalog Presentation API. Use `useEntityPresentation`, `EntityDisplayName`, or `entityPresentationApiRef` instead.
@@ -0,0 +1,9 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-org-react': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-techdocs': patch
---
Replaced deprecated `humanizeEntityRef` usage with the Catalog Presentation API.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Migrated permission registration to use the `PermissionsRegistryService` instead of the deprecated `createPermissionIntegrationRouter`. This fixes an issue where scaffolder permissions were not visible to RBAC plugins because the `actionsRegistryServiceRef` dependency caused an empty permissions metadata router to shadow the scaffolder's actual permission metadata. The old `createPermissionIntegrationRouter` path is retained as a fallback for standalone `createRouter` usage.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-node': patch
---
Added `PermissionResourceRef` definitions for scaffolder resource types: `scaffolderTemplatePermissionResourceRef`, `scaffolderActionPermissionResourceRef`, and `scaffolderTaskPermissionResourceRef`. These are exported from `@backstage/plugin-scaffolder-node/alpha`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Fixed `SingleInstanceGithubCredentialsProvider` to return app credentials when `getCredentials` is called with a bare host URL (e.g. `https://github.com`) instead of falling back to a personal access token.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-module-build': patch
---
When building dist-workspaces with --always-pack, batch `yarn pack` operations to avoid packing packages and their dependencies simultaneously.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Use Backstage Link component for markdown anchor rendering to ensure consistent internal and external link behavior.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/cli-module-build': patch
'@backstage/backend-defaults': patch
---
Added experimental support for using `embedded-postgres` as the database for local development. Set `backend.database.client` to `embedded-postgres` in your app config to enable this. The `embedded-postgres` package must be installed as an explicit dependency in your project.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-github': patch
---
Added automatic retry on temporary errors (like 5XX) to the shared GitHub GraphQL client used by `GithubOrgEntityProvider` and replaced the GraphQL client in `GithubEntityProvider` by this one as well, improving resilience against intermittent GitHub API failures.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Fixed the Tabs active indicator not hiding when no tab matches the current route.
**Affected components:** Tabs
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Fixed HeaderNav hover indicator covering tab text when theme uses opaque background colors. Also fixed an incorrect CSS variable reference (`--bui-font-family``--bui-font-regular`).
**Affected components:** Header
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Migrated `InspectEntityDialog` from Material UI to Backstage UI components. Added new translation keys: `inspectEntityDialog.overviewPage.copyAriaLabel`, `inspectEntityDialog.overviewPage.copiedStatus`, `inspectEntityDialog.overviewPage.helpLinkAriaLabel`, and `inspectEntityDialog.colocatedPage.entityListAriaLabel`.
+1
View File
@@ -0,0 +1 @@
Fix OAuth 2.0 Protected Resource Metadata endpoint returning wrong URL
+1
View File
@@ -0,0 +1 @@
Fixed incorrect name of the `legacy-frontend-plugin` template
+1
View File
@@ -0,0 +1 @@
Fix issue with missing permissions on the `/.well-known` endpoint for the scaffolder plugin
+1 -1
View File
@@ -143,7 +143,7 @@ Some things that changeset should NOT contain are:
### Backstage UI Changeset Format
Changesets for `@backstage/ui` must follow a standardized format to enable proper documentation generation. See [`.changeset/README.md`](.changeset/README.md#backstage-ui-changeset-format) for the complete guide.
Changesets for `@backstage/ui` must follow a standardized format to enable proper documentation generation. See [`.changeset/README.md`](.changeset/README.md#backstage-ui-changesets) for the complete guide.
**Required structure:**
+1 -1
View File
@@ -633,7 +633,7 @@ Chunk optimization should be disabled for the initial implementation.
The `publicPath` output config in webpack is a mandatory attribute for federated modules. However, at build time, it is impossible to guess where the assets are served from. From origin to the pathname, this is specific to each installation.
We can leverage the [auto](https://webpack.js.org/guides/public-path/#automatic-publicpath) option. However this means that some manifest transformation has to happen at runtime when entry scripts are loaded into the browser. More on that in the [Plugin manifest](#plugin-manifest), [CDN Plugin](#dynamic-assets-server-plugin), and [Plugin loading](#plugin-loading) sections.
We can leverage the [auto](https://webpack.js.org/guides/public-path/#automatic-publicpath) option. However this means that some manifest transformation has to happen at runtime when entry scripts are loaded into the browser. More on that in the [Plugin manifest](#plugin-manifest), [CDN Plugin](#dynamic-assets-server), and [Plugin loading](#plugin-loading) sections.
#### Sample webpack configuration
@@ -36,7 +36,7 @@ The changes to the service-to-service auth are aimed to be the minimum needed to
## Motivation
This proposal aims to address several of the points in the [Auth Meta issue](https://github.com/backstage/backstage/issues/15999), with the overarching goal being to replace the existing [API request authentication](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/authenticate-api-requests.md) tutorial in `contrib/` with a more robust and secure built-in solution. The tutorial exists for two purposes: to add authentication of API requests as part of using the permission system in Backstage, and to protect a Backstage instance from external access. It does a fairly good job of the former, although we want to avoid placing user tokens in cookies, but it does a quite poor job of the latter, which we want to fix.
This proposal aims to address several of the points in the [Auth Meta issue](https://github.com/backstage/backstage/issues/15999), with the overarching goal being to replace the existing [API request authentication](https://github.com/backstage/backstage/blob/a93b7fdafb6789277661ba77b8cc3390de82db27/contrib/docs/tutorials/authenticate-api-requests.md) tutorial in `contrib/` with a more robust and secure built-in solution. The tutorial exists for two purposes: to add authentication of API requests as part of using the permission system in Backstage, and to protect a Backstage instance from external access. It does a fairly good job of the former, although we want to avoid placing user tokens in cookies, but it does a quite poor job of the latter, which we want to fix.
A secondary goal is to do this work before stabilizing the APIs in the new Backend system, as it will have some impact on how plugin backends are built. This will inevitably also lead to the need to improve the way that service-to-service auth is handled in Backstage, although that is not the primary goal of this work.
@@ -1,459 +0,0 @@
# Authenticate API requests
> [!CAUTION]
> This entire guide MUST NOT BE USED by users of Backstage 1.26 and
> newer. If you have applied the changes in this guide, you need to remove them
> again as you upgrade to recent versions of Backstage. When [the new auth changes](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution)
> landed backends became natively secured through the framework, and the
> instructions outlined in here can interfere with the backend functioning
> correctly.
The Backstage backend APIs are by default available without authentication. To avoid evil-doers from accessing or modifying data, one might use a network protection mechanism such as a firewall or an authenticating reverse proxy. For Backstage instances that are available on the Internet one can instead use the experimental IdentityClient as outlined below.
API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response.
**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. If you have not done so already, you will also need to implement [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth).
As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire).
## Old Backend System Setup
Create `packages/backend/src/authMiddleware.ts`:
```typescript
import type { Config } from '@backstage/config';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import { NextFunction, Request, Response, RequestHandler } from 'express';
import { decodeJwt } from 'jose';
import { URL } from 'url';
import { PluginEnvironment } from './types';
function setTokenCookie(
res: Response,
options: { token: string; secure: boolean; cookieDomain: string },
) {
try {
const payload = decodeJwt(options.token);
res.cookie('token', options.token, {
expires: new Date(payload.exp ? payload.exp * 1000 : 0),
secure: options.secure,
sameSite: 'lax',
domain: options.cookieDomain,
path: '/',
httpOnly: true,
});
} catch (_err) {
// Ignore
}
}
export const createAuthMiddleware = async (
config: Config,
appEnv: PluginEnvironment,
) => {
const baseUrl = config.getString('backend.baseUrl');
const secure = baseUrl.startsWith('https://');
const cookieDomain = new URL(baseUrl).hostname;
const authMiddleware: RequestHandler = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const token =
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
(req.cookies?.token as string | undefined);
if (!token) {
res.status(401).send('Unauthorized');
return;
}
try {
req.user = await appEnv.identity.getIdentity({ request: req });
} catch {
await appEnv.tokenManager.authenticate(token);
}
if (!req.headers.authorization) {
// Authorization header may be forwarded by plugin requests
req.headers.authorization = `Bearer ${token}`;
}
if (token && token !== req.cookies?.token) {
setTokenCookie(res, {
token,
secure,
cookieDomain,
});
}
next();
} catch (error) {
res.status(401).send('Unauthorized');
}
};
return authMiddleware;
};
```
Install cookie-parser:
```bash
# From your Backstage root directory
yarn --cwd packages/backend add cookie-parser
```
Update routes in `packages/backend/src/index.ts`:
```typescript
// packages/backend/src/index.ts from a create-app deployment
import { createAuthMiddleware } from './authMiddleware';
import cookieParser from 'cookie-parser';
// ...
async function main() {
// ...
const authMiddleware = await createAuthMiddleware(config, appEnv);
const apiRouter = Router();
apiRouter.use(cookieParser());
// The auth route must be publicly available as it is used during login
apiRouter.use('/auth', await auth(authEnv));
// Add a simple endpoint to be used when setting a token cookie
apiRouter.use('/cookie', authMiddleware, (_req, res) => {
res.status(200).send(`Coming right up`);
});
// Only authenticated requests are allowed to the routes below
apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv));
apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv));
apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv));
apiRouter.use(authMiddleware, notFoundHandler());
// ...
}
```
## New Backend System Setup
Create `packages/backend/src/authMiddlewareFactory.ts`:
```typescript
import { HostDiscovery } from '@backstage/backend-app-api';
import { ServerTokenManager } from '@backstage/backend-common';
import {
LoggerService,
RootConfigService,
} from '@backstage/backend-plugin-api';
import {
DefaultIdentityClient,
getBearerTokenFromAuthorizationHeader,
} from '@backstage/plugin-auth-node';
import { NextFunction, Request, RequestHandler, Response } from 'express';
import { decodeJwt } from 'jose';
import lzstring from 'lz-string';
import { URL } from 'url';
type AuthMiddlewareFactoryOptions = {
config: RootConfigService;
logger: LoggerService;
};
export const authMiddlewareFactory = ({
config,
logger,
}: AuthMiddlewareFactoryOptions): RequestHandler => {
const baseUrl = config.getString('backend.baseUrl');
const discovery = HostDiscovery.fromConfig(config);
const identity = DefaultIdentityClient.create({ discovery });
const tokenManager = ServerTokenManager.fromConfig(config, { logger });
return async (req: Request, res: Response, next: NextFunction) => {
const fullPath = `${req.baseUrl}${req.path}`;
// Only apply auth to /api routes & skip auth for the following endpoints
// Add any additional plugin routes you want to whitelist eg. events
const nonAuthWhitelist = ['app', 'auth'];
const nonAuthRegex = new RegExp(
`^\/api\/(${nonAuthWhitelist.join('|')})(?=\/|$)\S*`,
);
if (!fullPath.startsWith('/api/') || nonAuthRegex.test(fullPath)) {
next();
return;
}
try {
// Token cookies are compressed to reduce size
const cookieToken = lzstring.decompressFromEncodedURIComponent(
req.cookies.token,
);
const token =
getBearerTokenFromAuthorizationHeader(req.headers.authorization) ??
cookieToken;
try {
// Attempt to authenticate as a frontend request token
await identity.authenticate(token);
} catch (err) {
// Attempt to authenticate as a backend request token
await tokenManager.authenticate(token);
}
if (!req.headers.authorization) {
// Authorization header may be forwarded by plugin requests
req.headers.authorization = `Bearer ${token}`;
}
if (token !== cookieToken) {
try {
const payload = decodeJwt(token);
res.cookie('token', token, {
// Compress token to reduce cookie size
encode: lzstring.compressToEncodedURIComponent,
expires: new Date((payload?.exp ?? 0) * 1000),
secure: baseUrl.startsWith('https://'),
sameSite: 'lax',
domain: new URL(baseUrl).hostname,
path: '/',
httpOnly: true,
});
} catch {
// Ignore
}
}
next();
} catch {
res.status(401).send(`Unauthorized`);
}
};
};
```
Install cookie-parser:
```bash
# From your Backstage root directory
yarn --cwd packages/backend add cookie-parser @types/cookie-parser
```
Create a custom configured `rootHttpRouterService` in `packages/backend/src/customRootHttpRouterService.ts`:
```typescript
import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api';
import cookieParser from 'cookie-parser';
import { authMiddlewareFactory } from './authMiddlewareFactory';
export default rootHttpRouterServiceFactory({
configure: ({ app, config, logger, middleware, routes }) => {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(cookieParser());
app.use(middleware.logging());
app.use(authMiddlewareFactory({ config, logger }));
// Simple handler to set auth cookie for user
app.use('/api/cookie', (_, res) => {
res.status(200).send();
});
app.use(routes);
app.use(middleware.notFound());
app.use(middleware.error());
},
});
```
Update `packages/backend/src/index.ts` to add the custom `rootHttpRouterService` and override the default:
```typescript
// ...
const backend = createBackend();
backend.add(import('./customRootHttpRouterService'));
// ...
```
## Frontend Setup
Create `packages/app/src/cookieAuth.ts`:
```typescript
import type { IdentityApi } from '@backstage/core-plugin-api';
// Parses supplied JWT token and returns the payload
function parseJwt(token: string): { exp: number } {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(
c =>
// eslint-disable-next-line prefer-template
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
)
.join(''),
);
return JSON.parse(jsonPayload);
}
// Returns milliseconds until the supplied JWT token expires
function msUntilExpiry(token: string): number {
const payload = parseJwt(token);
const remaining =
new Date(payload.exp * 1000).getTime() - new Date().getTime();
return remaining;
}
// Calls the specified url regularly using an auth token to set a token cookie
// to authorize regular HTTP requests when loading techdocs
export async function setTokenCookie(url: string, identityApi: IdentityApi) {
const { token } = await identityApi.getCredentials();
if (!token) {
return;
}
await fetch(url, {
mode: 'cors',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
},
});
// Call this function again a few minutes before the token expires
const ms = msUntilExpiry(token) - 4 * 60 * 1000;
setTimeout(
() => {
setTokenCookie(url, identityApi);
},
ms > 0 ? ms : 10000,
);
}
```
```typescript
// required types and packages for example below
import type { IdentityApi } from '@backstage/core-plugin-api';
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
// additional packages/app/src/App.tsx from a create-app deployment
import { setTokenCookie } from './cookieAuth';
// ...
const app = createApp({
// ...
components: {
SignInPage: props => {
const discoveryApi = useApi(discoveryApiRef);
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
onSignInSuccess={async (identityApi: IdentityApi) => {
setTokenCookie(
await discoveryApi.getBaseUrl('cookie'),
identityApi,
);
props.onSignInSuccess(identityApi);
}}
/>
);
},
},
// ...
});
// ...
```
**NOTE**: Most Backstage frontend plugins come with the support for the `IdentityApi`.
In case you already have a dozen of internal ones, you may need to update those too.
Assuming you follow the common plugin structure, the changes to your front-end may look like:
```diff
// plugins/internal-plugin/src/api.ts
- import { createApiRef } from '@backstage/core-plugin-api';
+ import { createApiRef, IdentityApi } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
// ...
type MyApiOptions = {
configApi: Config;
+ identityApi: IdentityApi;
// ...
}
interface MyInterface {
getData(): Promise<MyData[]>;
}
export class MyApi implements MyInterface {
private configApi: Config;
+ private identityApi: IdentityApi;
// ...
constructor(options: MyApiOptions) {
this.configApi = options.configApi;
+ this.identityApi = options.identityApi;
}
async getMyData() {
const backendUrl = this.configApi.getString('backend.baseUrl');
+ const { token } = await this.identityApi.getCredentials();
const requestUrl = `${backendUrl}/api/data/`;
- const response = await fetch(requestUrl);
+ const response = await fetch(
requestUrl,
{ headers: { Authorization: `Bearer ${token}` } },
);
// ...
}
```
and
```diff
// plugins/internal-plugin/src/plugin.ts
import {
configApiRef,
createApiFactory,
createPlugin,
+ identityApiRef,
} from '@backstage/core-plugin-api';
import { myPluginPageRouteRef } from './routeRefs';
import { MyApi, myApiRef } from './api';
export const plugin = createPlugin({
id: 'my-plugin',
routes: {
mainPage: myPluginPageRouteRef,
},
apis: [
createApiFactory({
api: myApiRef,
deps: {
configApi: configApiRef,
+ identityApi: identityApiRef,
},
- factory: ({ configApi }) =>
- new MyApi({ configApi }),
+ factory: ({ configApi, identityApi }) =>
+ new MyApi({ configApi, identityApi }),
}),
],
});
```
@@ -1,104 +0,0 @@
# Legacy: Running the backend behind a Corporate Proxy
> [!NOTE]
> On Node.js 22.21.0 or later, you can use Node.js's built-in proxy support instead of the workarounds described here. See the [recommended proxy setup guide](../../../docs/tutorials/corporate-proxy.md) for details.
This article helps you get your backend installation up and running making calls through corporate proxies.
## Background
Let's admit it, we've all been there. Sometimes you have to run stuff with no way out to the public internet, except via the smallest of corporate proxy tunnels. It's most likely that you're going to run into these issues from the backend part of Backstage as that's the part that isn't helped by your browser or OS settings for the corporate proxy.
Unfortunately, neither the Node.js native `fetch` nor the other frequently used library `node-fetch` (see [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013)) respect `HTTP(S)_PROXY` environment variables by default. As an additional complication, there is no single solution for configuring both native `fetch` and `node-fetch` at once, uniformly.
There are however some ways to get this to work without too much effort.
## Installation
**Note:** You're going to want to be in your backend working directory for these solutions as that's where the requests come from that don't go through this proxy.
1. Install the required packages in your backend, by running the following command inside your backend directory (typically `packages/backend` under your repository root).
```bash
yarn add undici global-agent
```
`undici` exposes the settings for native `fetch`, and `global-agent` can set things up for `node-fetch`.
1. Go to the entry file for the backend (typically `packages/backend/src/index.ts`), and add the following at the VERY top, before all other imports etc:
```ts
import 'global-agent/bootstrap';
import { setGlobalDispatcher, EnvHttpProxyAgent } from 'undici';
setGlobalDispatcher(new EnvHttpProxyAgent());
```
The first import automatically bootstraps `global-agent`, which addresses `node-fetch` proxying. The lines below that set up the `undici` package which affects native `fetch`.
1. Start the backend with the correct environment variables set. For example:
```sh
export HTTP_PROXY=http://username:password@proxy.example.net:8888
export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY}
yarn start
```
The default for `global-agent` is to have a prefix on the variable names, hence the need for specifying it twice. For further information about `HTTP(S)_PROXY` and `NO_PROXY` excludes, see [the global-agent documentation](https://github.com/gajus/global-agent) and [undici documentation](https://github.com/nodejs/undici).
## Configuration
If your development environment is in the cloud (like with [AWS Cloud9](https://aws.amazon.com/cloud9/) or an instance of [Theia](https://theia-ide.org/)), you will need to update your configuration.
You will probably need to make some changes in `app-config.yaml` (or another config file like `app-config.local.yaml` if you've created it, see the [configuration doc](https://backstage.io/docs/conf/#supplying-configuration)).
The exact values will depend on your setup but for instance, if your public URL is `https://your-public-url.com` and the port `3000` and `8080` are open:
```yaml
app:
baseUrl: https://your-public-url.com:3000
listen:
host: 0.0.0.0 # This makes the dev server bind to all IPv4 interfaces and not just the baseUrl hostname
backend:
baseUrl: https://your-public-url.com:8080
listen:
port: 8080
cors:
origin: https://your-public-url.com:3000
```
The app port must proxy web socket connections in order to make hot reloading work.
## Alternatives to `global-agent`
The `proxy-agent` package can be used as an alternative to `global-agent` (do not install both!), and also ensures that the `node-fetch` library correctly respects proxy settings, but [does NOT work](https://github.com/TooTallNate/proxy-agents/issues/239) for modern `undici` based native Node.js `fetch`, so you'll still have to also do the `undici` steps in the section above in addition to this.
`proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request.
1. Install `proxy-agent` using `yarn add proxy-agent`
2. Go to the entry file for the backend (`src/index.ts`)
3. At the top of the file paste the following:
```ts
import ProxyAgent from 'proxy-agent';
import http from 'http';
import https from 'https';
/*
Something to note here, this might need different configuration depending on your own setup.
If you only have an http_proxy then you'll need to set that as both the http and https globalAgent instead.
*/
if (process.env.HTTP_PROXY) {
http.globalAgent = new ProxyAgent(process.env.HTTP_PROXY);
}
if (process.env.HTTPS_PROXY) {
https.globalAgent = new ProxyAgent(process.env.HTTPS_PROXY);
}
```
4. Start the backend with `yarn start`
## Backstage CLI
The Backstage CLI [versions:bump](https://backstage.io/docs/tooling/cli/commands#versionsbump) command also supports proxies via `global-agent` environment variable configuration. See the [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated/#proxy) docs for more information.
@@ -1,468 +0,0 @@
# Migrating to the New JSX Transform using a Codemod
## Using the Codemod
While a codemod for the New JSX Transform was originally introduced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article, it is no longer functional. A working solution, inspired by the original, is detailed below:
1. **Create the transform file**
Create a file named `transform.js` in the root directory of your Backstage project.
```js
/**
* (c) Facebook, Inc. and its affiliates. Confidential and proprietary.
*
* @format
*/
module.exports = function (file, api, options) {
const j = api.jscodeshift;
const printOptions = options.printOptions || {};
const root = j(file.source);
const destructureNamespaceImports = options.destructureNamespaceImports;
// <https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md>
function getFirstNode() {
return root.find(j.Program).get('body', 0).node;
}
// Save the comments attached to the first node
const firstNode = getFirstNode();
const { comments } = firstNode;
function isVariableDeclared(variable) {
return (
root
.find(j.Identifier, {
name: variable,
})
.filter(
path =>
path.parent.value.type !== 'MemberExpression' &&
path.parent.value.type !== 'QualifiedTypeIdentifier' &&
// Added this
path.parent.value.type !== 'TSQualifiedName' &&
path.parent.value.type !== 'JSXMemberExpression',
)
.size() > 0
);
}
// Get all paths that import from React
const reactImportPaths = root
.find(j.ImportDeclaration, {
type: 'ImportDeclaration',
})
.filter(path => {
return (
(path.value.source.type === 'Literal' ||
path.value.source.type === 'StringLiteral') &&
(path.value.source.value === 'React' ||
path.value.source.value === 'react')
);
});
// get all namespace/default React imports
const reactPaths = reactImportPaths.filter(path => {
return (
path.value.specifiers.length > 0 &&
path.value.importKind === 'value' &&
path.value.specifiers.some(
specifier => specifier.local.name === 'React',
)
);
});
if (reactPaths.size() > 1) {
throw Error(
'There should only be one React import. Please remove the duplicate import and try again.',
);
}
if (reactPaths.size() === 0) {
return null;
}
const reactPath = reactPaths.paths()[0];
// Reuse the node so that we can preserve quoting style.
const reactLiteral = reactPath.value.source;
const isDefaultImport = reactPath.value.specifiers.some(
specifier =>
specifier.type === 'ImportDefaultSpecifier' &&
specifier.local.name === 'React',
);
// Check to see if we should keep the React import
const isReactImportUsed =
root
.find(j.Identifier, {
name: 'React',
})
.filter(path => {
return path.parent.parent.value.type !== 'ImportDeclaration';
})
.size() > 0;
// local: imported
const reactIdentifiers = {};
const reactTypeIdentifiers = {};
let canDestructureReactVariable = false;
if (
isReactImportUsed &&
(isDefaultImport || destructureNamespaceImports)
) {
// Checks to see if the react variable is used itself (rather than used to access its properties)
canDestructureReactVariable =
root
.find(j.Identifier, {
name: 'React',
})
.filter(path => {
return path.parent.parent.value.type !== 'ImportDeclaration';
})
.filter(
path =>
!(
path.parent.value.type === 'MemberExpression' &&
path.parent.value.object.name === 'React'
) &&
!(
path.parent.value.type === 'QualifiedTypeIdentifier' &&
path.parent.value.qualification.name === 'React'
) &&
!(
// Added this
(
path.parent.value.type === 'TSQualifiedName' &&
path.parent.value.left.name === 'React'
)
) &&
!(
path.parent.value.type === 'JSXMemberExpression' &&
path.parent.value.object.name === 'React'
),
)
.size() === 0;
if (canDestructureReactVariable) {
// Add React identifiers to separate object so we can destructure the imports
// later if we can. If a type variable that we are trying to import has already
// been declared, do not try to destructure imports
// (ex. Element is declared and we are using React.Element)
root
.find(j.QualifiedTypeIdentifier, {
qualification: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const id = path.value.id.name;
if (path.parent.parent.value.type === 'TypeofTypeAnnotation') {
// This is a typeof import so it isn't actually a type
reactIdentifiers[id] = id;
if (reactTypeIdentifiers[id]) {
canDestructureReactVariable = false;
}
} else {
reactTypeIdentifiers[id] = id;
if (reactIdentifiers[id]) {
canDestructureReactVariable = false;
}
}
if (isVariableDeclared(id)) {
canDestructureReactVariable = false;
}
});
// Added this
root
.find(j.TSQualifiedName, {
left: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const id = path.value.right.name;
reactIdentifiers[id] = id;
// We don't tend to use type imports
// Comment line above out and uncomment this to use type imports
// Also ignoring typeof imports?
// reactTypeIdentifiers[id] = id
// if (reactIdentifiers[id]) {
// canDestructureReactVariable = false
// }
if (isVariableDeclared(id)) {
canDestructureReactVariable = false;
}
});
// Add React identifiers to separate object so we can destructure the imports
// later if we can. If a variable that we are trying to import has already
// been declared, do not try to destructure imports
// (ex. createElement is declared and we are using React.createElement)
root
.find(j.MemberExpression, {
object: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const property = path.value.property.name;
reactIdentifiers[property] = property;
if (
isVariableDeclared(property) ||
reactTypeIdentifiers[property]
) {
canDestructureReactVariable = false;
}
});
// Add React identifiers to separate object so we can destructure the imports
// later if we can. If a JSX variable that we are trying to import has already
// been declared, do not try to destructure imports
// (ex. Fragment is declared and we are using React.Fragment)
root
.find(j.JSXMemberExpression, {
object: {
type: 'JSXIdentifier',
name: 'React',
},
})
.forEach(path => {
const property = path.value.property.name;
reactIdentifiers[property] = property;
if (
isVariableDeclared(property) ||
reactTypeIdentifiers[property]
) {
canDestructureReactVariable = false;
}
});
}
}
if (canDestructureReactVariable) {
// replace react identifiers
root
.find(j.QualifiedTypeIdentifier, {
qualification: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const id = path.value.id.name;
j(path).replaceWith(j.identifier(id));
});
// Added this
root
.find(j.TSQualifiedName, {
left: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const id = path.value.right.name;
j(path).replaceWith(j.identifier(id));
});
root
.find(j.MemberExpression, {
object: {
type: 'Identifier',
name: 'React',
},
})
.forEach(path => {
const property = path.value.property.name;
j(path).replaceWith(j.identifier(property));
});
root
.find(j.JSXMemberExpression, {
object: {
type: 'JSXIdentifier',
name: 'React',
},
})
.forEach(path => {
const property = path.value.property.name;
j(path).replaceWith(j.jsxIdentifier(property));
});
// Add existing React imports to map
reactImportPaths.forEach(path => {
const specifiers = path.value.specifiers;
for (let i = 0; i < specifiers.length; i++) {
const specifier = specifiers[i];
// get all type and regular imports that are imported
// from React
if (specifier.type === 'ImportSpecifier') {
if (
path.value.importKind === 'type' ||
specifier.importKind === 'type'
) {
reactTypeIdentifiers[specifier.local.name] =
specifier.imported.name;
} else {
reactIdentifiers[specifier.local.name] = specifier.imported.name;
}
}
}
});
const regularImports = [];
Object.keys(reactIdentifiers).forEach(local => {
const imported = reactIdentifiers[local];
regularImports.push(
j.importSpecifier(j.identifier(imported), j.identifier(local)),
);
});
const typeImports = [];
Object.keys(reactTypeIdentifiers).forEach(local => {
const imported = reactTypeIdentifiers[local];
typeImports.push(
j.importSpecifier(j.identifier(imported), j.identifier(local)),
);
});
if (regularImports.length > 0) {
j(reactPath).insertAfter(
j.importDeclaration(regularImports, reactLiteral),
);
}
if (typeImports.length > 0) {
j(reactPath).insertAfter(
j.importDeclaration(typeImports, reactLiteral, 'type'),
);
}
// remove all old react imports
reactImportPaths.forEach(path => {
// This is for import type React from 'react' which shouldn't
// be removed
if (
path.value.specifiers.some(
specifier =>
specifier.type === 'ImportDefaultSpecifier' &&
specifier.local.name === 'React' &&
(specifier.importKind === 'type' ||
path.value.importKind === 'type'),
)
) {
j(path).insertAfter(
j.importDeclaration(
[j.importDefaultSpecifier(j.identifier('React'))],
reactLiteral,
'type',
),
);
}
j(path).remove();
});
} else {
// Remove the import because it's not being used
// If we should keep the React import, just convert
// default imports to named imports
let isImportRemoved = false;
const specifiers = reactPath.value.specifiers;
for (let i = 0; i < specifiers.length; i++) {
const specifier = specifiers[i];
if (specifier.type === 'ImportNamespaceSpecifier') {
if (!isReactImportUsed) {
isImportRemoved = true;
j(reactPath).remove();
}
} else if (specifier.type === 'ImportDefaultSpecifier') {
if (isReactImportUsed) {
j(reactPath).insertAfter(
j.importDeclaration(
[j.importNamespaceSpecifier(j.identifier('React'))],
reactLiteral,
),
);
}
if (specifiers.length > 1) {
const typeImports = [];
const regularImports = [];
for (let x = 0; x < specifiers.length; x++) {
if (specifiers[x].type !== 'ImportDefaultSpecifier') {
if (specifiers[x].importKind === 'type') {
typeImports.push(specifiers[x]);
} else {
regularImports.push(specifiers[x]);
}
}
}
if (regularImports.length > 0) {
j(reactPath).insertAfter(
j.importDeclaration(regularImports, reactLiteral),
);
}
if (typeImports.length > 0) {
j(reactPath).insertAfter(
j.importDeclaration(typeImports, reactLiteral, 'type'),
);
}
}
isImportRemoved = true;
j(reactPath).remove();
}
}
if (!isImportRemoved) {
return null;
}
}
// If the first node has been modified or deleted, reattach the comments
const firstNode2 = getFirstNode();
if (firstNode2 !== firstNode) {
firstNode2.comments = comments;
}
return root.toSource(printOptions);
};
```
2. **Execute the transformation**
To apply the necessary changes, execute the following command twice from your root Backstage directory. First, run it for your packages, and then again for your plugins and any additional directories. Remember to adjust the paths to the transform script and parser source accordingly.
```console
npx jscodeshift --verbose=2 --ignore-pattern="**/node_modules/**" --parser ts --extensions=tsx,ts,jsx,js --transform ./path/to/the/transform.js --destructureNamespaceImports=true --parser=tsx ./path/to/src/
```
3. **Verify and clean up imports**
Review the codebase for any remaining instances of `import * as React from 'react'` or `import React from 'react'`. Replace these with named imports where possible, such as:
```tsx
import { useState, useEffect } from 'react';
```
If retaining the default React import is absolutely necessary, use the following syntax instead:
```tsx
import { default as React } from 'react';
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

@@ -1,115 +0,0 @@
# Prometheus
> [!NOTE]
> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation
## Overview
This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus.
## How to Setup Prometheus Metrics
1. First we need to add the needed dependencies to the `package.json` in the `\packages\backend`:
```diff
// packages/backend/package.json
"dependencies": {
+ "express-prom-bundle": "^7.0.0",
+ "prom-client": "^15.0.0",
```
2. Now we want to run `yarn install` from the root of the project to get those dependencies in place
3. Then we need to add a handler for the metrics by creating a file called `metrics.ts` in the `\packages\backend\src` folder
4. Next we add the following content to the `metrics.ts` file:
```ts
// packages/backend/src/metrics.ts
import { useHotCleanup } from '@backstage/backend-common';
import { RequestHandler } from 'express';
import promBundle from 'express-prom-bundle';
import prom from 'prom-client';
import * as url from 'url';
const rootRegEx = new RegExp('^/([^/]*)/.*');
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
export function normalizePath(req: any): string {
const path = url.parse(req.originalUrl || req.url).pathname || '/';
// Capture /api/ and the plugin name
if (apiRegEx.test(path)) {
return path.replace(apiRegEx, '/api/$1');
}
// Only the first path segment at root level
return path.replace(rootRegEx, '/$1');
}
/**
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
*/
export function metricsHandler(): RequestHandler {
// We can only initialize the metrics once and have to clean them up between hot reloads
useHotCleanup(module, () => prom.register.clear());
return promBundle({
includeMethod: true,
includePath: true,
// Using includePath alone is problematic, as it will include path labels with high
// cardinality (e.g. path params). Instead we would have to template them. However, this
// is difficult, as every backend plugin might use different routes. Instead we only take
// the first directory of the path, to have at least an idea how each plugin performs:
normalizePath,
promClient: { collectDefaultMetrics: {} },
});
}
```
5. Now we will extend the router configuration with the `metricsHandler`:
```diff
+import { metricsHandler } from './metrics';
...
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('', await healthcheck(healthcheckEnv))
+ .addRouter('', metricsHandler())
.addRouter('/api', apiRouter);
```
6. You now have everything setup, from the `\packages\backend` folder run `yarn start` this will start up the backend
7. Now in a browser load up `http://localhost:7007/metrics`, if everything went smoothly you should see metrics in your browser something like this:
![Prometheus Metrics Example Output](prometheus-metrics-output.png)
## Metrics
The following sections goes over the included and experimental metrics available once you have completed this tutorial
## Included
This tutorials uses the [`express-prom-bundle`](https://github.com/jochen-schweizer/express-prom-bundle) and the [`prom-client`](https://github.com/siimon/prom-client) to make this all work. They both come with some built in metrics:
- `express-prom-bundle` comes with 2 metrics:
- `up`: this normally will be just 1
- `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
- `prom-client` comes with a collection of metrics around memory, CPU, processes, etc. You can see the supported metrics in the `prom-client's` [`lib/metrics`](https://github.com/siimon/prom-client/tree/master/lib/metrics) folder.
### Experimental
There are some custom metrics that have been added to Backstage will be output for you, these are currently deemed experimental and may be changed or removed in a future release. Here is a rough list, again subject to changes:
- `catalog_entities_count`: Total amount of entities in the catalog
- `catalog_registered_locations_count`: Total amount of registered locations in the catalog
- `catalog_relations_count`: Total amount of relations between entities
- `catalog_stitched_entities_count`: Amount of entities stitched
- `catalog_processed_entities_count`: Amount of entities processed
- `catalog_processing_duration_seconds`: Time spent executing the full processing flow
- `catalog_processors_duration_seconds`: Time spent executing catalog processors
- `catalog_processing_queue_delay_seconds`: The amount of delay between being scheduled for processing, and the start of actually being processed
- `scaffolder_task_count`: Tracks successful task runs.
- `scaffolder_task_duration`: a histogram which tracks the duration of a task run
- `scaffolder_step_count`: a count that tracks each step run
- `scaffolder_step_duration`: a histogram which tracks the duration of each step run
@@ -1,53 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleComponent.tsx reference
```tsx
import { Typography, Grid } from '@material-ui/core';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import {
InfoCard,
Header,
Page,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
} from '@backstage/core-components';
import { ExampleFetchComponent } from '../ExampleFetchComponent';
export const ExampleComponent = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
return (
<Page themeId="tool">
<Header
title="Welcome to github-playground!"
subtitle="Optional subtitle"
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Plugin title">
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>
</Grid>
<Grid item>
<ExampleFetchComponent />
</Grid>
</Grid>
</Content>
</Page>
);
};
```
@@ -1,103 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleFetchComponent.tsx reference
```tsx
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import { graphql } from '@octokit/graphql';
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};
type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};
type DenseTableProps = {
viewer: Viewer;
};
export const DenseTable = ({ viewer }: DenseTableProps) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Created', field: 'createdAt' },
{ title: 'Description', field: 'description' },
{ title: 'Disk Usage', field: 'diskUsage' },
{ title: 'Fork', field: 'isFork' },
];
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={columns}
data={viewer.repositories.nodes}
/>
);
};
export const ExampleFetchComponent = () => {
const auth = useApi(githubAuthApiRef);
const { value, loading, error } = useAsync(async (): Promise<any> => {
const token = await auth.getAccessToken();
const gqlEndpoint = graphql.defaults({
// Uncomment baseUrl if using enterprise
// baseUrl: 'https://github.MY-BIZ.com/api',
headers: {
authorization: `token ${token}`,
},
});
const { viewer } = await gqlEndpoint(query);
return viewer;
}, []);
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
};
```
@@ -1,3 +1,6 @@
# Basic Kubernetes example with Helm
> [!NOTE]
> This documentation is deprecated and will be removed at a future date, please use the well-maintained [Backstage Helm Charts](https://github.com/backstage/charts) for this.
Note that these examples aim to show a minimal setup and do not include best practices for secure Kubernetes deployments. See the [Kubernetes documentation](https://kubernetes.io/docs/concepts/security/) for more information, or resources provided by your own organization.
+3
View File
@@ -1,5 +1,8 @@
# Confluence
> [!NOTE]
> This documentation is deprecated and will be removed at a future date. Please use the well-maintained [`@backstage-community/plugin-search-backend-module-confluence-collator` Community Plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/confluence/plugins/search-backend-module-confluence-collator) instead.
These files help you add Confluence as a source to the Backstage Search plugin.
To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app.
Then, add the following code to your packages/app/src/components/search/SearchPage.tsx:
@@ -140,7 +140,7 @@ Allows multiple panels to be open simultaneously.
<Snippet
align="center"
py={4}
height={280}
height="auto"
preview={<GroupMultipleOpen />}
code={groupMultipleOpenSnippet}
/>
@@ -0,0 +1,16 @@
'use client';
import { Badge } from '../../../../../packages/ui/src/components/Badge/Badge';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiBugLine } from '@remixicon/react';
export const Default = () => <Badge>Banana</Badge>;
export const WithIcon = () => <Badge icon={<RiBugLine />}>Banana</Badge>;
export const Sizes = () => (
<Flex direction="row" gap="2">
<Badge size="small">Banana</Badge>
<Badge size="medium">Banana</Badge>
</Flex>
);
+41
View File
@@ -0,0 +1,41 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, WithIcon, Sizes } from './components';
import { badgePropDefs } from './props-definition';
import { usage, preview, withIcons, sizes } from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { BadgeDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Badge"
description="A non-interactive label for annotating, categorizing, or highlighting content."
/>
<Snippet align="center" py={4} preview={<Default />} code={preview} />
## Usage
<CodeBlock code={usage} />
## API reference
### Badge
<PropsTable data={badgePropDefs} />
## Examples
### With icons
<Snippet align="center" py={4} open preview={<WithIcon />} code={withIcons} />
### Sizes
<Snippet align="center" py={4} open preview={<Sizes />} code={sizes} />
<Theming definition={BadgeDefinition} />
<ChangelogComponent component={['badge']} />
@@ -0,0 +1,27 @@
import {
classNamePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const badgePropDefs: Record<string, PropDef> = {
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the badge text.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
description: (
<>
Visual size of the badge. Use <Chip>small</Chip> for inline or dense
layouts, <Chip>medium</Chip> for standalone badges.
</>
),
},
...childrenPropDefs,
...classNamePropDefs,
};
@@ -0,0 +1,12 @@
export const usage = `import { Badge } from '@backstage/ui';
<Badge>Badge</Badge>`;
export const preview = `<Badge>Banana</Badge>`;
export const withIcons = `<Badge icon={<RiBugLine />}>Banana</Badge>`;
export const sizes = `<Flex direction="row" gap="2">
<Badge size="small">Banana</Badge>
<Badge size="medium">Banana</Badge>
</Flex>`;
@@ -0,0 +1,81 @@
'use client';
import { CheckboxGroup } from '../../../../../packages/ui/src/components/CheckboxGroup/CheckboxGroup';
import { Checkbox } from '../../../../../packages/ui/src/components/Checkbox/Checkbox';
export const Default = () => {
return (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
};
export const Horizontal = () => (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
orientation="horizontal"
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
export const Disabled = () => (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
isDisabled
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
export const DisabledSingle = () => (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack" isDisabled>
Slack
</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
export const Validation = () => (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github', 'slack']}
validationBehavior="aria"
validate={(value: string[]) =>
value.includes('slack') ? 'Slack is not available in your region.' : null
}
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
export const ReadOnly = () => (
<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
isReadOnly
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>
);
@@ -0,0 +1,110 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { checkboxGroupPropDefs } from './props-definition';
import {
checkboxGroupUsageSnippet,
defaultSnippet,
horizontalSnippet,
disabledSnippet,
disabledSingleSnippet,
validationSnippet,
readOnlySnippet,
} from './snippets';
import {
Default,
Horizontal,
Disabled,
DisabledSingle,
Validation,
ReadOnly,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { CheckboxGroupDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
checkboxGroup: 'https://react-aria.adobe.com/CheckboxGroup',
};
<PageTitle
title="CheckboxGroup"
description="A group of checkboxes for selecting multiple options from a list."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
<CodeBlock code={checkboxGroupUsageSnippet} />
## API reference
### CheckboxGroup
<PropsTable data={checkboxGroupPropDefs} />
<ReactAriaLink component="CheckboxGroup" href={reactAriaUrls.checkboxGroup} />
## Examples
### Horizontal
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Horizontal />}
code={horizontalSnippet}
/>
### Disabled
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Disabled />}
code={disabledSnippet}
/>
### Disabled single checkbox
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<DisabledSingle />}
code={disabledSingleSnippet}
/>
### Validation
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Validation />}
code={validationSnippet}
/>
### Read only
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<ReadOnly />}
code={readOnlySnippet}
/>
<Theming definition={CheckboxGroupDefinition} />
<ChangelogComponent component="checkbox-group" />
@@ -0,0 +1,82 @@
import {
classNamePropDefs,
childrenPropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const checkboxGroupPropDefs: Record<string, PropDef> = {
label: {
type: 'string',
description: 'The visible label for the checkbox group.',
},
'aria-label': {
type: 'string',
description:
'Accessible label when a visible label is not provided. Either label, aria-label, or aria-labelledby is required.',
},
'aria-labelledby': {
type: 'string',
description:
'ID of an element that labels the checkbox group. Either label, aria-label, or aria-labelledby is required.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary label text. Defaults to <Chip>Required</Chip> when isRequired
is true.
</>
),
},
description: {
type: 'string',
description: 'Helper text displayed below the label.',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'vertical',
description: 'The axis the checkboxes should align with.',
},
value: {
type: 'enum',
values: ['string[]'],
description: 'The selected values (controlled).',
},
defaultValue: {
type: 'enum',
values: ['string[]'],
description: 'The initial selected values (uncontrolled).',
},
onChange: {
type: 'enum',
values: ['(value: string[]) => void'],
description: 'Handler called when the selected values change.',
},
isDisabled: {
type: 'boolean',
description: 'Whether all checkboxes in the group are disabled.',
},
isReadOnly: {
type: 'boolean',
description: 'Whether all checkboxes in the group are read-only.',
},
isRequired: {
type: 'boolean',
description:
'Whether at least one selection is required for form submission.',
},
isInvalid: {
type: 'boolean',
description: 'Whether the checkbox group is in an invalid state.',
},
name: {
type: 'string',
description: 'The name used for form submission.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,65 @@
export const checkboxGroupUsageSnippet = `import { CheckboxGroup, Checkbox } from '@backstage/ui';
<CheckboxGroup label="Choose platforms for notifications" defaultValue={['github']}>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const defaultSnippet = `<CheckboxGroup label="Choose platforms for notifications" defaultValue={['github']}>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const horizontalSnippet = `<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
orientation="horizontal"
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const disabledSnippet = `<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
isDisabled
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const disabledSingleSnippet = `<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack" isDisabled>Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const validationSnippet = `<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github', 'slack']}
validationBehavior="aria"
validate={value =>
value.includes('slack') ? 'Slack is not available in your region.' : null
}
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
export const readOnlySnippet = `<CheckboxGroup
label="Choose platforms for notifications"
defaultValue={['github']}
isReadOnly
>
<Checkbox value="github">GitHub</Checkbox>
<Checkbox value="slack">Slack</Checkbox>
<Checkbox value="email">Email</Checkbox>
</CheckboxGroup>`;
@@ -30,7 +30,7 @@ const breadcrumbs = [
];
export const WithEverything = () => (
<MemoryRouter>
<MemoryRouter initialEntries={['/overview']}>
<Header
title="Page Title"
tabs={tabs.slice(0, 2)}
@@ -52,7 +52,7 @@ export const WithLongBreadcrumbs = () => (
);
export const WithTabs = () => (
<MemoryRouter>
<MemoryRouter initialEntries={['/overview']}>
<Header title="Page Title" tabs={tabs.slice(0, 3)} />
</MemoryRouter>
);
+1 -1
View File
@@ -47,7 +47,7 @@ Labels are truncated at 240px.
### Tabs
Tabs use React Router and highlight based on the current route.
Tabs auto-detect the active tab from the current route when `activeTabId` is omitted. Pass an explicit `activeTabId` to override, or `null` for no active tab.
<Snippet open preview={<WithTabs />} code={withTabs} />
@@ -42,9 +42,10 @@ export const headerPagePropDefs: Record<string, PropDef> = {
},
},
activeTabId: {
type: 'string',
type: 'enum',
values: ['string', 'null'],
description:
'ID of the currently active tab. Can be a flat tab ID or a child tab ID within a group.',
'ID of the currently active tab. Omit to auto-detect from the current route. Set to null for no active tab.',
},
breadcrumbs: {
type: 'complex',
@@ -24,6 +24,7 @@ import {
tableRootPropDefs,
columnPropDefs,
rowPropDefs,
tableBodySkeletonPropDefs,
} from './props-definition';
import {
tableUsageSnippet,
@@ -303,6 +304,12 @@ Low-level components for building custom table layouts.
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
#### TableBodySkeleton
A table body that renders animated skeleton rows as a loading placeholder. Useful when composing with primitives and you need a loading state without the full `Table` component.
<PropsTable data={tableBodySkeletonPropDefs} />
<Theming definition={TableDefinition} />
<ChangelogComponent
@@ -317,5 +324,6 @@ Low-level components for building custom table layouts.
'cell',
'cell-text',
'cell-profile',
'table-body-skeleton',
]}
/>
@@ -491,6 +491,15 @@ export const columnPropDefs: Record<string, PropDef> = {
},
};
export const tableBodySkeletonPropDefs: Record<string, PropDef> = {
columns: {
type: 'enum',
values: ['{ id: string }[]'],
description:
'Array of column objects. Each item must have an `id` property. Compatible with `ColumnConfig` and custom column types.',
},
};
export const rowPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
+8
View File
@@ -17,6 +17,10 @@ export const components: Page[] = [
title: 'Avatar',
slug: 'avatar',
},
{
title: 'Badge',
slug: 'badge',
},
{
title: 'Box',
slug: 'box',
@@ -41,6 +45,10 @@ export const components: Page[] = [
title: 'Checkbox',
slug: 'checkbox',
},
{
title: 'CheckboxGroup',
slug: 'checkbox-group',
},
{
title: 'Container',
slug: 'container',
+1
View File
@@ -17,6 +17,7 @@ export type Component =
| 'cell-profile'
| 'cell-text'
| 'checkbox'
| 'checkbox-group'
| 'collapsible'
| 'column'
| 'container'
@@ -9,6 +9,8 @@ This skill helps fully migrate an existing Backstage plugin from the old fronten
This is the preferred approach for internal plugins that are only used in a single app, since there is no need to maintain backward compatibility. It can also be used for published plugins when you're ready to drop old system support entirely.
It is highly recommended to be on Backstage version 1.49.x or above before starting this, although not mandatory, you may face issues with some of the instructions below. This can be verified by looking in the `backstage.json` file in the root of the repository.
## Key Differences from Dual Support
| Aspect | Dual Support | Full Migration |
@@ -272,7 +274,37 @@ export const myPage = PageBlueprint.make({
## Step 5: Replace Internal Routing with Sub-Pages
This is one of the biggest changes in a full migration. Old plugins often use React Router `<Route>` trees inside a router component to handle internal navigation. The new system replaces this with `SubPageBlueprint` for tabbed sub-pages.
Old frontend plugins often use React Router `<Route>` trees inside a router component to handle internal navigation. Before migrating, determine which routing pattern fits the plugin.
### Decide Which Routing Pattern to Use
Not all internal routing maps to tabs. Read the plugin's existing router component and ask the user:
> "Does your plugin use top-level tabs that users navigate between via a header (e.g. Overview / Settings)? Or does it use detail/drill-down routes (e.g. `/my-plugin/items/:id`)?"
Use `SubPageBlueprint` when:
- The sub-routes represent top-level tabs/sections of the plugin
- Users navigate between them via the header
Keep internal routing within a `PageBlueprint` `loader` when:
- Routes are detail/drill-down pages (e.g. `/my-plugin/items/:id`)
- The routing is deeply nested or dynamic
**If the plugin uses drill-down routing only**, use a `PageBlueprint` with a `loader` that handles its own `<Routes>` and skip the rest of this step:
```tsx
export const myPage = PageBlueprint.make({
params: {
path: '/my-plugin',
routeRef: rootRouteRef,
loader: () => import('./components/Router').then(m => <m.MyPluginRouter />),
},
});
```
**If the plugin uses top-level tabs**, continue with the `SubPageBlueprint` migration below.
### Old Pattern: Internal Router
@@ -349,30 +381,6 @@ How this works:
If the sub-page content needs padding, use `Container` from `@backstage/ui` as a wrapper inside the component.
### When NOT to Use Sub-Pages
Not all internal routing maps to tabs. Use `SubPageBlueprint` when:
- The sub-routes represent top-level tabs/sections of the plugin
- Users navigate between them via the header
Keep internal routing within a `PageBlueprint` `loader` when:
- Routes are detail/drill-down pages (e.g. `/my-plugin/items/:id`)
- The routing is deeply nested or dynamic
In those cases, use a `PageBlueprint` **with** a `loader` that handles its own `Routes`:
```tsx
export const myPage = PageBlueprint.make({
params: {
path: '/my-plugin',
routeRef: rootRouteRef,
loader: () => import('./components/Router').then(m => <m.MyPluginRouter />),
},
});
```
## Step 6: Update Hooks and Imports
Replace all `@backstage/core-plugin-api` imports with `@backstage/frontend-plugin-api`:
@@ -9,6 +9,8 @@ This skill helps add new frontend system (NFS) support to an existing Backstage
This is the preferred approach for published plugins or plugins that are used by external parties, since it avoids forcing consumers to migrate their app before they are ready.
It is highly recommended to be on Backstage version 1.49.x or above before starting this, although not mandatory, you may face issues with some of the instructions below. This can be verified by looking in the `backstage.json` file in the root of the repository.
## Key Concepts
- **Dual entry point:** The plugin keeps its existing `src/plugin.ts` (old system) and adds a new `src/alpha.tsx` (new system)
+93
View File
@@ -0,0 +1,93 @@
---
id: skills
title: AI Skills
description: Reusable AI skills for common Backstage development tasks.
---
Backstage publishes a set of curated _AI skills_ — self-contained guidance files that teach an AI coding assistant how to perform common Backstage engineering tasks. Skills are published to a [well-known endpoint](https://backstage.io/.well-known/skills/) on `backstage.io` and can be installed into your repository with the [`skills.sh`](https://skills.sh/) tool.
## Installing Skills
You need [Node.js](https://nodejs.org/) to run `npx`.
```bash
npx skills add https://backstage.io
```
This command reads the published index from `https://backstage.io/.well-known/skills/index.json` and allows you to select which of the available skills you want to install into your repository.
### Where skills are installed
`skills.sh` copies skill files into your repository under a directory it manages (typically `.github/skills/` or a similar location depending on your configuration). Refer to the [`skills.sh` documentation](https://skills.sh/) for details on target paths and how to customize them.
After installation, you can modify the installed files to adapt them to your project's conventions. Subsequent updates from `npx skills add` will offer to merge upstream changes.
### Using Skills with Your AI Assistant
Once a skill is installed in your repository, attach or reference the relevant `SKILL.md` file when starting a task with your AI coding assistant. Most AI assistants in editors such as VS Code will automatically pick up instruction files that are committed to your repository.
For example, when migrating MUI imports in a plugin, include the `mui-to-bui-migration` skill so the assistant follows the correct component mapping and import patterns.
## Contributing New Skills
Skills are authored in the Backstage monorepo at `docs/.well-known/skills/`. Each skill lives in its own subdirectory and must include a `SKILL.md` file as the primary entry point.
### Skill directory layout
```text
docs/.well-known/skills/
index.json # Published index of all skills
<skill-name>/
SKILL.md # Primary skill entry point (required)
<optional-supporting-files>
```
### Writing a SKILL.md
A `SKILL.md` file must include a YAML front matter block with the following fields:
```markdown
---
name: <skill-name>
description: <short description used in the published index>
---
# Skill Title
Introductory paragraph explaining when and why to use this skill.
...
```
The `name` must match the directory name. The `description` is shown to users when they browse or install skills and should be one to two sentences describing the task the skill covers.
Keep skills focused on a single, well-defined task. A skill that tries to cover too many scenarios is harder to use effectively. Prefer concrete, step-by-step guidance, working code examples, and explicit notes about common pitfalls.
### Registering the skill in the index
Add an entry to `docs/.well-known/skills/index.json`:
```json
{
"skills": [
{
"name": "<skill-name>",
"description": "<same description as in SKILL.md front matter>",
"files": ["SKILL.md"]
}
]
}
```
If your skill includes additional supporting files, list each one in the `files` array.
### Review process
All changes to skills go through the standard Backstage pull request process. When authoring or reviewing a skill, consider:
- **Accuracy** — Does the skill reflect current Backstage APIs and conventions?
- **Completeness** — Does it cover the most common cases a developer will encounter?
- **Safety** — Does it avoid patterns that could introduce security or correctness issues?
- **Scope** — Is the skill focused on a single task, or should it be split?
Skills are part of the published Backstage documentation surface, so they follow the same contribution guidelines as the rest of the docs. See [CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) for the full contribution process.
+50
View File
@@ -634,3 +634,53 @@ export const myTheme = createUnifiedTheme({
```
</details>
<details>
<summary>Missing v5 prefix for MUI 5 class names</summary>
If you are using MUI 5 components in the main app, you may notice that the rendered elements have a `v5-` prefix in front of the MUI class names, but not when you try to use the class name props in code.
Example:
```html
<button class="v5-MuiButtonBase-root v5-MuiButton-root ..." ...
```
```jsx
import { buttonClasses } from '@mui/material/Button'
...
console.log(buttonClasses.root)
// outputs "MuiButton-root" instead of "v5-MuiButton-root"
```
The reason for this is that the `UnifiedThemeProvider` is configuring the MUI class name generator function too late. According to [MUI 5 docs](https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat), it should be configured before any MUI 5 components load.
To resolve this issue:
1. Create a file that configures the class name generator, e.g. `packages/app/src/MuiClassnameSetup.ts`, containing:
```ts
// this replicates functionality from UnifiedThemeProvider
import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className';
ClassNameGenerator.configure(componentName => {
if ((componentName ?? '').startsWith('v5-')) {
return componentName;
}
return `v5-${componentName}`;
});
```
2. Import this as the very first thing in `packages/app/src/index.tsx`
```tsx
// CRITICAL: Must be first import so that static MUI V5 class names are
// generated before any MUI V5 components load
import './MuiClassnameSetup'
...
```
</details>
+1 -1
View File
@@ -113,7 +113,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app
For more details on how the `backend:bundle` command and the `skeleton.tar.gz`
file works, see the
[`backend:bundle` command docs](../tooling/cli/03-commands.md#backendbundle).
[`backend:bundle` command docs](../tooling/cli/03-commands.md#package-bundle).
The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be
executed with the root of the repo as the build context, in order to get access
+1 -1
View File
@@ -16,7 +16,7 @@ brand.
No, but it can be! Backstage is designed to be a developer portal for all your
infrastructure tooling, services, and documentation. So, it's not a monitoring
platform — but that doesn't mean you can't integrate a monitoring tool into
Backstage by writing [a plugin](#what-is-a-plugin-in-backstage).
Backstage by writing [a plugin](technical.md#what-is-a-plugin-in-backstage).
### How is Backstage licensed?
+4 -4
View File
@@ -156,11 +156,11 @@ For more information, see our
[Owners](https://github.com/backstage/backstage/blob/master/OWNERS.md) and
[Governance](https://github.com/backstage/community/blob/main/GOVERNANCE.md).
### Does Spotify provide a managed version of Backstage?
### Are there commercial or hosted versions of Backstage available?
No, this is not a service offering. We build the piece of software, and someone
in your infrastructure team is responsible for
[deploying](https://backstage.io/docs/deployment) and maintaining it.
Yes, there are several [commercial partners](https://backstage.io/community)
that offer hosted versions, enterprise support, and consulting services
around Backstage.
### How secure is Backstage?
+10 -22
View File
@@ -17,32 +17,20 @@ The first step is to add the Kubernetes frontend plugin to your Backstage applic
yarn --cwd packages/app add @backstage/plugin-kubernetes
```
Once the package has been installed, you need to import the plugin in your app by adding the "Kubernetes" tab to the respective catalog pages.
Once installed, the plugin is automatically available in your app through the default feature discovery. It adds a "Kubernetes" tab to entity pages for entities that have Kubernetes resources associated with them. For more details and alternative installation methods, see [installing plugins](../../frontend-system/building-apps/05-installing-plugins.md).
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
/* highlight-add-next-line */
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
The Kubernetes tab is shown by default for entities where Kubernetes data is available, based on the entity annotations. You can customize the entity filter for the tab through `app-config.yaml`:
// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityLayout>
{/* other tabs... */}
{/* highlight-add-start */}
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
{/* highlight-add-end */}
</EntityLayout>
);
```yaml title="app-config.yaml"
app:
extensions:
- entity-content:kubernetes/kubernetes:
config:
filter:
metadata.annotations.backstage.io/kubernetes-id:
$exists: true
```
:::note Note
The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
:::
That's it! But now, we need the Kubernetes Backend plugin for the frontend to work.
## Adding Kubernetes Backend plugin
+1 -1
View File
@@ -99,7 +99,7 @@ even if a valid ID token was attached that a cluster would authorize.
## Other known limitations
The proxy as it was released in [Backstage 1.9](../../releases/v1.9.0-changelog.md#patch-changes-15)
The proxy as it was released in [Backstage 1.9](../../releases/v1.9.0-changelog.md)
has a known bug:
- [#15901](https://github.com/backstage/backstage/issues/15901) - it cannot
+1 -1
View File
@@ -34,7 +34,7 @@ The following sections show the plugins and search engines currently supported b
| Plugin | Support Status |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| Software Catalog | ✅ |
| [TechDocs](./how-to-guides.md#how-to-index-techdocs-documents) | ✅ |
| [TechDocs](./how-to-guides.md#how-to-customize-fields-in-the-software-catalog-or-techdocs-index) | ✅ |
| [Stack Overflow](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/README.md#index-stack-overflow-questions-to-search) | ✅ |
### Search engines
@@ -0,0 +1,351 @@
---
id: search-getting-started--old
title: Getting Started with Search (Old Frontend System)
description: How to set up and install Backstage Search
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./getting-started.md) instead.
::::
Search functions as a plugin to Backstage, so you will need to use Backstage to
use Search.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
> If you used `npx @backstage/create-app`, and you have a search page defined in
> `packages/app/src/components/search`, skip to
> [`Customizing Search`](#customizing-search) below.
## Adding Search to the Frontend
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react
```
Create a new `packages/app/src/components/search/SearchPage.tsx` file in your
Backstage app with the following contents:
```tsx
import { Content, Header, Page } from '@backstage/core-components';
import { Grid, List, Card, CardContent } from '@material-ui/core';
import {
SearchBar,
SearchResult,
DefaultResultListItem,
SearchFilter,
} from '@backstage/plugin-search-react';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
export const searchPage = (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar />
</Grid>
<Grid item xs={3}>
<Card>
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['experimental', 'production']}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
default:
return (
<DefaultResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
```
Bind the above Search Page to the `/search` route in your
`packages/app/src/App.tsx` file, like this:
```tsx
import { SearchPage } from '@backstage/plugin-search';
import { searchPage } from './components/search/SearchPage';
const routes = (
<FlatRoutes>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
</FlatRoutes>
);
```
### Using the Search Modal
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
## Adding Search to the Backend
Add the following plugins into your backend app:
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-module-pg @backstage/plugin-search-backend-module-catalog @backstage/plugin-search-backend-module-techdocs
```
Then add the following lines:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
// Other plugins...
/* highlight-add-start */
// search plugin
backend.add(import('@backstage/plugin-search-backend'));
// search engines
backend.add(import('@backstage/plugin-search-backend-module-pg'));
// search collators
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
/* highlight-add-end */
backend.start();
```
With the above setup Search will use the [Lunr](https://github.com/olivernn/lunr.js) in-memory Search Engine but if your have Postgres setup as your database then it will use Postgres as your Search Engine. Learn more in the [Search Engines](./search-engines.md) documentation.
The above also sets up two Collators for you - Catalog and TechDocs - which will index content from these two locations so that you can easily search them. Learn more in the [Collators documentation](./collators.md).
## Customizing Search
### Frontend
The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties,
including `<SearchFilter.Select />` and `<SearchFilter.Checkbox />`. These allow
you to provide values relevant to your Backstage instance that, when selected,
get passed to the backend.
```tsx {2-5,8-11}
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['production', 'experimental']}
/>
</CardContent>
```
If you have advanced filter needs, you can specify your own filter component
like this (although new core filter contributions are welcome):
```tsx
import { useSearch, SearchFilter } from '@backstage/plugin-search-react';
const MyCustomFilter = () => {
// Note: filters contain filter data from other filter components. Be sure
// not to clobber other filters' data!
const { filters, setFilters } = useSearch();
return (/* ... */);
};
// Which could be rendered like this:
<SearchFilter component={MyCustomFilter} />
```
It's good practice for search results to highlight information that was used to
return it in the first place! The code below highlights how you might specify a
custom result item component, using the `<CatalogSearchResultListItem />` component as
an example:
```tsx {7-13}
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
// result.type is the index type defined by the collator.
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
// ...
}
})}
</List>
)}
</SearchResult>
```
> For more advanced customization of the Search frontend, also see how to guides such as [How to implement your own Search API](./how-to-guides--old.md#how-to-implement-your-own-search-api) and [How to customize search results highlighting styling](./how-to-guides--old.md#how-to-customize-search-results-highlighting-styling)
### Backend
Backstage Search isn't a search engine itself, rather, it provides an interface
between your Backstage instance and a
[Search Engine](./concepts.md#search-engines) of your choice. Currently, we only
support two engines, an in-memory search Engine called Lunr and Elasticsearch.
See [Search Engines](./search-engines.md) documentation for more information how
to configure these in your Backstage instance.
Backstage Search can be used to power search of anything! Plugins like the
Catalog offer default [collators](./concepts.md#collators) (e.g.
[DefaultCatalogCollator](https://github.com/backstage/backstage/blob/df12cc25aa4934a98bc42ed03c07f64a1a0a9d72/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts))
which are responsible for providing documents
[to be indexed](./concepts.md#documents-and-indices). You can register any
number of collators with the `IndexBuilder` like this:
```typescript
const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine });
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
initialDelay: { seconds: 3 },
});
const everyHourSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { hours: 1 },
timeout: { minutes: 90 },
initialDelay: { seconds: 3 },
});
indexBuilder.addCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
indexBuilder.addCollator({
schedule: everyHourSchedule,
factory: new MyCustomCollatorFactory(),
});
```
Backstage Search builds and maintains its index
[on a schedule](./concepts.md#the-scheduler). You can change how often the
indexes are rebuilt for a given type of document. You may want to do this if
your documents are updated more or less frequently. You can do so by configuring
a scheduled `SchedulerServiceTaskRunner` to pass into the `schedule` value, like this:
```typescript {3}
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
initialDelay: { seconds: 3 },
});
indexBuilder.addCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
```
:::note Note
if you are using the in-memory Lunr search engine, you probably want to
implement a non-distributed `SchedulerServiceTaskRunner` like the following to ensure consistency
if you're running multiple search backend nodes (alternatively, you can configure
the search plugin to use a non-distributed database such as
[SQLite](../../tutorials/configuring-plugin-databases.md#postgresql-and-sqlite-3)):
:::
```typescript
import {
SchedulerServiceTaskRunner,
SchedulerServiceTaskInvocationDefinition,
} from '@backstage/backend-plugin-api';
const schedule: SchedulerServiceTaskRunner = {
run: async (task: SchedulerServiceTaskInvocationDefinition) => {
const startRefresh = async () => {
while (!task.signal?.aborted) {
try {
await task.fn(task.signal);
} catch {
// ignore intentionally
}
await new Promise(resolve => setTimeout(resolve, 600 * 1000));
}
};
startRefresh();
},
};
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
```
> For more advanced customization of the Search backend, also see how to guides such as [How to customize fields in the Software Catalog or TechDocs index](./how-to-guides--old.md#how-to-customize-fields-in-the-software-catalog-or-techdocs-index)
+71 -160
View File
@@ -4,128 +4,60 @@ title: Getting Started with Search
description: How to set up and install Backstage Search
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./getting-started--old.md)
instead.
::::
Search functions as a plugin to Backstage, so you will need to use Backstage to
use Search.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
> If you used `npx @backstage/create-app`, and you have a search page defined in
> `packages/app/src/components/search`, skip to
> [`Customizing Search`](#customizing-search) below.
## Adding Search to the Frontend
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react
```
Create a new `packages/app/src/components/search/SearchPage.tsx` file in your
Backstage app with the following contents:
Once installed, the search plugin is automatically available in your app through
the default feature discovery. It provides a search page at `/search`, a search
navigation item in the sidebar, and a search modal accessible from the sidebar.
For more details and alternative installation methods, see
[installing plugins](../../frontend-system/building-apps/05-installing-plugins.md).
```tsx
import { Content, Header, Page } from '@backstage/core-components';
import { Grid, List, Card, CardContent } from '@material-ui/core';
import {
SearchBar,
SearchResult,
DefaultResultListItem,
SearchFilter,
} from '@backstage/plugin-search-react';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
### Configuring the search page
export const searchPage = (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar />
</Grid>
<Grid item xs={3}>
<Card>
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['experimental', 'production']}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
default:
return (
<DefaultResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
The search page can be configured through `app-config.yaml`. For example, to
disable search result tracking:
```yaml title="app-config.yaml"
app:
extensions:
- page:search:
config:
noTrack: true
```
Bind the above Search Page to the `/search` route in your
`packages/app/src/App.tsx` file, like this:
### Search result list items
```tsx
import { SearchPage } from '@backstage/plugin-search';
import { searchPage } from './components/search/SearchPage';
The search page automatically discovers and uses search result list item
extensions provided by installed plugins. For example, the catalog plugin
provides a `CatalogSearchResultListItem` and the TechDocs plugin provides a
`TechDocsSearchResultListItem`. These are automatically registered when the
respective plugins are installed.
const routes = (
<FlatRoutes>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
</FlatRoutes>
);
```
You can also install additional search result list item extensions using the
`SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`.
### Using the Search Modal
### Search filters
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
Similarly, search filter extensions are automatically discovered. You can add
custom filters using the `SearchFilterBlueprint` or
`SearchFilterResultTypeBlueprint` from `@backstage/plugin-search-react/alpha`.
## Adding Search to the Backend
@@ -165,70 +97,49 @@ The above also sets up two Collators for you - Catalog and TechDocs - which will
### Frontend
The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties,
including `<SearchFilter.Select />` and `<SearchFilter.Checkbox />`. These allow
you to provide values relevant to your Backstage instance that, when selected,
get passed to the backend.
The search plugin provides extension points for customizing the search
experience through blueprints. You can add custom search result list items,
filters, and result type filters.
```tsx {2-5,8-11}
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['production', 'experimental']}
/>
</CardContent>
```
If you have advanced filter needs, you can specify your own filter component
like this (although new core filter contributions are welcome):
For example, to create a custom search result list item, use the
`SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`:
```tsx
import { useSearch, SearchFilter } from '@backstage/plugin-search-react';
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
const MyCustomFilter = () => {
// Note: filters contain filter data from other filter components. Be sure
// not to clobber other filters' data!
const { filters, setFilters } = useSearch();
return (/* ... */);
};
// Which could be rendered like this:
<SearchFilter component={MyCustomFilter} />
export const MySearchResultListItem = SearchResultListItemBlueprint.make({
name: 'my-result-item',
params: {
predicate: result => result.type === 'my-custom-type',
component: async () => {
const { MyResultItem } = await import('./components/MyResultItem');
return MyResultItem;
},
},
});
```
It's good practice for search results to highlight information that was used to
return it in the first place! The code below highlights how you might specify a
custom result item component, using the `<CatalogSearchResultListItem />` component as
an example:
Install this in your app by wrapping it in a frontend module and passing it to `createApp`:
```tsx {7-13}
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
// result.type is the index type defined by the collator.
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
// ...
}
})}
</List>
)}
</SearchResult>
```tsx title="packages/app/src/search/searchModule.ts"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { MySearchResultListItem } from './MySearchResultListItem';
export const searchCustomizations = createFrontendModule({
pluginId: 'search',
extensions: [MySearchResultListItem],
});
```
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { searchCustomizations } from './search/searchModule';
const app = createApp({
features: [searchCustomizations],
});
export default app.createRoot();
```
> For more advanced customization of the Search frontend, also see how to guides such as [How to implement your own Search API](./how-to-guides.md#how-to-implement-your-own-search-api) and [How to customize search results highlighting styling](./how-to-guides.md#how-to-customize-search-results-highlighting-styling)
@@ -342,4 +253,4 @@ indexBuilder.addCollator({
});
```
> For more advanced customization of the Search backend, also see how to guides such as [How to index TechDocs documents](./how-to-guides.md#how-to-index-techdocs-documents) and [How to limit what can be searched in the Software Catalog](./how-to-guides.md#how-to-limit-what-can-be-searched-in-the-software-catalog)
> For more advanced customization of the Search backend, also see how to guides such as [How to customize fields in the Software Catalog or TechDocs index](./how-to-guides.md#how-to-customize-fields-in-the-software-catalog-or-techdocs-index)
+367
View File
@@ -0,0 +1,367 @@
---
id: search-how-to-guides--old
title: Search How-To guides (Old Frontend System)
sidebar_label: How-To guides
description: Search How To guides
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./how-to-guides.md) instead.
::::
## How to implement your own Search API
The Search plugin provides implementation of one primary API by default: the
[SearchApi](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L35),
which is responsible for talking to the search-backend to query search results.
There may be occasions where you need to implement this API yourself, to
customize it to your own needs - for example if you have your own search backend
that you want to talk to. The purpose of this guide is to walk you through how
to do that in two steps.
1. Implement the `SearchApi`
[interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31)
according to your needs.
```typescript
export class SearchClient implements SearchApi {
// your implementation
}
```
2. Override the API ref `searchApiRef` with your new implemented API in the
`App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
```
## How to customize fields in the Software Catalog or TechDocs index
Sometimes, you might want to have the ability to control which data passes into the search index
in the catalog collator or customize data for a specific kind. You can easily achieve this
by passing an `entityTransformer` callback to the `DefaultCatalogCollatorFactory`. This behavior
is also possible for the `DefaultTechDocsCollatorFactory`. You can either simply amend the default behavior
or even write an entirely new document (which should still follow some required basic structure).
> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`.
```ts title="packages/backend/src/plugins/search.ts"
const catalogEntityTransformer: CatalogCollatorEntityTransformer = (
entity: Entity,
) => {
if (entity.kind === 'SomeKind') {
return {
// customize here output for 'SomeKind' kind
};
}
return {
// and customize default output
...defaultCatalogCollatorEntityTransformer(entity),
text: 'my super cool text',
};
};
indexBuilder.addCollator({
collator: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: catalogEntityTransformer,
}),
});
const techDocsEntityTransformer: TechDocsCollatorEntityTransformer = (
entity: Entity,
) => {
return {
// add more fields to the index
tags: entity.metadata.tags,
};
};
const techDocsDocumentTransformer: TechDocsCollatorDocumentTransformer = (
doc: MkSearchIndexDoc,
) => {
return {
// add more fields to the index
bost: doc.boost,
};
};
indexBuilder.addCollator({
collator: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: techDocsEntityTransformer,
/* highlight-add-next-line */
documentTransformer: techDocsDocumentTransformer,
}),
});
```
## How to customize search results highlighting styling
The default highlighting styling for matched terms in search results is your
browsers default styles for the `<mark>` HTML tag. If you want to customize
how highlighted terms look you can follow Backstage's guide on how to
[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)
to create an override with your preferred styling.
For example, using the new MUI V4+V5 unified theming method, the following will result
in highlighted words to be bold & underlined:
```typescript jsx title=packages/app/src/theme/theme.ts
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
UnifiedTheme,
} from '@backstage/theme';
export const myLightTheme: UnifiedTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
}),
defaultPageTheme: 'home',
components: {
/** @ts-ignore This is temporarily necessary until MUI V5 transition is completed. */
BackstageHighlightedSearchResultText: {
styleOverrides: {
highlight: {
color: 'inherit',
backgroundColor: 'inherit',
fontWeight: 'bold',
textDecoration: 'underline',
},
},
},
},
});
```
```typescript jsx title= packages/app/src/App.tsx
const app : BackstageApp = createApp({
...
themes: [{
id: 'my-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />)
}]
});
```
Obviously if you wanted a dark theme, you would need to provide that as well.
## How to render search results using extensions
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
### 1. Providing an extension in your plugin package
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
> a rendering function to the `<SearchResult />` component.
Using the example below, you can provide an extension to be used as a search result item:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props:
```tsx
export const YourSearchResultListItemExtension: (
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
) => JSX.Element | null = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
// Only results matching your type will be rendered by this extension
predicate: result => result.type === 'YOUR_RESULT_TYPE',
}),
);
```
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
```tsx title="plugins/your-plugin/src/index.ts"
export { YourSearchResultListItem } from './plugin.ts';
```
For more details, see the [createSearchResultListItemExtension](https://backstage.io/api/stable/functions/_backstage_plugin-search-react.index.createSearchResultListItemExtension.html) API reference.
### 2. Custom search result extension in the SearchPage
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
Here is an example of customizing your `SearchPage`:
```tsx title="packages/app/src/components/searchPage.tsx"
import { Grid, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import {
Page,
Header,
Content,
DocsIcon,
CatalogIcon,
} from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
// This example omits other components, like filter and pagination
const SearchPage = () => (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={12}>
<SearchResult>
<YourSearchResultListItem />
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
export const searchPage = <SearchPage />;
```
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
### 2. Custom search result extension in the SidebarSearchModal
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
```tsx title="packages/app/src/components/Root/Root.tsx"
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
```
### 3. Custom search result extension in a custom SearchModal
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
```tsx title="packages/app/src/components/searchModal.tsx"
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import { DocsIcon, CatalogIcon } from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
<>
<DialogTitle>
<Paper>
<SearchBar />
</Paper>
</DialogTitle>
<DialogContent>
<SearchResult onClick={toggleModal}>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
{/* As a "default" extension, it does not define a predicate function,
so it must be the last child to render results that do not match the above extensions */}
<YourSearchResultListItem />
</SearchResult>
</DialogContent>
</>
);
```
There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions).
+57 -207
View File
@@ -5,6 +5,13 @@ sidebar_label: How-To guides
description: Search How To guides
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./how-to-guides--old.md)
instead.
::::
## How to implement your own Search API
The Search plugin provides implementation of one primary API by default: the
@@ -26,24 +33,9 @@ to do that in two steps.
}
```
2. Override the API ref `searchApiRef` with your new implemented API in the
`App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
```
2. Override the default API extension by creating a custom API extension using
`createApiExtension` from `@backstage/frontend-plugin-api`, and install it
in your app. See the [Utility APIs](../../frontend-system/utility-apis/01-index.md) documentation for details on how to create and install custom API extensions.
## How to customize fields in the Software Catalog or TechDocs index
@@ -119,7 +111,7 @@ how highlighted terms look you can follow Backstage's guide on how to
[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)
to create an override with your preferred styling.
For example, using the new MUI V4+V5 unified theming method, the following will result
For example, using the unified theming method, the following will result
in highlighted words to be bold & underlined:
```typescript jsx title=packages/app/src/theme/theme.ts
@@ -151,211 +143,69 @@ export const myLightTheme: UnifiedTheme = createUnifiedTheme({
});
```
```typescript jsx title= packages/app/src/App.tsx
const app : BackstageApp = createApp({
...
themes: [{
id: 'my-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />)
}]
});
```
Obviously if you wanted a dark theme, you would need to provide that as well.
Custom themes are installed as extensions in the new frontend system. See the
[theming documentation](../../frontend-system/building-apps/02-configuring-extensions.md)
for details on how to install custom themes.
## How to render search results using extensions
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
Extensions for search results let you customize components used to render
search result items. It is possible to provide your own search result item
extensions or use the ones provided by plugin packages.
### 1. Providing an extension in your plugin package
### Providing a search result list item extension
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
> a rendering function to the `<SearchResult />` component.
In the new frontend system, search result list item extensions are created
using the `SearchResultListItemBlueprint` from
`@backstage/plugin-search-react/alpha`:
Using the example below, you can provide an extension to be used as a search result item:
```tsx title="plugins/your-plugin/src/extensions.ts"
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props:
```tsx
export const YourSearchResultListItemExtension: (
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
) => JSX.Element | null = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
// Only results matching your type will be rendered by this extension
export const YourSearchResultListItem = SearchResultListItemBlueprint.make({
name: 'your-result-item',
params: {
predicate: result => result.type === 'YOUR_RESULT_TYPE',
}),
);
component: async () => {
const { YourSearchResultListItem } = await import('./components');
return YourSearchResultListItem;
},
},
});
```
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
The extension is then exported from your plugin's alpha entry point and
automatically discovered when the plugin is installed.
```tsx title="plugins/your-plugin/src/index.ts"
export { YourSearchResultListItem } from './plugin.ts';
If you need to provide a search result list item extension from your app
rather than a plugin, wrap it in a frontend module and pass it to `createApp`:
```tsx title="packages/app/src/search/searchModule.ts"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { YourSearchResultListItem } from './YourSearchResultListItem';
export const searchCustomizations = createFrontendModule({
pluginId: 'search',
extensions: [YourSearchResultListItem],
});
```
For more details, see the [createSearchResultListItemExtension](https://backstage.io/api/stable/functions/_backstage_plugin-search-react.index.createSearchResultListItemExtension.html) API reference.
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { searchCustomizations } from './search/searchModule';
### 2. Custom search result extension in the SearchPage
const app = createApp({
features: [searchCustomizations],
});
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
Here is an example of customizing your `SearchPage`:
```tsx title="packages/app/src/components/searchPage.tsx"
import { Grid, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import {
Page,
Header,
Content,
DocsIcon,
CatalogIcon,
} from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
// This example omits other components, like filter and pagination
const SearchPage = () => (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={12}>
<SearchResult>
<YourSearchResultListItem />
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
export const searchPage = <SearchPage />;
export default app.createRoot();
```
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
### Search result item ordering
### 2. Custom search result extension in the SidebarSearchModal
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
```tsx title="packages/app/src/components/Root/Root.tsx"
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
```
### 3. Custom search result extension in a custom SearchModal
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
```tsx title="packages/app/src/components/searchModal.tsx"
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import { DocsIcon, CatalogIcon } from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
<>
<DialogTitle>
<Paper>
<SearchBar />
</Paper>
</DialogTitle>
<DialogContent>
<SearchResult onClick={toggleModal}>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
{/* As a "default" extension, it does not define a predicate function,
so it must be the last child to render results that do not match the above extensions */}
<YourSearchResultListItem />
</SearchResult>
</DialogContent>
</>
);
```
When multiple search result list item extensions are installed, the search page
uses them to render results based on their predicate functions. The first
extension whose predicate matches a given result is used to render it. Extensions
without a predicate act as fallback renderers and should be ordered last.
There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions).
@@ -0,0 +1,508 @@
---
id: catalog-customization--old
title: Catalog Customization (Old Frontend System)
description: How to add custom filters or interface elements to the Backstage software catalog
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./catalog-customization.md) instead.
::::
The Backstage software catalog comes with a default `CatalogIndexPage` to filter and find catalog entities. This is already set up by default by `@backstage/create-app`. If you want to change the default index page - to set the initially selected filter, adjust columns, add actions, or to add a custom filter to the catalog - the following sections will show you how.
## Pagination
Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of Backstage, so make sure you are on that version or newer to use this feature. To enable pagination you simply need to pass in the `pagination` prop like this:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage pagination />} />
```
## Initially Selected Filter
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage initiallySelectedFilter="all" />}
/>
```
Possible options are: owned, starred, or all
## Initially Selected Kind
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
```
Possible options are all the [default Kinds](system-model.md) as well as any custom Kinds that you have added.
## Owner Picker Mode
The Owner filter by default will only contain a list of Users and/or Groups that actually own an entity in the Catalog, now you may have reason to change this. Here's how:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage ownerPickerMode="all" />} />
```
Possible options are: owners-only or all
## Table Options
The tables used within Backstage are built on top of [`@material-table/core`](https://material-table-core.github.io/) and the `CatalogIndexPage` has a `tableOptions` prop that allows you to customize the underlying table to a certain extent, but there are some hard coded Backstage settings that can't be changed. Here's an example of how to use this prop to disable the search filter field in the table's header:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage tableOptions={{ search: false }} />}
/>
```
There are many options that can be set using `tableOptions`, the full list of settings can be found in the [`@material-table/core` `Options` interface](https://github.com/material-table-core/core/blob/v3.1.0/types/index.d.ts#L323) (this link goes to `v3.1.0` of `@material-table/core` as that is the version currently used by Backstage).
## Customize Columns
The columns you see in the `CatalogIndexPage` were selected to be a good starting point for most, but there may be cases where you would like to add or remove columns from existing or custom Kinds.
### Adding a column to an existing Kind
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* prettier-ignore */ /* highlight-add-end */}
```
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
```tsx title="packages/app/src/App.tsx"
{/* highlight-add-start */}
const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
title: 'User Email',
field: 'entity.spec.profile.email',
render: ({ entity }) => (
<OverflowTooltip
text={entity.spec?.profile?.['email'] || 'N/A'}
placement="bottom-start"
/>
),
});
{/* highlight-add-end */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
{/* highlight-add-next-line */}
createUserEmailColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
```
Finally, we can pass the `myColumnsFunc` to the `CatalogIndexPage` component:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Route
path="/catalog"
element={
<CatalogIndexPage
pagination={{ mode: 'offset', limit: 20 }}
{/* highlight-add-next-line */}
columns={myColumnsFunc}
/>
}
/>
{/* Other routes */}
</FlatRoutes>
)
```
### Adding columns to a custom or specific Kind
Another use case for customization is when adding a custom `Kind`. This feature is available in Backstage >= `v1.23.0`. For example:
```tsx title="packages/app/src/App.tsx"
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
{/* highlight-add-start */}
CatalogTable,
CatalogTableColumnsFunc,
{/* highlight-add-end */}
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'MyKind') {
return [
CatalogTable.columns.createNameColumn(),
CatalogTable.columns.createOwnerColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />
```
:::note Note
In the examples above, the contents of the files have been shortened for simplicity.
:::
## Customize Actions
The `CatalogIndexPage` comes with three default actions - view, edit, and star. You might want to add more.
To do this, first you'll need to add `@mui/utils` to your `packages/app/package.json`:
```sh
yarn --cwd packages/app add @mui/utils
```
Then you'll do the following:
```tsx title="packages/app/src/App.tsx"
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
{/* highlight-add-next-line */}
TableProps,
} from '@backstage/core-components';
import {
CatalogEntityPage,
CatalogIndexPage,
{/* highlight-add-next-line */}
CatalogTableRow,
catalogPlugin,
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
import { Typography } from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { visuallyHidden } from '@mui/utils';
{/* highlight-add-end */}
{/* highlight-add-start */}
const customActions: TableProps<CatalogTableRow>['actions'] = [
({ entity }) => {
const url = 'https://backstage.io/';
const title = `View - ${entity.metadata.name}`;
return {
icon: () => (
<>
<Typography style={visuallyHidden}>{title}</Typography>
<OpenInNew fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
window.open(url, '_blank');
},
};
},
];
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />
```
:::note Note
In the example above, the contents of `App.tsx` has been shortened for simplicity.
:::
The above customization will override the existing actions. Currently, the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168).
## Customize Filters
There are various ways to customize filters: adjusting the existing filters with props, adding or removing default filters, creating brand-new custom filters, etc. The following sections cover these cases:
### Default Filter Props
There are a set of default filters that you can use, which surface all the props mentioned earlier in this document. Here's how they can be used:
```tsx title="packages/app/src/App.tsx"
import { DefaultFilters } from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters
initialKind="Domain"
initiallySelectedFilter="all"
ownerPickerMode="all"
/>
</>
}
/>
}
/>;
```
### Removing Default Filters
If you have reasons not to use the Lifecycle, Tag, and Processing Status filters, here's an example of how to remove them:
```tsx title="packages/app/src/App.tsx"
import {
EntityKindPicker,
EntityTypePicker,
UserListPicker,
EntityOwnerPicker,
EntityNamespacePicker,
} from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityNamespacePicker />
</>
}
/>
}
/>;
```
### Custom Filters
You can add custom filters. For example, suppose that we want to allow filtering by a custom annotation added to entities, `company.com/security-tier`. Here is how we can build a filter to support that need.
First we need to create a new filter that implements the `EntityFilter` interface:
```ts
import { EntityFilter } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
class EntitySecurityTierFilter implements EntityFilter {
constructor(readonly values: string[]) {}
filterEntity(entity: Entity): boolean {
const tier = entity.metadata.annotations?.['company.com/security-tier'];
return tier !== undefined && this.values.includes(tier);
}
}
```
The `EntityFilter` interface permits backend filters, which are passed along to the `catalog-backend` - or frontend filters, which are applied after entities are loaded from the backend.
We'll use this filter to extend the default filters in a type-safe way. Let's create the custom filter shape extending the default somewhere alongside this filter:
```ts
export type CustomFilters = DefaultEntityFilters & {
securityTiers?: EntitySecurityTierFilter;
};
```
To control this filter, we can create a React component that shows checkboxes for the security tiers. This component will make use of the `useEntityList` hook, which accepts this extended filter type as a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) parameter:
```tsx
export const EntitySecurityTierPicker = () => {
// The securityTiers key is recognized due to the CustomFilter generic
const {
filters: { securityTiers },
updateFilters,
} = useEntityList<CustomFilters>();
// Toggles the value, depending on whether it's already selected
function onChange(value: string) {
const newTiers = securityTiers?.values.includes(value)
? securityTiers.values.filter(tier => tier !== value)
: [...(securityTiers?.values ?? []), value];
updateFilters({
securityTiers: newTiers.length
? new EntitySecurityTierFilter(newTiers)
: undefined,
});
}
const tierOptions = ['1', '2', '3'];
return (
<FormControl component="fieldset">
<Typography variant="button">Security Tier</Typography>
<FormGroup>
{tierOptions.map(tier => (
<FormControlLabel
key={tier}
control={
<Checkbox
checked={securityTiers?.values.includes(tier)}
onChange={() => onChange(tier)}
/>
}
label={`Tier ${tier}`}
/>
))}
</FormGroup>
</FormControl>
);
};
```
Now we can add the component to `CatalogIndexPage`:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
import { DefaultFilters } from '@backstage/plugin-catalog-react';
{/* prettier-ignore */ /* highlight-add-end */}
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters />
<EntitySecurityTierPicker />
</>
}
/>
}
/>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
The same method can be used to customize the _default_ filters with a different interface - for such usage, the generic argument isn't needed since the filter shape remains the same as the default.
## Advanced Customization
For those where none of the above fits their needs you can take the option of creating a fully custom `CatalogIndexPage`.
```tsx title="packages/app/src/components/catalog/CustomCatalogIndex.tsx"
import {
PageWithHeader,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { CatalogTable } from '@backstage/plugin-catalog';
import {
EntityListProvider,
CatalogFilterLayout,
EntityKindPicker,
EntityLifecyclePicker,
EntityNamespacePicker,
EntityOwnerPicker,
EntityProcessingStatusPicker,
EntityTagPicker,
EntityTypePicker,
UserListPicker,
} from '@backstage/plugin-catalog-react';
export const CustomCatalogPage = () => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<PageWithHeader title={orgName} themeId="home">
<Content>
<ContentHeader title="">
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider pagination>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
};
```
The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx)
:::note Note
The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
:::
To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route path="/catalog" element={<CatalogIndexPage />}>
<CustomCatalogPage />
</Route>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
@@ -4,423 +4,157 @@ title: Catalog Customization
description: How to add custom filters or interface elements to the Backstage software catalog
---
The Backstage software catalog comes with a default `CatalogIndexPage` to filter and find catalog entities. This is already set up by default by `@backstage/create-app`. If you want to change the default index page - to set the initially selected filter, adjust columns, add actions, or to add a custom filter to the catalog - the following sections will show you how.
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./catalog-customization--old.md)
instead.
::::
## Pagination
The Backstage software catalog comes with a default catalog index page and entity pages that are highly configurable through `app-config.yaml`. This guide covers how to customize the catalog in the new frontend system.
Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of Backstage, so make sure you are on that version or newer to use this feature. To enable pagination you simply need to pass in the `pagination` prop like this:
## Catalog index page
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage pagination />} />
The catalog index page can be configured through extensions in `app-config.yaml`. For example, to enable pagination:
```yaml title="app-config.yaml"
app:
extensions:
- page:catalog:
config:
pagination: true
```
## Initially Selected Filter
You can also configure pagination with additional options:
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage initiallySelectedFilter="all" />}
/>
```yaml title="app-config.yaml"
app:
extensions:
- page:catalog:
config:
pagination:
mode: offset
limit: 20
```
Possible options are: owned, starred, or all
### Catalog filters
## Initially Selected Kind
The catalog index page includes a set of default filters (kind, type, owner, lifecycle, tag, namespace, processing status). These filters can be configured through extensions. For example, to set the initial kind filter:
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
```yaml title="app-config.yaml"
app:
extensions:
- catalog-filter:catalog/kind:
config:
initialFilter: domain
```
Possible options are all the [default Kinds](system-model.md) as well as any custom Kinds that you have added.
To set the initial list filter to "all" instead of "owned":
## Owner Picker Mode
The Owner filter by default will only contain a list of Users and/or Groups that actually own an entity in the Catalog, now you may have reason to change this. Here's how:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage ownerPickerMode="all" />} />
```yaml title="app-config.yaml"
app:
extensions:
- catalog-filter:catalog/list:
config:
initialFilter: all
```
Possible options are: owners-only or all
### Custom filters
## Table Options
You can create custom catalog filters using the `CatalogFilterBlueprint` from `@backstage/plugin-catalog-react/alpha`. For example, to add a custom security tier filter:
The tables used within Backstage are built on top of [`@material-table/core`](https://material-table-core.github.io/) and the `CatalogIndexPage` has a `tableOptions` prop that allows you to customize the underlying table to a certain extent, but there are some hard coded Backstage settings that can't be changed. Here's an example of how to use this prop to disable the search filter field in the table's header:
```tsx title="packages/app/src/catalog/SecurityTierFilter.tsx"
import { CatalogFilterBlueprint } from '@backstage/plugin-catalog-react/alpha';
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage tableOptions={{ search: false }} />}
/>
```
There are many options that can be set using `tableOptions`, the full list of settings can be found in the [`@material-table/core` `Options` interface](https://github.com/material-table-core/core/blob/v3.1.0/types/index.d.ts#L323) (this link goes to `v3.1.0` of `@material-table/core` as that is the version currently used by Backstage).
## Customize Columns
The columns you see in the `CatalogIndexPage` were selected to be a good starting point for most, but there may be cases where you would like to add or remove columns from existing or custom Kinds.
### Adding a column to an existing Kind
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* prettier-ignore */ /* highlight-add-end */}
```
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
```tsx title="packages/app/src/App.tsx"
{/* highlight-add-start */}
const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
title: 'User Email',
field: 'entity.spec.profile.email',
render: ({ entity }) => (
<OverflowTooltip
text={entity.spec?.profile?.['email'] || 'N/A'}
placement="bottom-start"
/>
),
});
{/* highlight-add-end */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
{/* highlight-add-next-line */}
createUserEmailColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
```
Finally, we can pass the `myColumnsFunc` to the `CatalogIndexPage` component:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Route
path="/catalog"
element={
<CatalogIndexPage
pagination={{ mode: 'offset', limit: 20 }}
{/* highlight-add-next-line */}
columns={myColumnsFunc}
/>
}
/>
{/* Other routes */}
</FlatRoutes>
)
```
### Adding columns to a custom or specific Kind
Another use case for customization is when adding a custom `Kind`. This feature is available in Backstage >= `v1.23.0`. For example:
```tsx title="packages/app/src/App.tsx"
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
{/* highlight-add-start */}
CatalogTable,
CatalogTableColumnsFunc,
{/* highlight-add-end */}
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'MyKind') {
return [
CatalogTable.columns.createNameColumn(),
CatalogTable.columns.createOwnerColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />
```
:::note Note
In the examples above, the contents of the files have been shortened for simplicity.
:::
## Customize Actions
The `CatalogIndexPage` comes with three default actions - view, edit, and star. You might want to add more.
To do this, first you'll need to add `@mui/utils` to your `packages/app/package.json`:
```sh
yarn --cwd packages/app add @mui/utils
```
Then you'll do the following:
```tsx title="packages/app/src/App.tsx"
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
{/* highlight-add-next-line */}
TableProps,
} from '@backstage/core-components';
import {
CatalogEntityPage,
CatalogIndexPage,
{/* highlight-add-next-line */}
CatalogTableRow,
catalogPlugin,
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
import { Typography } from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { visuallyHidden } from '@mui/utils';
{/* highlight-add-end */}
{/* highlight-add-start */}
const customActions: TableProps<CatalogTableRow>['actions'] = [
({ entity }) => {
const url = 'https://backstage.io/';
const title = `View - ${entity.metadata.name}`;
return {
icon: () => (
<>
<Typography style={visuallyHidden}>{title}</Typography>
<OpenInNew fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
window.open(url, '_blank');
},
};
export const securityTierFilter = CatalogFilterBlueprint.make({
name: 'security-tier',
params: {
loader: async () => {
const { EntitySecurityTierPicker } = await import(
'./EntitySecurityTierPicker'
);
return <EntitySecurityTierPicker />;
},
},
];
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />
});
```
:::note Note
Then install it as a frontend module:
In the example above, the contents of `App.tsx` has been shortened for simplicity.
```tsx title="packages/app/src/catalog/catalogCustomizations.tsx"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { securityTierFilter } from './SecurityTierFilter';
:::
The above customization will override the existing actions. Currently, the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168).
## Customize Filters
There are various ways to customize filters: adjusting the existing filters with props, adding or removing default filters, creating brand-new custom filters, etc. The following sections cover these cases:
### Default Filter Props
There are a set of default filters that you can use, which surface all the props mentioned earlier in this document. Here's how they can be used:
```tsx title="packages/app/src/App.tsx"
import { DefaultFilters } from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters
initialKind="Domain"
initiallySelectedFilter="all"
ownerPickerMode="all"
/>
</>
}
/>
}
/>;
export default createFrontendModule({
pluginId: 'catalog',
extensions: [securityTierFilter],
});
```
### Removing Default Filters
If you have reasons not to use the Lifecycle, Tag, and Processing Status filters, here's an example of how to remove them:
Then register the module in your app:
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import catalogCustomizations from './catalog/catalogCustomizations';
const app = createApp({
features: [catalogCustomizations],
});
export default app.createRoot();
```
### Removing default filters
Default filters can be disabled through `app-config.yaml` by setting them to `false`:
```yaml title="app-config.yaml"
app:
extensions:
- catalog-filter:catalog/lifecycle: false
- catalog-filter:catalog/tag: false
- catalog-filter:catalog/processing-status: false
```
## Customizing columns, actions, and table options
In the old frontend system, customizing the catalog table columns, row actions,
and table options was done by passing props directly to the `CatalogIndexPage`
component. In the new frontend system, these customizations are done by
overriding the `page:catalog` extension.
For example, to customize the catalog index page with custom columns or actions,
you can override the page extension using a frontend module:
```tsx title="packages/app/src/catalog/customCatalogPage.tsx"
import {
EntityKindPicker,
EntityTypePicker,
UserListPicker,
EntityOwnerPicker,
EntityNamespacePicker,
} from '@backstage/plugin-catalog-react';
PageBlueprint,
createFrontendModule,
createRouteRef,
} from '@backstage/frontend-plugin-api';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityNamespacePicker />
</>
}
/>
}
/>;
const customCatalogPage = PageBlueprint.make({
params: {
path: '/catalog',
routeRef: createRouteRef({ aliasFor: 'catalog.catalogIndex' }),
loader: async () => {
const { CustomCatalogPage } = await import('./CustomCatalogPage');
return <CustomCatalogPage />;
},
},
});
export default createFrontendModule({
pluginId: 'catalog',
extensions: [customCatalogPage],
});
```
### Custom Filters
Inside your custom catalog page component you have full control over the table
columns, actions, and options. You can compose a page using components from
`@backstage/plugin-catalog` and `@backstage/plugin-catalog-react`:
You can add custom filters. For example, suppose that we want to allow filtering by a custom annotation added to entities, `company.com/security-tier`. Here is how we can build a filter to support that need.
First we need to create a new filter that implements the `EntityFilter` interface:
```ts
import { EntityFilter } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
class EntitySecurityTierFilter implements EntityFilter {
constructor(readonly values: string[]) {}
filterEntity(entity: Entity): boolean {
const tier = entity.metadata.annotations?.['company.com/security-tier'];
return tier !== undefined && this.values.includes(tier);
}
}
```
The `EntityFilter` interface permits backend filters, which are passed along to the `catalog-backend` - or frontend filters, which are applied after entities are loaded from the backend.
We'll use this filter to extend the default filters in a type-safe way. Let's create the custom filter shape extending the default somewhere alongside this filter:
```ts
export type CustomFilters = DefaultEntityFilters & {
securityTiers?: EntitySecurityTierFilter;
};
```
To control this filter, we can create a React component that shows checkboxes for the security tiers. This component will make use of the `useEntityList` hook, which accepts this extended filter type as a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) parameter:
```tsx
export const EntitySecurityTierPicker = () => {
// The securityTiers key is recognized due to the CustomFilter generic
const {
filters: { securityTiers },
updateFilters,
} = useEntityList<CustomFilters>();
// Toggles the value, depending on whether it's already selected
function onChange(value: string) {
const newTiers = securityTiers?.values.includes(value)
? securityTiers.values.filter(tier => tier !== value)
: [...(securityTiers?.values ?? []), value];
updateFilters({
securityTiers: newTiers.length
? new EntitySecurityTierFilter(newTiers)
: undefined,
});
}
const tierOptions = ['1', '2', '3'];
return (
<FormControl component="fieldset">
<Typography variant="button">Security Tier</Typography>
<FormGroup>
{tierOptions.map(tier => (
<FormControlLabel
key={tier}
control={
<Checkbox
checked={securityTiers?.values.includes(tier)}
onChange={() => onChange(tier)}
/>
}
label={`Tier ${tier}`}
/>
))}
</FormGroup>
</FormControl>
);
};
```
Now we can add the component to `CatalogIndexPage`:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
import { DefaultFilters } from '@backstage/plugin-catalog-react';
{/* prettier-ignore */ /* highlight-add-end */}
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters />
<EntitySecurityTierPicker />
</>
}
/>
}
/>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
The same method can be used to customize the _default_ filters with a different interface - for such usage, the generic argument isn't needed since the filter shape remains the same as the default.
## Advanced Customization
For those where none of the above fits their needs you can take the option of creating a fully custom `CatalogIndexPage`.
```tsx title="packages/app/src/components/catalog/CustomCatalogIndex.tsx"
```tsx title="packages/app/src/catalog/CustomCatalogPage.tsx"
import {
PageWithHeader,
Content,
@@ -475,41 +209,20 @@ export const CustomCatalogPage = () => {
};
```
The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx)
:::note Note
The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
The catalog index page is designed to have a minimal code footprint to support
easy customization, but creating a replica does introduce a possibility of
drifting out of date over time. Be sure to check the catalog
[CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md)
periodically.
:::
To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change:
For more details on extension overrides and the different override patterns
available, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route path="/catalog" element={<CatalogIndexPage />}>
<CustomCatalogPage />
</Route>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
## New Frontend System
This section of the documentation explains how to create and configure catalog extensions in the [new frontend system](../../frontend-system/index.md).
:::warning Warning
This section is a work in progress.
:::
## Entity page
### Entity filters

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