From aac778ba8c58c66ae02541c4a2fe0d88383872ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:05:09 +0100 Subject: [PATCH] docs: adapt cross-cutting plugin docs into the frontend system section Instead of just referencing the legacy docs/plugins documentation, adapt and add them as new pages under frontend-system/building-plugins/ with updated imports and examples for the new frontend system. Also moves the OpenAPI sidebar entry under Framework instead of top-level. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-plugins/01-index.md | 10 +- .../07-internationalization.md | 410 +++++++++++++ .../building-plugins/08-analytics.md | 317 ++++++++++ .../building-plugins/09-feature-flags.md | 90 +++ .../10-integrating-search-into-plugins.md | 550 ++++++++++++++++++ docs/plugins/analytics.md | 2 +- docs/plugins/feature-flags.md | 2 +- .../integrating-search-into-plugins.md | 2 +- docs/plugins/internationalization.md | 2 +- microsite/sidebars.ts | 28 +- mkdocs.yml | 8 +- 11 files changed, 1396 insertions(+), 25 deletions(-) create mode 100644 docs/frontend-system/building-plugins/07-internationalization.md create mode 100644 docs/frontend-system/building-plugins/08-analytics.md create mode 100644 docs/frontend-system/building-plugins/09-feature-flags.md create mode 100644 docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 795249c73f..24a815ef7b 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -241,9 +241,9 @@ For a more complete list of the different kinds of extensions that you can creat ## Related topics -The following guides cover cross-cutting concerns that apply to both the old and new frontend systems: +The following guides cover cross-cutting concerns for building frontend plugins: -- [Internationalization (i18n)](../../plugins/internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. -- [Plugin Analytics](../../plugins/analytics.md) — Instrumenting user interactions with the Analytics API, including the `AnalyticsImplementationBlueprint` for the new frontend system. -- [Feature Flags](../../plugins/feature-flags.md) — Defining and using feature flags. In the new frontend system, flags are declared in the `featureFlags` option of `createFrontendPlugin`. -- [Integrating Search into Plugins](../../plugins/integrating-search-into-plugins.md) — Building search experiences and collators. For the new frontend system, use `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. +- [Internationalization (i18n)](./07-internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. +- [Plugin Analytics](./08-analytics.md) — Instrumenting user interactions with the Analytics API using `AnalyticsImplementationBlueprint`. +- [Feature Flags](./09-feature-flags.md) — Defining and using feature flags via the `featureFlags` option of `createFrontendPlugin`. +- [Integrating Search into Plugins](./10-integrating-search-into-plugins.md) — Building search experiences, collators, and custom result list items using `SearchResultListItemBlueprint`. diff --git a/docs/frontend-system/building-plugins/07-internationalization.md b/docs/frontend-system/building-plugins/07-internationalization.md new file mode 100644 index 0000000000..270f1f6625 --- /dev/null +++ b/docs/frontend-system/building-plugins/07-internationalization.md @@ -0,0 +1,410 @@ +--- +id: internationalization +title: Internationalization +sidebar_label: Internationalization +description: Adding internationalization to plugins and apps +--- + +## Overview + +The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. + +## For a plugin developer + +When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example: + +```ts +import { createTranslationRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + indexPage: { + title: 'All your components', + createButtonTitle: 'Create new component', + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +And then use these messages in your components like: + +```tsx +import { useTranslationRef } from '@backstage/frontend-plugin-api'; + +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( + + + +); +``` + +You will see how the initial dictionary structure and nesting get converted into dot notation, so we encourage `camelCase` in key names and lean on the nesting structure to separate keys. + +### Guidelines for `i18n` messages and keys + +The API for `i18n` messages and keys can be pretty tricky to get right, as it's a pretty flexible API. We've put together some guidelines to help you get started that encourage good practices when thinking about translating plugins: + +#### Key names + +When defining messages it is recommended to use a nested structure that represents the semantic hierarchy in your translations. This allows for better organization and understanding of the structure. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + widgets: { + weather: { + title: 'Weather', + description: 'Shows the weather', + }, + calendar: { + title: 'Calendar', + description: 'Shows the calendar', + }, + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +Think about the semantic placement of content rather than the text content itself. Group related translations under a common prefix, and use nesting to represent relationships between different parts of your application. It's good to start grouping under extensions, page sections, or visual scopes and experiences. + +Translations should avoid using their own text content as key where possible, as this can lead to confusion if the translation changes. Instead prefer to use keys that describe the location or usage of the text. + +#### Common Key names + +This list is intended to grow over time, but below are some examples of common key names and patterns that we encourage you to use where possible: + +- `${page}.title` +- `${page}.subtitle` +- `${page}.description` + +- `${page}.header.title` + +#### Key reuse + +Reusing the same key in multiple places is discouraged. This helps prevent ambiguity, and instead keeps the usage of each key as clear as possible. Consider creating duplicate keys that are grouped under a semantic section instead. + +#### Flat keys + +Avoid a flat key structure at the root level, as it can lead to naming conflicts and make the translation file harder to manage and change evolve over time. Instead, group translations under a common prefix. + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + // this is BAD + title: 'My page', + subtitle: 'My subtitle', + // this is GOOD + dashboardPage: { + header: { + title: 'All your components', + subtitle: 'Create new component', + }, + }, + }, +}); +``` + +#### Plurals + +The `i18next` library, which is used as the underlying implementation, has built-in support for pluralization. You can use this feature as is described in [the documentation](https://www.i18next.com/translation-function/plurals). + +We encourage you to use this feature and avoid creating different key prefixes for pluralized content. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + cards: { + title_one: 'You have one card', + title_two: 'You have two cards', + title_other: 'You have many cards ({{count}})', + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +#### JSX Elements + +The translation API supports interpolation of JSX elements by passing them directly as values to the translation function. If any of the provided interpolation values are JSX elements, the translation function will return a JSX element instead of a string. + +For example, you might define the following messages: + +```ts title="define the message" +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + entityPage: { + redirect: { + message: 'The entity you are looking for has been moved to {{link}}.', + link: 'new location', + }, + }, + }, +}); +``` + +Which can be used within a component like this: + +```tsx title="use within a component" +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( +
+ {t('entityPage.redirect.message', { + link: {t('entityPage.redirect.link')}, + })} +
+); +``` + +The return type of the outer `t` function will be a `JSX.Element`, with the underlying value being a React fragment of the different parts of the message. + +## For an application developer + +As an app developer you can both override the default English messages of any plugin, and provide translations for additional languages. + +### Overriding messages + +To customize specific messages without adding new languages, create a translation resource that overrides the default English messages: + +```ts +// packages/app/src/translations/catalog.ts + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +export const catalogTranslations = createTranslationResource({ + ref: catalogTranslationRef, + translations: { + en: () => + Promise.resolve({ + default: { + 'indexPage.title': 'Service directory', + 'indexPage.createButtonTitle': 'Register new service', + }, + }), + }, +}); +``` + +Then register it in your app: + +```diff ++ import { catalogTranslations } from './translations/catalog'; + + const app = createApp({ ++ __experimentalTranslations: { ++ resources: [catalogTranslations], ++ }, + }) +``` + +You only need to include the keys you want to override — any missing keys fall back to the plugin's defaults. + +### Adding language translations + +To add support for additional languages, create translation resources with lazy-loaded message files for each language: + +```ts +// packages/app/src/translations/userSettings.ts + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +export const userSettingsTranslations = createTranslationResource({ + ref: userSettingsTranslationRef, + translations: { + zh: () => import('./userSettings-zh'), + }, +}); +``` + +The translation messages can be defined using `createTranslationMessages` for type safety: + +```ts +// packages/app/src/translations/userSettings-zh.ts + +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +const zh = createTranslationMessages({ + ref: userSettingsTranslationRef, + full: false, // False means that this is a partial translation + messages: { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + }, +}); + +export default zh; +``` + +Or as a plain object export: + +```ts +// packages/app/src/translations/userSettings-zh.ts +export default { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + 'languageToggle.description': '切换语言', + 'themeToggle.title': '主题', + 'themeToggle.description': '切换主题', + 'themeToggle.select': '选择{{theme}}', + 'themeToggle.selectAuto': '选择自动主题', + 'themeToggle.names.auto': '自动', + 'themeToggle.names.dark': '暗黑', + 'themeToggle.names.light': '明亮', +}; +``` + +Register it with the available languages declared: + +```diff ++ import { userSettingsTranslations } from './translations/userSettings'; + + const app = createApp({ ++ __experimentalTranslations: { ++ availableLanguages: ['en', 'zh'], ++ resources: [userSettingsTranslations], ++ }, + }) +``` + +Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly. + +### Using the CLI for full translation workflows + +When translating your app to other languages at scale — especially when working with external translation systems — the Backstage CLI provides `translations export` and `translations import` commands that automate the extraction and wiring of translation messages across all your plugin dependencies. + +#### Exporting default messages + +From your app package directory (e.g. `packages/app`), run: + +```bash +yarn backstage-cli translations export +``` + +This scans all frontend plugin dependencies (including transitive ones) for `TranslationRef` definitions and writes their default English messages as JSON files: + +```text +translations/ + manifest.json + messages/ + catalog.en.json + org.en.json + scaffolder.en.json + ... +``` + +Each `.en.json` file contains the flattened message keys and their default values: + +```json +{ + "indexPage.title": "All your components", + "indexPage.createButtonTitle": "Create new component", + "entityPage.notFound": "Entity not found" +} +``` + +#### Creating translations + +Copy the exported files and translate them for your target languages: + +```bash +cp translations/messages/catalog.en.json translations/messages/catalog.zh.json +``` + +Then edit `catalog.zh.json` with the translated strings. You only need to include the keys you want to translate — missing keys fall back to the English defaults at runtime. + +#### Generating wiring code + +Once you have translated files in place, run: + +```bash +yarn backstage-cli translations import +``` + +This generates a TypeScript module at `src/translations/resources.ts` that wires everything together: + +```ts +// This file is auto-generated by backstage-cli translations import +// Do not edit manually. + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +export default [ + createTranslationResource({ + ref: catalogTranslationRef, + translations: { + zh: () => import('../../translations/messages/catalog.zh.json'), + }, + }), +]; +``` + +Import the generated resources in your app: + +```ts +import translationResources from './translations/resources'; + +const app = createApp({ + __experimentalTranslations: { + availableLanguages: ['en', 'zh'], + resources: translationResources, + }, +}); +``` + +#### Custom file patterns + +By default, message files use the pattern `messages/{id}.{lang}.json` (e.g. `messages/catalog.en.json`). You can change this with the `--pattern` option: + +```bash +yarn backstage-cli translations export --pattern '{lang}/{id}.json' +``` + +This produces a directory structure grouped by language instead: + +```text +translations/en/catalog.json +translations/zh/catalog.json +``` + +The pattern is stored in the manifest, so the `import` command automatically uses the same layout. + +#### Integration with external translation systems + +The exported JSON files are standard key-value pairs compatible with most external translation systems. A typical workflow looks like: + +1. Run `translations export` to generate the source English files +2. Upload the `.en.json` files to your translation system +3. Download the translated files back into the translations directory +4. Run `translations import` to regenerate the wiring code + +For full command reference, see the [CLI commands documentation](../../tooling/cli/03-commands.md#translations-export). diff --git a/docs/frontend-system/building-plugins/08-analytics.md b/docs/frontend-system/building-plugins/08-analytics.md new file mode 100644 index 0000000000..08babdb612 --- /dev/null +++ b/docs/frontend-system/building-plugins/08-analytics.md @@ -0,0 +1,317 @@ +--- +id: analytics +title: Plugin Analytics +sidebar_label: Analytics +description: Measuring usage of your Backstage instance +--- + +Setting up, maintaining, and iterating on an instance of Backstage can be a +large investment. To help measure return on this investment, Backstage comes +with an event-based Analytics API that grants app integrators the flexibility to +collect and analyze Backstage usage in the analytics tool of their choice, while +providing plugin developers a standard interface for instrumenting key user +interactions. + +## Concepts + +- **Events** consist of, at a minimum, an `action` (like `click`) and a + `subject` (like `thing that was clicked on`). +- **Attributes** represent additional dimensional data (in the form of key/value + pairs) that may be provided on an event-by-event basis. To continue the above + example, the URL a user clicked to might look like `{ "to": "/a/page" }`. +- **Context** represents the broader context in which an event took place. By + default, information like `pluginId`, `extension`, and `routeRef` are + provided. + +This composition of events aims to allow analysis at different levels of detail, +enabling very granular questions (like "what is the most clicked on thing on a +particular route") as well as very high-level questions (like "what is the most +used plugin in my Backstage instance") to be answered. + +## Supported Analytics Tools + +While all that's needed to consume and forward these events to an analytics tool +is a concrete implementation of [AnalyticsApi][analytics-api-type], common +integrations are packaged and provided as plugins. Find your analytics tool of +choice below. + +| Analytics Tool | Support Status | +| ------------------------------------- | -------------- | +| [Google Analytics][ga] | Yes ✅ | +| [Google Analytics 4][ga4] | Yes ✅ | +| [New Relic Browser][newrelic-browser] | Community ✅ | +| [Matomo][matomo] | Community ✅ | +| [Quantum Metric][qm] | Community ✅ | +| [Generic HTTP][generic-http] | Community ✅ | + +To suggest an integration, please [open an issue][add-tool] for the analytics +tool your organization uses. Or jump to [Writing Integrations][int-howto] to +learn how to contribute the integration yourself! + +[ga]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga/README.md +[ga4]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga4/README.md +[newrelic-browser]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-newrelic-browser/README.md +[qm]: https://github.com/quantummetric/analytics-module-qm/blob/main/README.md +[matomo]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-matomo/README.md +[add-tool]: https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE +[int-howto]: #writing-integrations +[analytics-api-type]: https://backstage.io/api/stable/types/_backstage_core-plugin-api.index.AnalyticsApi.html +[generic-http]: https://github.com/pfeifferj/backstage-plugin-analytics-generic/blob/main/README.md + +## Key Events + +The following table summarizes events that, depending on the plugins you have +installed, may be captured. + +| Action | Subject | Other Notes | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `navigate` | The URL of the page that was navigated to. | Fired immediately when route location changes (unless associated plugin/route data is ambiguous, in which case the event is fired after plugin/route data becomes known, immediately before the next event or document unload). The parameters of the current route will be included as attributes. | +| `click` | The text of the link that was clicked on. | The `to` attribute represents the URL clicked to. | +| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`). The `value` represents the number of minutes saved by running the template (based on the template's `backstage.io/time-saved` annotation, if available). | +| `search` | The search term entered in any search bar component. | The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | +| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided. | +| `not-found` | The path of the resource that resulted in a not found page | Fired by at least TechDocs. | + +If there is an event you'd like to see captured, please [open an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md&title=[Analytics%20Event]:%20THE+EVENT+TO+CAPTURE) describing the event you want to see and the questions it +would help you answer. Or jump to [Capturing Events](#capturing-events) to learn how +to contribute the instrumentation yourself! + +_OSS plugin maintainers: feel free to document your events in the table above._ + +## Writing Integrations + +Analytics event forwarding is implemented as a Backstage [Utility API](../utility-apis/01-index.md). The +provided API need only provide a single method `captureEvent`, which takes +an `AnalyticsEvent` object. + +A simple implementation using `AnalyticsImplementationBlueprint`: + +```ts +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: {}, + factory() { + return { + captureEvent: event => { + window._AcmeAnalyticsQ.push(event); + }, + }; + }, + }), + }); +``` + +In reality, you would likely want to encapsulate instantiation logic and pull +some details from configuration. A more complete example might look like: + +```ts +import { + AnalyticsApi, + AnalyticsEvent, + configApiRef, +} from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; +import { AcmeAnalytics } from 'acme-analytics'; + +class AcmeAnalyticsImpl implements AnalyticsApi { + private constructor(accountId: number) { + AcmeAnalytics.init(accountId); + } + + static fromConfig(config) { + const accountId = config.getString('app.analytics.acme.id'); + return new AcmeAnalyticsImpl(accountId); + } + + captureEvent(event: AnalyticsEvent) { + const { action, ...rest } = event; + AcmeAnalytics.send(action, rest); + } +} + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalyticsImpl.fromConfig(configApi), + }), + }); +``` + +If you are integrating with an analytics service (as opposed to an internal +tool), consider contributing your API implementation as a plugin! + +By convention, such packages should be named +`@backstage/analytics-module-[name]`, and any configuration should be keyed +under `app.analytics.[name]`. + +### Handling User Identity + +If the analytics platform you are integrating with has a first-class concept of +user identity, you can (optionally) choose to support this by the following this +convention: + +- Allow your implementation to be instantiated with the `identityApi` as one of + its dependencies. +- Use the `userEntityRef` resolved by `identityApi`'s `getBackstageIdentity()` + method as the basis for the user ID you send to your analytics platform. + +## Capturing Events + +To instrument an event in a component, start by retrieving an analytics tracker +using the `useAnalytics()` hook provided by `@backstage/frontend-plugin-api`. The +tracker includes a `captureEvent` method which takes an `action` and a `subject` +as arguments. + +```ts +import { useAnalytics } from '@backstage/frontend-plugin-api'; + +const analytics = useAnalytics(); +analytics.captureEvent('deploy', serviceName); +``` + +### Providing Extra Attributes + +Additional dimensional `attributes` as well as a numeric `value` can be provided +on a third `options` argument if/when relevant for the event: + +```ts +analytics.captureEvent('merge', pullRequestName, { + value: pullRequestAgeInMinutes, + attributes: { + org, + repo, + }, +}); +``` + +In the above example, an event resembling the following object would be +captured: + +```json +{ + "action": "merge", + "subject": "Name of Pull Request", + "value": 60, + "attributes": { + "org": "some-org", + "repo": "some-repo" + } +} +``` + +### Providing Context for Events + +The `attributes` option is good for capturing details available to you within +the component that you're instrumenting. For capturing metadata only available +further up the react tree, or to help app integrators aggregate distinct events +by some common value, use an ``. + +```tsx +import { AnalyticsContext, useAnalytics } from '@backstage/frontend-plugin-api'; + +const MyComponent = ({ value }) => { + const analytics = useAnalytics(); + const handleClick = () => analytics.captureEvent('check', value); + return ; +}; + +const MyWrapper = () => { + return ( + + + + ); +}; +``` + +In the above example, clicking on `` would result in an analytics +event resembling: + +```json +{ + "action": "check", + "subject": "Some Value", + "context": { + "segment": "xyz" + } +} +``` + +Note that, for brevity in the example above, the context keys provided by +Backstage core (`pluginId`, `extension`, and `routeRef`) have been omitted. In +reality, those details would be included alongside any additional context +provided by you. + +Analytics contexts can be nested; their values are merged down the react tree, +allowing keys to be overwritten. + +### Event Naming Considerations + +An event is split into its constituent parts to enable analysis at various +levels of granularity. In order to maintain this flexibility at analysis-time, +it's important to keep each of these levels of detail disaggregated. + +- Avoid providing an overly specific `action`. For example, instead of + `filterEntityTable`, consider just using `filter` as the action, and allowing + `EntityTable` to be specified as part of the event's `context` (most likely + automatically as part of the `extension` in which the `filter` event was + captured). + +- On the flip side, when adding `attributes` to or `context` around an event, + look at existing events and see if the data you are capturing matches the + intention, type, or even the content of _their_ `attributes` or `context`. + For instance, it's common for events that involve the Catalog to include an + `entityRef` contextual key. Using the same keys and values in your event will + ensure that events instrumented across plugins can easily be aggregated. + +### Unit Testing Event Capture + +The `@backstage/frontend-test-utils` package includes a `MockAnalyticsApi` implementation +that you can use in your unit tests to spy on and make assertions about any +analytics events captured. + +Use it like this: + +```tsx +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { analyticsApiRef } from '@backstage/frontend-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/frontend-test-utils'; + +describe('SomeComponent', () => { + it('should capture event on click', () => { + const apiSpy = new MockAnalyticsApi(); + + const { getByText } = render( + wrapInTestApp( + + + , + ), + ); + + fireEvent.click(getByText('some component text')); + + await waitFor(() => { + expect(apiSpy.getEvents()[0]).toMatchObject({ + action: 'expected action', + subject: 'expected subject', + attributes: { + foo: 'bar', + }, + }); + }); + }); +}); +``` diff --git a/docs/frontend-system/building-plugins/09-feature-flags.md b/docs/frontend-system/building-plugins/09-feature-flags.md new file mode 100644 index 0000000000..0d3f60da10 --- /dev/null +++ b/docs/frontend-system/building-plugins/09-feature-flags.md @@ -0,0 +1,90 @@ +--- +id: feature-flags +title: Feature Flags +sidebar_label: Feature Flags +description: Defining and using feature flags in plugins and apps +--- + +Backstage offers the ability to define feature flags inside a plugin or during application creation. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. + +This page describes the process of defining, setting and reading a feature flag. If you are looking for using feature flags specifically with software templates please see [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). + +## Defining a Feature Flag + +### In a plugin + +Feature flags are declared via the `featureFlags` option in `createFrontendPlugin`: + +```ts title="src/plugin.ts" +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; + +export const examplePlugin = createFrontendPlugin({ + pluginId: 'example', + featureFlags: [ + { + name: 'show-example-feature', + description: 'Enables the new beta dashboard view', + }, + ], + extensions: [ + // ... + ], +}); +``` + +Note that the `description` property is optional. If not provided, the default "Registered in {pluginId} plugin" message is shown. + +### In the application + +Defining a feature flag in the application is done by adding feature flags in the `featureFlags` array in the +`createApp()` function call: + +```ts title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; + +const app = createApp({ + // ... + featureFlags: [ + { + name: 'tech-radar', + description: 'Enables the tech radar plugin', + }, + ], + // ... +}); +``` + +## Enabling Feature Flags + +Feature flags are defaulted to off and can be updated by individual users in the backstage interface. These are set by navigating to the page under `Settings` > `Feature Flags`. + +The user's selection is saved in the user's browser local storage. Once a feature flag is toggled it may be required for a user to refresh the page to see the change. + +## FeatureFlagged Component + +The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/api/stable/functions/_backstage_core-app-api.FeatureFlagged.html) component. + +```ts +import { FeatureFlagged } from '@backstage/core-app-api'; + +... + + + + + + + + +``` + +## Evaluating Feature Flag State + +It is also possible to query a feature flag using the [FeatureFlags Api](https://backstage.io/api/stable/interfaces/_backstage_core-plugin-api.index.FeatureFlagsApi.html). + +```ts +import { useApi, featureFlagsApiRef } from '@backstage/frontend-plugin-api'; + +const featureFlagsApi = useApi(featureFlagsApiRef); +const isOn = featureFlagsApi.isActive('show-example-feature'); +``` diff --git a/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md b/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md new file mode 100644 index 0000000000..892d3b80a0 --- /dev/null +++ b/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md @@ -0,0 +1,550 @@ +--- +id: integrating-search-into-plugins +title: Integrating Search into a Plugin +sidebar_label: Integrating Search +description: How to integrate Search into a Backstage plugin +--- + +The Backstage Search Platform was designed to give plugin developers the APIs +and interfaces needed to offer search experiences within their plugins, while +abstracting away (and instead empowering application integrators to choose) the +specific underlying search technologies. + +On this page, you'll find concepts and tutorials for leveraging the Backstage +Search Platform in your plugin. + +## Providing data to the search platform + +### Create a collator + +> Knowing what a [collator](../../features/search/concepts.md#collators) is will help you as you build it out. + +Imagine you have a plugin that is responsible for storing FAQ snippets in a database. You want other engineers to be able to easily find your questions and answers. So that means you want them to be indexed by the search platform. Lets say the FAQ snippets can be viewed at a URL like `backstage.example.biz/faq-snippets`. + +The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. + +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. + +#### 1. Create a collator module package + +In order to add an FAQ collator to the Backstage index registry we have to create a [plugin module](https://backstage.io/docs/backend-system/building-plugins-and-modules/index#modules), and the best way to do this is to create it in a separate package, e.g., `plugins/search-backend-module-faq-snippets-collator`, using the `yarn new` command: + +1. Access your Backstage project root directory and run `yarn new`; +2. When asked about what do you want to create, please select `backend-module` and hit enter; +3. Input `search` as the plugin ID and `faq-snippets-collator` as the module ID; +4. A `search-backend-module-faq-snippets-collator` folder should have been created in your project's "plugins" directory. + +#### 2. Install the collator dependencies + +We will use some libraries in the module, so let's add them to your plugin module dependencies: + +```sh +# Create a new branch using Git command-line +git checkout -b tutorials/new-faq-snippets-collator + +# Install the package containing the interface +yarn workspace @internal/backstage-plugin-search-backend-module-faq-snippets-collator add @backstage/plugin-search-common @backstage/plugin-search-backend-node +``` + +#### 3. Use Backstage App configuration + +Your new collator could benefit from using configuration directly from the Backstage `app-config.yaml` file which is located on the project's root folder: + +```yaml +faq: + baseUrl: https://backstage.example.biz/faq-snippets +``` + +It is optional to define a schedule for the collator to run, or else it defaults to the value in the collator factory code (See [5. Implement the collator factory](#5-implement-the-collator-factory)): + +```yaml +faq: + baseUrl: https://backstage.example.biz/faq-snippets + /* highlight-add-start */ + schedule: + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } + /* highlight-add-end */ +``` + +#### 4. Define the collator document type + +Before we can start generating documents from our FAQ entries, we first have to define a document type containing all necessary information we need to later display our entry as search result. The package `@backstage/plugin-search-common` we installed earlier contains a type `IndexableDocument` that we can extend. + +Create a new file `plugins/search-backend-module-faq-snippets-collator/src/types.ts` and paste the following below: + +```ts +import { IndexableDocument } from '@backstage/plugin-search-common'; + +export interface FaqSnippetDocument extends IndexableDocument { + answered_by: string; +} +``` + +#### 5. Implement the collator factory + +Imagine your FAQs can be retrieved at the URL `https://backstage.example.biz/faq-snippets` with following JSON response format: + +```json +{ + "items": [ + { + "id": 42, + "question": "What is The Answer to the Ultimate Question of Life, the Universe, and Everything?", + "answer": "Forty-two", + "user": "Deep Thought" + } + ] +} +``` + +Below we provide an example implementation of how the FAQ collator factory could look like using our new document type, placed in the `plugins/search-backend-module-faq-snippets-collator/src/factory.ts` file: + +```ts +import { Readable } from 'stream'; +import { + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { FaqSnippetDocument } from './types'; + +const DEFAULT_BASE_URL = 'https://backstage.example.biz/faq-snippets'; + +export class FaqSnippetsCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'faq-snippets'; + private readonly baseUrl: string; + private readonly logger: LoggerService; + + private constructor(options: { logger: LoggerService; baseUrl: string }) { + this.baseUrl = options.baseUrl; + this.logger = options.logger; + } + + static fromConfig( + config: RootConfigService, + options: { + logger: LoggerService; + }, + ) { + const baseUrl = config.getOptionalString('faq.baseUrl') ?? DEFAULT_BASE_URL; + return new FaqSnippetsCollatorFactory({ ...options, baseUrl }); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + this.logger.info(`Fetching faq snippets from ${this.baseUrl}`); + const response = await fetch(this.baseUrl); + const data = await response.json(); + for (const faq of data.items) { + yield { + title: faq.question, + location: `/faq-snippets/${faq.id}`, + text: faq.answer, + answered_by: faq.user, + }; + } + } +} +``` + +#### 6. Implement the collator plugin module + +Now we have to connect the search backend plugin with our FAQ Snippets collator factory, so replace the `module.ts` file with the content below: + +```ts title='plugins/search-backend-module-faq-snippets-collator/src/module.ts' +import { + coreServices, + createBackendModule, + readSchedulerServiceTaskScheduleDefinitionFromConfig, +} from '@backstage/backend-plugin-api'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { FaqSnippetsCollatorFactory } from './factory'; + +export const searchFaqSnippetsCollatorModule = createBackendModule({ + pluginId: 'search', + moduleId: 'faq-snippets-collator', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ config, logger, scheduler, indexRegistry }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + const schedule = config.has('faq.schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('faq.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: FaqSnippetsCollatorFactory.fromConfig(config, { logger }), + }); + }, + }); + }, +}); +``` + +In the fragment above, the module is registered, and when the Backstage backend initializes it, it adds the FAQ Snippets collator to the search index registry. Now let's export the module as default from the `index.ts` file: + +```ts title='plugins/search-backend-module-faq-snippets-collator/src/index.ts' +export { searchFaqSnippetsCollatorModule as default } from './module'; +``` + +#### 7. Install the collator module + +The newly created module should be added to the backend package dependencies as follows: + +```sh +yarn --cwd packages/backend add @internal/backstage-plugin-search-backend-module-faq-snippets-collator +``` + +After that, install the module on your Backstage backend instance: + +```ts title='packages/backend/src/index.ts' +import { createBackend } from '@backstage/backend-defaults'; +//... +const backend = createBackend(); +// Installing the search backend plugin +backend.add(import('@backstage/plugin-search-backend')); +// Installing the newly created faq snippets collator module +backend.add( + import( + '@internal/backstage-plugin-search-backend-module-faq-snippets-collator' + ), +); +//... +backend.start(); +``` + +#### 8. Testing the collator code + +To verify your implementation works as expected make sure to add tests for it. For your convenience, there is the [`TestPipeline`](https://backstage.io/api/stable/classes/_backstage_plugin-search-backend-node.index.TestPipeline.html) utility that emulates a pipeline into which you can integrate your custom collator. + +Look at [DefaultTechDocsCollatorFactory test](https://github.com/backstage/backstage/blob/de294ce5c410c9eb56da6870a1fab795268f60e3/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts), for an example. + +You can also check out the documentation on [how to test Backstage plugin modules](../../backend-system/building-plugins-and-modules/02-testing.md). + +#### 9. Running the collator locally + +Run `yarn start` in the root folder of your Backstage project and look for logs like these: + +```sh +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Registered scheduled task: search_index_faq_snippets, {"version":2,"cadence":"PT10M","initialDelayDuration":"PT3S","timeoutAfterDuration":"PT15M"} task=search_index_faq_snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets via FaqSnippetsCollatorFactory documentType=faq-snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Fetching faq snippets from https://backstage.example.biz/faq-snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets succeeded documentType=faq-snippets +``` + +It means that the collator task was started and completed successfully. Visit http://localhost:3000, log in, select the 'All' tab, and type in one of your snippets title in the search box. + +Results should appear for snippets. + +#### 10. Make your plugins collator discoverable for others + +If you want to make your collator discoverable for other adopters, add it to the list of [plugins integrated to search](https://backstage.io/docs/features/search/#plugins-integrated-with-backstage-search). + +## Building a search experience into your plugin + +While the core Search plugin offers components and extensions that empower app +integrators to compose a global search experience, you may find that you want a +narrower search experience just within your plugin. This could be as literal as +an autocomplete-style search bar focused on documents provided by your plugin +(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx) +component), or as abstract as a widget that presents a list of links that +are contextually related to something else on the page. + +### Search Result List Items + +In the new frontend system, search result list items are created using `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. This blueprint lets you register custom result renderers that the search plugin will use when displaying results of your document type. + +```tsx +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; + +const mySearchResultListItem = SearchResultListItemBlueprint.make({ + name: 'my-plugin', + params: { + lineClamp: 3, + predicate: result => result.type === 'my-plugin', + component: async () => { + const { MySearchResultListItem } = await import( + './components/MySearchResultListItem' + ); + return MySearchResultListItem; + }, + }, +}); +``` + +### Search Experience Concepts + +Knowing these high-level concepts will help you as you craft your in-plugin +search experience. + +- All search experiences must be wrapped in a ``, which + is provided by `@backstage/plugin-search-react`. This context keeps track + of state necessary to perform search queries and display any results. As + inputs to the query are updated (e.g. a `term` or `filter` values), the + updated query is executed and `results` are refreshed. Check out the + [SearchContextValue](https://backstage.io/api/stable/types/_backstage_plugin-search-react.index.SearchContextValue.html) + for details. +- The aforementioned state can be modified and/or consumed via the + `useSearch()` hook, also exported by `@backstage/plugin-search-react`. +- For more literal search experiences, reusable components are available + to import and compose into a cohesive experience in your plugin (e.g. + `` or ``). You can see all such + components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). + +### Search Experience Tutorials + +The following tutorials make use of packages and plugins that you may not yet +have as dependencies for your plugin; be sure to add them before you use them! + +- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A + package containing components, hooks, and types that are shared across all + frontend plugins, including plugins like yours! +- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The + main search plugin, used by app integrators to compose global search + experiences. +- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A + package containing generic components useful for a variety of experiences + built in Backstage. + +#### Improved "404" page experience + +Imagine you have a plugin that allows users to manage _widgets_. Perhaps they +can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`. +At some point, a widget is renamed, and links to that widget's page from +chat systems, wikis, or browser bookmarks become stale, resulting in errors or +404s. + +What if instead of showing a broken page or the generic "looks like someone +dropped the mic" 404 page, you showed a list of possibly related widgets? + +```javascript +import { Link } from '@backstage/core-components'; +import { SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const Widget404Page = ({ widgetName }) => { + // Supplying this to runs a pre-filtered search with + // the given widgetName as the search term, focused on search result of type + // "widget" with no other filters. + const preFiltered = { + term: widgetName, + types: ['widget'], + filters: {}, + }; + + return ( + + {/* The component allows us to iterate through results and + display them in whatever way fits best! */} + + {({ results }) => ( + {results.map(({ document }) => ( + + {document.title} + + ))} + )} + + + ); +); +``` + +Not all search experiences require user input! As you can see, it's possible to +leverage the Backstage Search Platform's frontend framework without necessarily +giving users input controls. + +#### Simple search page + +Of course, it's also possible to provide a more fully featured search +experience in your plugin. The simplest way is to leverage reusable components +provided by the `@backstage/plugin-search` package, like this: + +```javascript +import { useProfile } from '@internal/api'; +import { + Content, + ContentHeader, + PageWithHeader, +} from '@backstage/core-components'; +import { SearchBar, SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const ManageMyWidgets = () => { + const { primaryTeam } = useProfile(); + // In this example, note how we are pre-filtering results down to a specific + // owner field value (the currently logged-in user's team), but allowing the + // search term to be controlled by the user via the component. + const preFiltered = { + types: ['widget'], + term: '', + filters: { + owner: primaryTeam, + }, + }; + + return ( + + + + + + + {/* Render results here, just like above */} + + + + + ); +}; +``` + +#### Custom search control surfaces + +If the reusable search components provided by `@backstage/plugin-search` aren't +adequate, no problem! There's an API in place that you can use to author your +own components to control the various parts of the search context. + +```javascript +import { useSearch } from '@backstage/plugin-search-react'; +import ChipInput from 'material-ui-chip-input'; + +export const CustomChipFilter = ({ name }) => { + const { filters, setFilters } = useSearch(); + const chipValues = filters[name] || []; + + // When a chip value is changed, update the filters value by calling the + // setFilters function from the search context. + const handleChipChange = (chip, index) => { + // There may be filters set for other fields. Be sure to maintain them. + setFilters(prevState => { + const { [name]: filter = [], ...others } = prevState; + + if (index === undefined) { + filter.push(chip); + } else { + filter.splice(index, 1); + } + + return { ...others, [name]: filter }; + }); + }; + + return ( + + ); +}; +``` + +Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx) +for more details on what methods and values are available for manipulating and +reading the search context. + +If you produce something generic and reusable, consider contributing your +component upstream so that all users of the Backstage Search Platform can +benefit. Issues and pull requests welcome. + +#### Custom search results + +Search results throughout Backstage are rendered as lists so that list items can easily be customized; although a [default result list item](https://backstage.io/storybook/?path=/story/plugins-search-defaultresultlistitem--default) is available, plugins are in the best position to provide custom result list items that surface relevant information only known to the plugin. + +The example below imagines `YourCustomSearchResult` as a type of search result that contains associated `tags` which could be rendered as chips below the title/text. + +```tsx +import { Link } from '@backstage/core-components'; +import { useAnalytics } from '@backstage/frontend-plugin-api'; +import { ResultHighlight } from '@backstage/plugin-search-common'; +import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; + +type CustomSearchResultListItemProps = { + result: YourCustomSearchResult; + rank?: number; + highlight?: ResultHighlight; +}; + +export const CustomSearchResultListItem = ( + props: CustomSearchResultListItemProps, +) => { + const { title, text, location, tags } = props.result; + + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', title, { + attributes: { to: location }, + value: props.rank, + }); + }; + + return ( + + + + + ) : ( + title + ) + } + secondary={ + highlight?.fields?.text ? ( + + ) : ( + text + ) + } + /> + {tags && + tags.map((tag: string) => ( + + ))} + + + + + ); +}; +``` + +The optional use of the `` component makes it possible to highlight relevant parts of the result based on the user's search query. + +**Note on Analytics**: In order for app integrators to track and improve search experiences across Backstage, it's important for them to understand when and what users search for, as well as what they click on after searching. When providing a custom result component, it's your responsibility as a plugin developer to instrument it according to search analytics conventions. In particular: + +- You must use the `analytics.captureEvent` method, from the `useAnalytics()` hook (detailed [plugin analytics docs are here](./08-analytics.md)). +- You must ensure that the action of the event, representing a click on a search result item, is `discover`, and the subject is the `title` of the clicked result. In addition, the `to` attribute should be set to the result's `location`, and the `value` of the event must be set to the `rank` (passed in as a prop). +- You must ensure that the aforementioned `captureEvent` method is called when a user clicks the link; you should further ensure that the `noTrack` prop is added to the link (which disables default link click tracking, in favor of this custom instrumentation). + +For other examples and inspiration on custom result list items, check out the [``](https://github.com/backstage/backstage/blob/c981e83/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx) or [``](https://github.com/backstage/backstage/blob/c981e83/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx) components. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 692849c1eb..a758d63ebe 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -6,7 +6,7 @@ description: Measuring usage of your Backstage instance. :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The concepts and events described here apply to both the old and new frontend systems. However, the "Writing Integrations" section shows examples for both the old frontend system (`createApiFactory`) and the new frontend system (`AnalyticsImplementationBlueprint`). For new development, prefer the new frontend system approach. See also [Utility APIs](../frontend-system/utility-apis/01-index.md). +This section is part of the legacy plugins documentation. For the new frontend system version, see [Plugin Analytics](../frontend-system/building-plugins/08-analytics.md). The concepts and events described here apply to both the old and new frontend systems. ::: diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index 5d35e3c057..25251e3db5 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -6,7 +6,7 @@ description: Details the process of defining setting and reading a feature flag. :::caution Legacy Documentation -This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system, feature flags are declared via the `featureFlags` option in `createFrontendPlugin` — see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. +This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system version, see [Feature Flags](../frontend-system/building-plugins/09-feature-flags.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. ::: diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index c27192e05c..39dd490799 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -6,7 +6,7 @@ description: How to integrate Search into a Backstage plugin :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. For the new frontend system, search result list items are created using `SearchResultListItemBlueprint` — see [Common Extension Blueprints](../frontend-system/building-plugins/03-common-extension-blueprints.md). +This section is part of the legacy plugins documentation. For the new frontend system version, see [Integrating Search into a Plugin](../frontend-system/building-plugins/10-integrating-search-into-plugins.md). The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. ::: diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md index 7466d17efa..67a6b962a0 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -6,7 +6,7 @@ description: Documentation on adding internationalization to plugins and apps :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. The "For an application developer" section already shows the new frontend system approach using `createTranslationResource` from `@backstage/frontend-plugin-api`. +This section is part of the legacy plugins documentation. For the new frontend system version, see [Internationalization](../frontend-system/building-plugins/07-internationalization.md). The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. ::: diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 1be013af7d..2b04fbe1a5 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -410,18 +410,6 @@ export default { sidebarElementWithIndex({ label: 'Okta' }, ['integrations/okta/org']), ], ), - sidebarElementWithIndex( - { - label: 'OpenAPI', - description: - 'Work with OpenAPI specifications and generate clients.', - }, - [ - 'openapi/01-getting-started', - 'openapi/generate-client', - 'openapi/test-case-validation', - ], - ), sidebarElementWithIndex( { label: 'Configuration', @@ -555,6 +543,10 @@ export default { 'frontend-system/building-plugins/common-extension-blueprints', 'frontend-system/building-plugins/built-in-data-refs', 'frontend-system/building-plugins/migrating', + 'frontend-system/building-plugins/internationalization', + 'frontend-system/building-plugins/analytics', + 'frontend-system/building-plugins/feature-flags', + 'frontend-system/building-plugins/integrating-search-into-plugins', ], ), sidebarElementWithIndex( @@ -624,6 +616,18 @@ export default { 'conf/user-interface/sidebar', ], ), + sidebarElementWithIndex( + { + label: 'OpenAPI', + description: + 'Work with OpenAPI specifications and generate clients.', + }, + [ + 'openapi/01-getting-started', + 'openapi/generate-client', + 'openapi/test-case-validation', + ], + ), ], ), sidebarElementWithIndex( diff --git a/mkdocs.yml b/mkdocs.yml index 2030d87c45..ecc7b5890d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,10 +141,6 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - OpenAPI: - - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' - - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' - - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' - Plugins (Legacy): - Intro to plugins: 'plugins/index.md' - Create a Backstage Plugin: 'plugins/create-a-plugin.md' @@ -155,6 +151,10 @@ nav: - Plugin Analytics: 'plugins/analytics.md' - Feature Flags: 'plugins/feature-flags.md' - Internationalization (i18n): 'plugins/internationalization.md' + - OpenAPI: + - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' + - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' + - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md'