Merge branch 'master' of https://github.com/backstage/backstage into fix/scaffolder-after-zero-query-param
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-module-new': patch
|
||||
---
|
||||
|
||||
Rename the legacy `frontend-plugin` to `frontend-plugin-legacy`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bumped create-app version.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bumped create-app version.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Add GitLab SCM event translation layer for instant catalog reprocessing.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
DefaultActionsRegistryService: add json middleware to /.backstage/actions/ routes only
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Add an initial CheckboxGroup component implementation and docs coverage.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
---
|
||||
|
||||
Support configuring `showArrowHeads` on `page:catalog-graph` and `entity-card:catalog-graph/relations`.
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Use Backstage Link component for markdown anchor rendering to ensure consistent internal and external link behavior.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Fixed the Tabs active indicator not hiding when no tab matches the current route.
|
||||
|
||||
**Affected components:** Tabs
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -0,0 +1 @@
|
||||
Fix OAuth 2.0 Protected Resource Metadata endpoint returning wrong URL
|
||||
@@ -0,0 +1 @@
|
||||
Fixed incorrect name of the `legacy-frontend-plugin` template
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue with missing permissions on the `/.well-known` endpoint for the scaffolder plugin
|
||||
+1
-1
@@ -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:**
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
@@ -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
Reference in New Issue
Block a user