PlatformEngr® Backstage plugins course

Module 05 · The frontend plugin · 40 min · You are: Platform team

A frontend plugin in the new frontend system

In this module

  • You learn: What extensions, blueprints and frontend plugins are, how the app finds installed plugins, and how app.extensions configures any extension by id.
  • You do: Generate the plugin, write a placeholder page, find it in the sidebar and the Visualizer, then change its path in app-config.yaml.
  • You end with: A Readiness item in the sidebar opens /readiness, served by your own frontend plugin. Merged.

The new frontend system builds the whole app from extensions: pages, nav items, entity cards, API clients. You do not edit App.tsx to add a page; you install a package. Once you see the app as a tree of extensions with ids, you can configure, replace or turn off any part of it, yours or Backstage's.

Open this module in the interactive course to track progress.

Concepts

Extension
One piece of the app with an id (page:readiness), a place it attaches to, and outputs such as a React element. Extensions.
Blueprint
A factory for one kind of extension. PageBlueprint makes a page and, with a title, a sidebar item. Blueprints, common blueprints.
createFrontendPlugin
Groups extensions under a plugin id. The app installs every plugin package listed in packages/app/package.json (app.packages: all). Frontend plugins.
Config by id
app.extensions in app-config.yaml can disable an extension or set its config without code. The first course moved page:catalog this way.

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Generate the plugin

yarn new creates plugins/readiness and adds it to packages/app/package.json. That is all the app needs to install it.

cd ~/work/portal && yarn new --select frontend-plugin --option pluginId=readiness
grep -n readiness packages/app/package.json
cd plugins/readiness && rm -r src/components/TodoList src/components/TodoPage

2. Write the page and the plugin

A placeholder page for now. Module 06 fills it with data.

Create ~/work/portal/plugins/readiness/src/components/ReadinessPage.tsx

import { Container, Header, Text } from '@backstage/ui';

/** Every scorecard, lowest score first. For now: a placeholder. */
export const ReadinessPage = () => (
  <>
    <Header title="Production readiness" />
    <Container>
      <Text>Scorecards show up here once the page talks to the backend.</Text>
    </Container>
  </>
);

Replace the whole file ~/work/portal/plugins/readiness/src/plugin.tsx
The title makes a sidebar item.

import {
  createFrontendPlugin,
  PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { rootRouteRef } from './routes';

/** /readiness: every scorecard. The title puts it in the sidebar. */
export const readinessPage = PageBlueprint.make({
  params: {
    path: '/readiness',
    routeRef: rootRouteRef,
    title: 'Readiness',
    loader: () =>
      import('./components/ReadinessPage').then(m => <m.ReadinessPage />),
  },
});

export const readinessPlugin = createFrontendPlugin({
  pluginId: 'readiness',
  extensions: [readinessPage],
  routes: {
    root: rootRouteRef,
  },
});
cd ~/work/portal && yarn tsc

3. Find it in the app

Restart the portal (Ctrl+C, then start it again): the app has a new package.

1. The sidebar has Readiness. Click it: /readiness shows Production readiness and the placeholder text.

2. Open Visualizer (bottom of the sidebar), tab Text, and search for readiness. You see page:readiness, attached to the app's routes.

4. Move it with config

Any extension's config can be set by its id. Try it, then take it out again.

Add to ~/work/portal/app-config.yaml under app: → extensions:, as the first item

    - page:readiness:
        config:
          path: /scorecards

Save. The app reloads; Readiness in the sidebar now opens /scorecards. Remove it again:

Delete from ~/work/portal/app-config.yaml

-     - page:readiness:
-         config:
-           path: /scorecards

5. Merge it

cd ~/work/portal

Propose it as a pull request. Put it on its own branch and push it:

git switch -c readiness-frontend && git add -A && git commit -qm "Readiness: frontend plugin with a page"
git push -qu origin HEAD

Open the pull request. It prints a link:

gh pr create --fill

Open that link. Files changed shows exactly your diff.

Merge it on GitHub (Squash and merge, then Delete branch), or from the terminal:

gh pr merge --squash --delete-branch

Bring your copy up to date:

git switch -q main && git pull -q

Simplified for the course

The page has no data yet. Module 06 adds the API client.

Break it on purpose: Configure an extension that does not exist

The platform team moves the page to /scorecards for a demo, and types the id from memory.

You practise: where the new frontend system reports config mistakes.

1. Break it

Add to ~/work/portal/app-config.yaml under app: → extensions:, as the first item
One letter short.

    - page:readines:
        config:
          path: /scorecards

Save, then open http://localhost:3000/scorecards.

2. What you see

Expected: the Readiness page at /scorecards. You get: a not-found page, and Readiness still opens /readiness. No error on the page.

3. Fix it yourself

Find where the app reports the mistake, then fix the id. You are done when /scorecards shows the page.

Hint 1

Open the browser's developer tools, tab Console, and reload.

Hint 2

Compare the id with the one in the Visualizer.

The fix

The console says App startup encountered warnings: and INVALID_EXTENSION_CONFIG_KEY: Extension page:readines does not exist.

Change in ~/work/portal/app-config.yaml

-     - page:readines:
+     - page:readiness:

Check /scorecards, then remove the three lines again: the course keeps /readiness.

After you fixed it: the five questions
What failed?
The config named an extension id that no plugin provides, so the app ignored it.
How was it detected?
Only as a warning in the browser console.
Who responds?
The platform team, who own app-config.yaml.
How is it corrected?
Use the exact id shown in the Visualizer.
How is it prevented?
Copy ids from the Visualizer, and open the browser console in every review of app config changes.

Quiz

  1. How did the app learn about your plugin?
    • yarn new added the package to packages/app/package.json, and the app installs every plugin package listed there
    • You imported the plugin in App.tsx and added it to the list of routes
    • The backend sends the list of plugins to the browser at startup
  2. What makes the Readiness item appear in the sidebar?
    • A NavItem component added to Sidebar.tsx by yarn new
    • The title on the page made with PageBlueprint
    • A sidebar: key under the plugin in app-config.yaml
  3. How do you change the path of someone else's page without forking it?
    • Copy the plugin into your repo and change the path there
    • Wrap the page in your own page with a different route
    • Set config.path for its extension id under app.extensions
Answers
  1. yarn new added the package to packages/app/package.json, and the app installs every plugin package listed there With app.packages: all, a dependency is all it takes.
  2. The title on the page made with PageBlueprint A page with a title also gives the app a nav item.
  3. Set config.path for its extension id under app.extensions Every extension is configurable by id; that is the point of the system.

Field note

The Visualizer is the most underused page in Backstage. When a page is missing or in the wrong place, I start there: the extension is either not in the tree, or attached somewhere I did not expect.

Report a problem with this module