Skip to main content

Adding Custom Plugin to Existing Monorepo App

September 15th 2020 - v0.1.1-alpha.21

This document takes you through setting up a new plugin for your existing monorepo with a GitHub provider already setup. If you don't have either of those, you can clone simple-backstage-app which this document builds on.

This document does not cover authoring a plugin for sharing with the Backstage community. That will have to be a later discussion.

We start with a skeleton plugin install. And after verifying its functionality, extend the Sidebar to make our life easy. Finally, we add custom code to display GitHub repository information.

This document assumes you have Node.js 16 active along with Yarn and Python. Please note, that at the time of this writing, the current version is 0.1.1-alpha.21. This guide can still be used with future versions, just, verify as you go. If you run into issues, you can compare your setup with mine here > simple-backstage-app-plugin.

The Skeleton Plugin

  1. Start by using the built-in creator. From the terminal and root of your project run: yarn new --select plugin
  2. Enter a plugin ID. I used github-playground
  3. When the process finishes, let's start the backend: yarn --cwd packages/backend start
  4. If you see errors starting, refer to Auth Configuration for more information on environment variables.
  5. And now the frontend, from a new terminal window and the root of your project: yarn start
  6. As usual, a browser window should popup loading the App.
  7. Now manually navigate to our plugin page from your browser: http://localhost:3000/github-playground
  8. You should see successful verbiage for this endpoint, Welcome to github-playground!

The Shortcut

Let's add a shortcut.

  1. Open and modify root: packages > app > src > components > Root.tsx with the following:
import GitHubIcon from '@material-ui/icons/GitHub';
...
<SidebarItem icon={GitHubIcon} to="github-playground" text="GitHub Repository" />

Simple! The App will reload with your changes automatically. You should now see a GitHub icon displayed in the sidebar. Clicking that will link to our new plugin. And now, the API fun begins.

The Identity

Our first modification will be to extract information from the Identity API.

  1. Start by opening root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx
  2. Add two new imports
// Add identityApiRef to the list of imported from core
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
  1. Adjust the ExampleComponent from inline to block

from inline:

const ExampleComponent = () => ( ... )

to block:

const ExampleComponent = () => {

return (
...
)
}
  1. Now add our hook and const data before the return statement
// our API hook
const identityApi = useApi(identityApiRef);

// data to use
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
  1. Finally, update the InfoCard's jsx to use our new data
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>

If everything is saved, you should see your name, id, and email on the github-playground page. Our data accessed is synchronous. So we just grab and go.

https://github.com/backstage/backstage/tree/master/contrib

  1. Here is the entire file for reference ExampleComponent.tsx

The Wipe

The last file we will touch is ExampleFetchComponent. Because of the number of changes, let's start by wiping this component clean.

  1. Start by opening root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx
  2. Replace everything in the file with the following:
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
import { graphql } from '@octokit/graphql';

export const ExampleFetchComponent = () => {
return <div>Nothing to see yet</div>;
};
  1. Save that and ensure you see no errors. Comment out the unused imports if your linter gets in the way.
We will add a lot to this file for the sake of ease. Please don't do this in productional code!

The Graph Model

GitHub has a GraphQL API available for interacting. Let's start by adding our basic repository query

  1. Add the query const statement outside ExampleFetchComponent
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
  1. Using this structure as a guide, we will break our query into type parts
  2. Add the following outside of ExampleFetchComponent
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};

type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};

The Table Model

Using Backstage's own component library, let's define a custom table. This component will get used if we have data to display.

  1. Add the following outside of ExampleFetchComponent
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}
/>
);
};

The Fetch

We're ready to flush out our fetch component

  1. Add our api hook inside ExampleFetchComponent
const auth = useApi(githubAuthApiRef);
  1. The access token we need to make our GitHub request and the request itself is obtained in an asynchronous manner.
  2. Add the useAsync block inside the ExampleFetchComponent
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;
}, []);
  1. The resolved data is conveniently destructured with value containing our Viewer type. loading as a boolean, self explanatory. And error which is present only if necessary. So let's use those as the first 3 of 4 multi return statements.
  2. Add the if return blocks below our async block
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
  1. The third line here utilizes our custom table accepting our Viewer type.
  2. Finally, we add our else return block to catch any other scenarios.
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
  1. After saving that, and given we don't have any errors, you should see a table with basic information on your repositories.
  2. Here is the entire file for reference ExampleFetchComponent.tsx
  3. We finished! You should see your own GitHub repository's information displayed in a basic table. If you run into issues, you can compare the repo that backs this document, simple-backstage-app-plugin

Where to go from here

Break apart ExampleFetchComponent into smaller logical parts contained in their own files. Rename your components to something other than ExampleXxx.

You might be really proud of a plugin you develop. Follow this next tutorial for an in-depth look at publishing and including that for the entire Backstage community. TODO.