Clean up of the contrib folder

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
Andre Wanlin
2026-03-18 08:59:09 -05:00
parent a4b9c45277
commit 5cbd39e980
9 changed files with 6 additions and 1138 deletions
@@ -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 }),
}),
],
});
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

@@ -1,115 +0,0 @@
# Prometheus
> [!NOTE]
> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation
## Overview
This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus.
## How to Setup Prometheus Metrics
1. First we need to add the needed dependencies to the `package.json` in the `\packages\backend`:
```diff
// packages/backend/package.json
"dependencies": {
+ "express-prom-bundle": "^7.0.0",
+ "prom-client": "^15.0.0",
```
2. Now we want to run `yarn install` from the root of the project to get those dependencies in place
3. Then we need to add a handler for the metrics by creating a file called `metrics.ts` in the `\packages\backend\src` folder
4. Next we add the following content to the `metrics.ts` file:
```ts
// packages/backend/src/metrics.ts
import { useHotCleanup } from '@backstage/backend-common';
import { RequestHandler } from 'express';
import promBundle from 'express-prom-bundle';
import prom from 'prom-client';
import * as url from 'url';
const rootRegEx = new RegExp('^/([^/]*)/.*');
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
export function normalizePath(req: any): string {
const path = url.parse(req.originalUrl || req.url).pathname || '/';
// Capture /api/ and the plugin name
if (apiRegEx.test(path)) {
return path.replace(apiRegEx, '/api/$1');
}
// Only the first path segment at root level
return path.replace(rootRegEx, '/$1');
}
/**
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
*/
export function metricsHandler(): RequestHandler {
// We can only initialize the metrics once and have to clean them up between hot reloads
useHotCleanup(module, () => prom.register.clear());
return promBundle({
includeMethod: true,
includePath: true,
// Using includePath alone is problematic, as it will include path labels with high
// cardinality (e.g. path params). Instead we would have to template them. However, this
// is difficult, as every backend plugin might use different routes. Instead we only take
// the first directory of the path, to have at least an idea how each plugin performs:
normalizePath,
promClient: { collectDefaultMetrics: {} },
});
}
```
5. Now we will extend the router configuration with the `metricsHandler`:
```diff
+import { metricsHandler } from './metrics';
...
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('', await healthcheck(healthcheckEnv))
+ .addRouter('', metricsHandler())
.addRouter('/api', apiRouter);
```
6. You now have everything setup, from the `\packages\backend` folder run `yarn start` this will start up the backend
7. Now in a browser load up `http://localhost:7007/metrics`, if everything went smoothly you should see metrics in your browser something like this:
![Prometheus Metrics Example Output](prometheus-metrics-output.png)
## Metrics
The following sections goes over the included and experimental metrics available once you have completed this tutorial
## Included
This tutorials uses the [`express-prom-bundle`](https://github.com/jochen-schweizer/express-prom-bundle) and the [`prom-client`](https://github.com/siimon/prom-client) to make this all work. They both come with some built in metrics:
- `express-prom-bundle` comes with 2 metrics:
- `up`: this normally will be just 1
- `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
- `prom-client` comes with a collection of metrics around memory, CPU, processes, etc. You can see the supported metrics in the `prom-client's` [`lib/metrics`](https://github.com/siimon/prom-client/tree/master/lib/metrics) folder.
### Experimental
There are some custom metrics that have been added to Backstage will be output for you, these are currently deemed experimental and may be changed or removed in a future release. Here is a rough list, again subject to changes:
- `catalog_entities_count`: Total amount of entities in the catalog
- `catalog_registered_locations_count`: Total amount of registered locations in the catalog
- `catalog_relations_count`: Total amount of relations between entities
- `catalog_stitched_entities_count`: Amount of entities stitched
- `catalog_processed_entities_count`: Amount of entities processed
- `catalog_processing_duration_seconds`: Time spent executing the full processing flow
- `catalog_processors_duration_seconds`: Time spent executing catalog processors
- `catalog_processing_queue_delay_seconds`: The amount of delay between being scheduled for processing, and the start of actually being processed
- `scaffolder_task_count`: Tracks successful task runs.
- `scaffolder_task_duration`: a histogram which tracks the duration of a task run
- `scaffolder_step_count`: a count that tracks each step run
- `scaffolder_step_duration`: a histogram which tracks the duration of each step run
@@ -1,53 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleComponent.tsx reference
```tsx
import { Typography, Grid } from '@material-ui/core';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import {
InfoCard,
Header,
Page,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
} from '@backstage/core-components';
import { ExampleFetchComponent } from '../ExampleFetchComponent';
export const ExampleComponent = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
return (
<Page themeId="tool">
<Header
title="Welcome to github-playground!"
subtitle="Optional subtitle"
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Plugin title">
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>
</Grid>
<Grid item>
<ExampleFetchComponent />
</Grid>
</Grid>
</Content>
</Page>
);
};
```
@@ -1,103 +0,0 @@
### Source repo: https://github.com/johnson-jesse/simple-backstage-app-plugin
ExampleFetchComponent.tsx reference
```tsx
import useAsync from 'react-use/lib/useAsync';
import Alert from '@material-ui/lab/Alert';
import { githubAuthApiRef, useApi } from '@backstage/core-plugin-api';
import { Table, TableColumn, Progress } from '@backstage/core-components';
import { graphql } from '@octokit/graphql';
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};
type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};
type DenseTableProps = {
viewer: Viewer;
};
export const DenseTable = ({ viewer }: DenseTableProps) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Created', field: 'createdAt' },
{ title: 'Description', field: 'description' },
{ title: 'Disk Usage', field: 'diskUsage' },
{ title: 'Fork', field: 'isFork' },
];
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={columns}
data={viewer.repositories.nodes}
/>
);
};
export const ExampleFetchComponent = () => {
const auth = useApi(githubAuthApiRef);
const { value, loading, error } = useAsync(async (): Promise<any> => {
const token = await auth.getAccessToken();
const gqlEndpoint = graphql.defaults({
// Uncomment baseUrl if using enterprise
// baseUrl: 'https://github.MY-BIZ.com/api',
headers: {
authorization: `token ${token}`,
},
});
const { viewer } = await gqlEndpoint(query);
return viewer;
}, []);
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
};
```
@@ -1,3 +1,6 @@
# Basic Kubernetes example with Helm
> [!NOTE]
> This documentation is deprecated and will be removed at a future date, please use the well maintained [Backstage Helm Charts](https://github.com/backstage/charts) for this.
Note that these examples aim to show a minimal setup and do not include best practices for secure Kubernetes deployments. See the [Kubernetes documentation](https://kubernetes.io/docs/concepts/security/) for more information, or resources provided by your own organization.
+3
View File
@@ -1,5 +1,8 @@
# Confluence
> [!NOTE]
> This documentation is deprecated and will be removed at a future date, please use the well maintained [`@backstage-community/plugin-search-backend-module-confluence-collator` Community Plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/confluence/plugins/search-backend-module-confluence-collator) for this.
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:
-122
View File
@@ -1,122 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "backstage.io/v1alpha1",
"type": "object",
"title": "A JSON Schema for Backstage catalog entities.",
"description": "Each descriptor file has a number of entities. This schema matches each of those.",
"examples": [
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "Component",
"metadata": {
"name": "LoremService",
"description": "Creates Lorems like a pro.",
"labels": {
"product_name": "Random value Generator"
},
"annnotations": {
"docs": "https://github.com/..../tree/develop/doc"
},
"teams": [
{
"name": "Team super great",
"email": "greatTeam@geemel.com"
}
]
},
"spec": {
"type": "service",
"lifecycle": "production",
"owner": "tools@example.com"
}
}
],
"required": ["apiVersion", "kind", "metadata"],
"additionalProperties": false,
"properties": {
"apiVersion": {
"type": "string",
"description": "Version of the specification format for a particular file is written against.",
"enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
},
"kind": {
"type": "string",
"description": "High level entity type being described, from the Backstage system model.",
"enum": ["Component"]
},
"metadata": {
"$ref": "#/definitions/metadata"
},
"spec": {
"$ref": "#/definitions/spec"
}
},
"definitions": {
"metadata": {
"type": "object",
"description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.",
"required": ["name"],
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z0-9A-Z_.-]{1,63}$",
"description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity"
},
"description": {
"type": "string",
"description": "A human readable description of the entity, to be shown in Backstage. Should be kept short and informative."
},
"namespace": {
"type": "string",
"description": "The name of a namespace that the entity belongs to."
},
"labels": {
"type": "object",
"description": "Labels are optional key/value pairs of that are attached to the entity, and their use is identical to kubernetes object labels.",
"additionalProperties": true,
"patternProperties": {
"^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": {
"type": "string",
"pattern": "^[a-z0-9A-Z_.-]{1,63}$"
}
}
},
"annnotations": {
"type": "object",
"description": "Arbitrary non-identifying metadata attached to the entity, identical in use to kubernetes object annotations.",
"additionalProperties": true,
"patternProperties": {
"^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": {
"type": "string",
"pattern": "^[a-z0-9A-Z_.-]{1,63}$"
}
}
}
}
},
"spec": {
"type": "object",
"description": "Actual specification data that describes the entity. TODO: shape depend on `kind`",
"required": ["type", "lifecycle", "owner"],
"additionalProperties": true,
"properties": {
"type": {
"type": "string",
"description": "The type of component.",
"examples": ["service"]
},
"lifecycle": {
"type": "string",
"description": "The lifecycle step that this component is in.",
"examples": ["production"]
},
"owner": {
"type": "string",
"description": "The owner of the component.",
"examples": ["tools@example.com"]
}
}
}
}
}
-286
View File
@@ -1,286 +0,0 @@
---
id: quickstart-app-plugin
title: Adding Custom Plugin to Existing Monorepo App
description: Tutorial for adding a custom plugin to an existing Backstage monorepo application
---
::::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](./quickstart-app-plugin--old.md)
instead.
::::
> This document takes you through setting up a new plugin for your existing
> monorepo with a _GitHub provider already setup_.
>
> 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, we add custom code to display GitHub repository information.
## The Skeleton Plugin
1. Start by using the built-in creator. From the terminal and root of your
project run: `yarn new` and select `frontend-plugin`.
1. Enter a plugin ID. We'll use `github-playground` for this tutorial.
1. When the process finishes, let's start the backend:
`yarn --cwd packages/backend start`
1. If you see errors starting, refer to
[Auth Configuration](https://backstage.io/docs/auth/) for more information on
environment variables.
1. And now the frontend, from a new terminal window and the root of your
project: `yarn start`
1. As usual, a browser window should popup loading the App.
1. Now manually navigate to the plugin page from your browser:
`http://localhost:3000/github-playground`
1. You should see successful verbiage for this endpoint,
`Welcome to github-playground!`
With the new frontend system, plugins are auto-discovered when installed as
dependencies of your `packages/app` package. The plugin was already added there
by `yarn new`, so the route and a sidebar item are available without any manual
wiring in `App.tsx` or `Root.tsx`.
## 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`
1. Add two new imports
```tsx
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
```
3. Adjust the ExampleComponent from inline to block
_from inline:_
```tsx
const ExampleComponent = () => ( ... )
```
_to block:_
```tsx
const ExampleComponent = () => {
return (
...
)
}
```
4. Now add our hook and const data before the return statement
```tsx
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
```
5. Finally, update the InfoCard's jsx to use our new data
```tsx
<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
6. Here is the entire file for reference
[ExampleComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md)
## 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`
1. Replace everything in the file with the following:
```tsx
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>;
};
```
3. 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
```tsx
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
```
2. Using this structure as a guide, we will break our query into type parts
3. Add the following outside of ExampleFetchComponent
```tsx
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
```tsx
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
```tsx
const auth = useApi(githubAuthApiRef);
```
2. The access token we need to make our GitHub request and the request itself is
obtained in an asynchronous manner.
3. Add the `useAsync` block inside the ExampleFetchComponent
```tsx
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;
}, []);
```
4. 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.
5. Add the _if return_ blocks below our async block
```tsx
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
```
6. The third line here utilizes our custom table accepting our Viewer type.
7. Finally, we add our _else return_ block to catch any other scenarios.
```tsx
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
```
8. After saving that, and given we don't have any errors, you should see a table
with basic information on your repositories.
9. Here is the entire file for reference
[ExampleFetchComponent.tsx](https://github.com/backstage/backstage/tree/master/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md)
10. 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](https://github.com/johnson-jesse/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. Consider sharing it with
> the Backstage community by contributing to the [community-plugins repository](https://github.com/backstage/community-plugins).