Backstage
This guide will guide you in setting up basic Backstage integration with Digital Rebar.
You will be creating a simple backend plugin to provide functionality to the template and frontend plugins you will create afterward. The end goal is a simple integration that will allow you to quickly spin up, view, and destroy Clusters in DRP.
It is composed of three parts:
- The backend plugin, a requirement for the following two parts,
- The scaffolder, a sample template to spin up DRP Clusters, and
- The frontend plugin, a view for listing Clusters and deleting them.
Backend¶
The backend plugin is responsible for providing custom actions to the scaffolder and API functionality to the frontend.
In this tutorial, we will be setting up the aforementioned plugin to work with our two other components.
We will:
- Create the plugin
- Optionally add custom actions for the scaffolder
- Optionally add router extensions for the frontend
- Export our plugin changes
- Authorize our Backstage instance with DRP
- Register the plugin in Backstage
While you can choose to omit adding custom actions or router extensions, it is recommended that you do both so that you get a full sense for Digital Rebar's power with Backstage.
Info
A sample, reference backend plugin is available at https://gitlab.com/zfranks/backstage-plugin-digitalrebar-backend.
Creating the plugin¶
The official documentation for creating a backend plugin is available here, but we will walk through it for our cases.
Start from the root directory of your Backstage instance.
Let's name (ID) our plugin digitalrebar
. This will create a package at plugins/digitalrebar-backend
. The package
will be named @internal/plugin-digitalrebar-backend
. You are free to rename these after this tutorial.
Using the Digital Rebar TypeScript API¶
From the root of your Backstage instance, issue the following command to add the DRP TS API to your plugin.
This will add the official Digital Rebar TypeScript API to your backend plugin. It can be used as a lightweight wrapper for DRP REST API calls, which will greatly simplify the implementation of our custom actions and the functionality our frontend should present to the user.
Adding custom actions¶
First, open up your Backstage instance in the text editor of your choice. In your plugin directory
at plugins/digitalrebar-backend
, create a new directory actions
:
Create a new file clusters.ts
in this folder. It will hold the functionality for our drp:clusters:create
action we
will use in the scaffolder.
Set clusters.ts
to the following. We will break it down after the source.
import {Config} from '@backstage/config';
import type {JsonValue} from '@backstage/types';
import {createTemplateAction} from '@backstage/plugin-scaffolder-node';
import DRApi, {DRMachine} from '@rackn/digitalrebar-api';
export const clustersActions = (config: Config) => {
// DRP endpoints use self-signed certificates
// you may want to set this manually, but it is here
// for development purposes
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
return [
createTemplateAction({
id: 'drp:clusters:create',
schema: {
input: {
type: 'object',
},
},
handler: async (ctx) => {
const endpoint = config.getString('digitalrebar.endpoint');
const token = config.getString('digitalrebar.token');
const api = new DRApi(endpoint);
api.setToken(token);
ctx.logger.info('Creating cluster...');
const response = await api.clusters.create(
ctx.input as unknown as DRMachine
);
ctx.logger.info(`Created cluster with UUID ${response.data.Uuid}`);
ctx.output('object', response.data as unknown as JsonValue);
ctx.output('endpoint', endpoint);
},
}),
];
};
Ignoring our import section, let's look at the entire thing, broken down:
export const clustersActions = (config: Config) => {
// DRP endpoints use self-signed certificates
// you may want to set this manually, but it is here
// for development purposes
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
This section sets the NODE_TLS_REJECT_UNAUTHORIZED
environment variable to 0
, as DRP endpoints use self-signed
certificates. You may want to set this elsewhere, or remove it entirely if your DRP endpoint is properly signed.
We return an array of actions. In this case, we are only creating one action. Actions are created using the
Backstage-provided createTemplateAction
. We will call it drp:clusters:create
. Its input schema is simply an object
with no known parameters; it simply gets shipped to our DRP API as the object we are trying to create. In this way, the
input object of this action can be an entire DRP Cluster.
handler: async (ctx) => {
const endpoint = config.getString('digitalrebar.endpoint');
const token = config.getString('digitalrebar.token');
const api = new DRApi(endpoint);
api.setToken(token);
In this section, we grab the endpoint
and token
config options from our app-config.yaml
(see the section
on DRP authorization) and we give them to our DRApi
so that we can make calls to our DRP
endpoint.
ctx.logger.info('Creating cluster...');
const response = await api.clusters.create(
ctx.input as unknown as DRMachine
);
Here, we note to the user that we are creating the cluster. Then, we call api.clusters.create
with our input object.
Note
Note the TypeScript that is happening here (as unknown as DRMachine
). Backstage provides input
as a JsonValue
,
which cannot be properly cast to DRMachine
. We trust the conversion, so we assume input
is unknown
,
and then DRMachine
, so that the value can be passed into the create
method.
ctx.logger.info(`Created cluster with UUID ${response.data.Uuid}`);
ctx.output('object', response.data as unknown as JsonValue);
ctx.output('endpoint', endpoint);
We notify the user that the cluster has been created, and its UUID is fetched from response.data.Uuid
. The entire
Cluster object that was created is available at response.data
, for that matter.
We set our outputs:
object
is ourresponse.data
described above. Note again the TypeScript casting that is used for the inverse case.endpoint
is our endpoint we got from our configuration a few sections above. Again, see the section on DRP authorization.
Adding router extensions¶
In order to provide functionality to the frontend, we need to extend Backstage's router in our backend plugin.
Create a new folder service
in your backend plugin's directory if it does not exist:
Then, create a file router.ts
in it and set it to the following. Again, we will break it down afterwards.
There is a chance this file already exists for you in your boilerplate plugin. If it does, make sure to add the relevant parts discussed below.
import {errorHandler} from '@backstage/backend-common';
import {Config} from '@backstage/config'; // add this line
import DRApi, {DRWorkOrder} from '@rackn/digitalrebar-api'; // add this line
import express from 'express';
import Router from 'express-promise-router';
import {Logger} from 'winston';
export interface RouterOptions {
logger: Logger;
config: Config; // add this line
}
export async function createRouter(
options: RouterOptions
): Promise<express.Router> {
const {logger} = options;
const router = Router();
router.use(express.json());
// --- add these lines ---
const endpoint = options.config.getString('digitalrebar.endpoint');
const token = options.config.getString('digitalrebar.token');
const api = new DRApi(endpoint);
api.setToken(token);
// --- add those lines ---
router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({status: 'ok'});
});
// --- add these lines ---
router.get('/clusters', async (_, response) => {
response.json((await api.clusters.list({aggregate: 'true'})).data);
});
router.delete('/clusters/:uuid', async (request, response) => {
response.json((await api.clusters.delete(request.params.uuid)).data);
});
router.post('/work_orders', async (request, response) => {
response.json((await api.workOrders.create(request.body)).data);
});
router.patch('/profiles/:name', async (request, response) => {
response.json(
(await api.profiles.patch(request.params.name, request.body)).data
);
});
router.patch('/clusters/:uuid/scale', async (request, response) => {
const {current, count} = request.body;
const {data: cluster} = await api.clusters.get(request.params.uuid);
const {data: profile} = await api.profiles.get(cluster.Name);
// If the cluster is not in workorder mode, change it
if (!cluster.WorkOrderMode) {
await api.clusters.patch(cluster.Uuid, [
{op: 'test', path: '/WorkOrderMode', value: false as unknown as object},
{op: 'replace', path: '/WorkOrderMode', value: true as unknown as object},
]);
}
// If the profile has the cluster/count param, replace its value
// otherwise add the param
await api.profiles.patch(cluster.Name, 'cluster/count' in (profile.Params ?? {}) ? [
{op: 'test', path: '/Params/cluster~1count', value: current},
{op: 'replace', path: '/Params/cluster~1count', value: count},
] : [
{op: 'add', path: '/Params/cluster~1count', value: count},
]);
// Run the base-cluster blueprint to deploy the changes
const wo = await api.workOrders.create({
Blueprint: 'universal-application-base-cluster',
Machine: cluster.Uuid,
Context: cluster.Context,
} as DRWorkOrder);
response.send(wo.data);
});
// --- add those lines
router.use(errorHandler());
return router;
}
Much of this is Backstage boilerplate, and is somewhat explained in the official documentation. The parts we care about are below:
const endpoint = options.config.getString('digitalrebar.endpoint');
const token = options.config.getString('digitalrebar.token');
const api = new DRApi(endpoint);
api.setToken(token);
This should look familiar if you completed the custom actions part of this tutorial.
router.get('/clusters', async (_, response) => {
response.json((await api.clusters.list({aggregate: 'true'})).data);
});
This call adds a listener to GET /clusters
to our router. It returns a list of Clusters from our DRP endpoint, thanks
to the api.clusters.list
method provided by the DRP TypeScript API. We also
set aggregate: 'true'
here so that DRP responds with all Params, even those across inherited Profiles, as all
Clusters have an associated Profile.
router.delete('/clusters/:uuid', async (request, response) => {
response.json((await api.clusters.delete(request.params.uuid)).data);
});
Like the call above, this will add a listener to DELETE /clusters/:uuid
. It will be responsible for deleting clusters
given their UUID.
router.post("/work_orders", async (request, response) => {
response.json((await api.workOrders.create(request.body)).data);
});
This adds a listener to POST /work_orders
, which will allow us to send arbitrary requests to create DRP Work Order
objects to our DRP instance. We will use it to create the work order that scales our cluster.
router.patch("/profiles/:name", async (request, response) => {
response.json(
(await api.profiles.patch(request.params.name, request.body)).data
);
});
Likewise, this adds a listener to PATCH /profiles/:name
that allows us to patch existing profiles by their name.
Again, this is used to set the cluster size of our cluster as that information is not stored on the Cluster object
itself, but rather its associated Profile.
router.patch('/clusters/:uuid/scale', async (request, response) => {
const {current, count} = request.body;
const {data: cluster} = await api.clusters.get(request.params.uuid);
const {data: profile} = await api.profiles.get(cluster.Name);
// If the cluster is not in workorder mode, change it
if (!cluster.WorkOrderMode) {
await api.clusters.patch(cluster.Uuid, [
{op: 'test', path: '/WorkOrderMode', value: false as unknown as object},
{op: 'replace', path: '/WorkOrderMode', value: true as unknown as object},
]);
}
// If the profile has the cluster/count param, replace its value
// otherwise add the param
await api.profiles.patch(cluster.Name, 'cluster/count' in (profile.Params ?? {}) ? [
{op: 'test', path: '/Params/cluster~1count', value: current},
{op: 'replace', path: '/Params/cluster~1count', value: count},
] : [
{op: 'add', path: '/Params/cluster~1count', value: count},
]);
// Run the base-cluster blueprint to deploy the changes
const wo = await api.workOrders.create({
Blueprint: 'universal-application-base-cluster',
Machine: cluster.Uuid,
Context: cluster.Context,
} as DRWorkOrder);
response.send(wo.data);
});
Finally, this last handler handles PATCH /clusters/:uuid/scale
which scales our cluster for us. It ensures
WorkOrderMode is set on the cluster, which allows us to run blueprints on it on the fly independent of a routine
workflow. It patches the Cluster Profile, changing its size. Then, it creates a WorkOrder that
runs universal-application-base-cluster
, which is the Blueprint in DRP that is used to re-provision and scale
clusters, creating new machines or removing old ones.
Our plugin is now prepared to listen to API calls from our frontend.
Exporting our plugin functionality¶
To make our backend functionality visible to the rest of Backstage, set your plugin's index.ts
to the following:
// Export our cluster actions
export * from './actions/clusters';
// Export our router extensions for the frontend
export * from './service/router';
DRP Authorization¶
In order for your Backstage instance to be able to make requests to DRP, you need to add some information to your
instance's app-config.yaml
.
First, you'll need to get your endpoint. Your endpoint should be the IP address followed by the port DRP listens to (by
default, 8092
). It is the same as the IP and port you use to log into the DRP UX. You'll replace YOUR-ENDPOINT
in
the following yaml
excerpt with this address.
Then, you'll need a DRP user's token that the plugin will make requests through. You can find this by running the
following command somewhere with drpcli
access (be sure to change USERNAME
to the DRP username, default being
rocketskates
):
drpcli users token USERNAME ttl 9999999 | jq .Token -r
Open app-config.yaml
in the text editor of your choice, and add the following at the top-level somewhere in the file:
You will also need to add a file in your backend plugin. Create the file
plugins/digitalrebar-backend/config.d.ts
, and paste the following into it:
export interface Config {
digitalrebar: {
/**
* The endpoint (IP and port) of the DRP instance.
* @visibility frontend
*/
endpoint: string;
/**
* The auth token of the DRP instance.
* @visibility secret
*/
token: string;
};
}
This simply instructs Backstage to check for this config schema when it reads its app-config.yaml
. Finally, you need
to register this schema file with your plugin package. Open plugins/digitalrebar-backend/package.json
and remove the
entry for files
. Then, add the following to the root object:
Register the plugin¶
To register the plugin with Backstage, you'll have to create and modify a few files.
If you made router extensions¶
Start by creating packages/backend/src/plugins/digitalrebar.ts
and setting it to the following.
import {createRouter} from '@internal/plugin-digitalrebar-backend';
import {Router} from 'express';
import {PluginEnvironment} from '../types';
export default async function createPlugin(
env: PluginEnvironment
): Promise<Router> {
return await createRouter({logger: env.logger, config: env.config});
}
Then, open packages/backend/src/index.ts
, and add:
// imports ...
import digitalrebar from './plugins/digitalrebar'; // add this line
// down a bit, in the main function ...
async function main() {
// ... more environments here ...
const appEnv = useHotMemoize(module, () => createEnv('app'));
const drpEnv = useHotMemoize(module, () => createEnv('drp')); // add this line
// ...
apiRouter.use('/search', await search(searchEnv));
apiRouter.use('/drp', await digitalrebar(drpEnv)); // add this line
// ...
}
This will register the plugin router with Backstage's API router.
If you added custom actions¶
Add the following to packages/backend/src/plugins/scaffolder.ts
:
// add these two import lines
import {clustersActions} from '@internal/plugin-digitalrebar-backend';
import {ScmIntegrations} from '@backstage/integration';
// replace the line that imports createRouter with this
import {
createBuiltinActions,
createRouter,
} from '@backstage/plugin-scaffolder-backend';
// ...
export default async function createPlugin(
env: PluginEnvironment
): Promise<Router> {
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
// add this
const integrations = ScmIntegrations.fromConfig(env.config);
// since we are adding actions, we must manually create the built-in actions
// if you are already adding custom actions, simply add the `...clustersActions` line
// to your current actions array. otherwise, you will need to these lines
const actions = [
...createBuiltinActions({
catalogClient,
integrations,
config: env.config,
reader: env.reader,
}),
// add this following line
...clustersActions(env.config),
];
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
catalogClient,
identity: env.identity,
permissions: env.permissions,
actions, // add this line if you were not using custom actions before
});
}
Usage¶
As an independent component, the backend plugin does not do much. That said, continue on to the scaffolder (if you added custom actions) or to the frontend (if you added router extensions). If you did both, choose either!
Scaffolder¶
The scaffolder, otherwise known as software templates by the Backstage docs, is a way to create objects based on defined templates. The sample template that ships with a standard Backstage installation is a Node.js app.
In the case of Digital Rebar, templates can be used to quickly spin up DRP objects like Clusters. This tutorial will explain how to create such a template, utilizing the Digital Rebar template actions provided by the backend plugin.
In this tutorial, we will be creating a Backstage template that walks a user through the process of spinning up a cluster in DRP. It is assumed that the steps in backend are followed so that the required custom actions are available.
Adding templates¶
This tutorial operates under the assumption that you have an understanding of how to create new templates and add them to your Backstage instance. This is described in the official Backstage documentation. A description of the template schema can also be found here.
Let's start by creating a directory to store our templates. From our Backstage instance...
Now, let's create our template.
Next, we need to register the template with our Backstage instance. This is described in the documentation link above.
Open app-config.yaml
in the editor of your choice, and look for the catalog:
object at the root of the configuration file.
By default, an example template is registered in a standard, unmodified Backstage installation. It looks like this:
catalog:
# ...
locations:
# ...
# Local example template
- type: file
target: ../../examples/template/template.yaml
rules:
- allow: [ Template ]
Let's add another one of those - type: file
entries. It will reference the template we created.
The template should now be registered in your Backstage instance, except our template is blank. Let's fill it out with our intended functionality.
Custom actions¶
The Backstage scaffolder/templates interact with the Digital Rebar API through custom actions. In the examples linked
above, some actions include fetch:template
, fetch:plain
, publish:github
, and catalog:register
. These are actions
that will be taken as steps are reached. The backend plugin is responsible for implementing these custom actions,
which allow us to use things like creating clusters in our case.
Please complete the backend tutorial if you have not already.
Writing the template¶
Below is a blob of yaml
source that we will set our template to. It will be broken down and explained afterwards. Set
the template at packages/backend/templates/drp/create-cluster.yaml
to the following source:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: drp-create-cluster
title: Create DRP Cluster
description: Create a Digital Rebar Cluster.
spec:
owner: user:guest
type: service
parameters:
- title: Cluster information
required: [ "name", "broker" ]
properties:
name:
title: Name
type: string
description: The name of the Cluster to add.
ui:autofocus: true
broker:
title: Broker
type: string
description: The broker for the Cluster.
steps:
- id: create
name: Create Cluster
action: drp:clusters:create # our custom action!
input:
# the following fields are directly
# passed to the created DRP object
Name: ${{ parameters.name }}
Params:
broker/name: ${{ parameters.broker }}
output:
links:
- title: Jump to Cluster in UX
url: https://portal.rackn.io/#/e/${{ steps.create.output.endpoint }}/clusters/${{ steps.create.output.object.Uuid }}
Now, let's look at each section of our template individually.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: drp-create-cluster
title: Create DRP Cluster
description: Create a Digital Rebar Cluster.
spec:
owner: user:guest
type: service
The section above is simple template metadata. More information on this is described in Backstage documentation.
parameters:
- title: Cluster information
required: [ 'name', 'broker' ]
properties:
name:
title: Name
type: string
description: The name of the Cluster to add.
ui:autofocus: true
broker:
title: Broker
type: string
description: The broker for the Cluster.
The section above declares the parameters of the template. In our case, we need parameters for the name of the Cluster, and for the broker the Cluster should be assigned.
Again, documentation will help with adding more fields as necessary. You will likely want to include more parameters to construct more interesting objects, such as cluster size.
steps:
- id: create
name: Create Cluster
action: drp:clusters:create # our custom action!
input:
# the following fields are directly
# passed to the created DRP object
Name: ${{ parameters.name }}
Params:
broker/name: ${{ parameters.broker }}
The steps
object is responsible for declaring what the template actually does when it is constructed. Here, it calls
the drp:clusters:create
action we set up in the backend, and creates a preliminary DRP Cluster object with the data
from our parameters.
output:
links:
- title: Jump to Cluster in UX
url: https://portal.rackn.io/#/e/${{ steps.create.output.endpoint }}/clusters/${{ steps.create.output.object.Uuid }}
Finally, the output
object describes what is displayed to the user after the template is finished being constructed.
Here, we have it set to show a link to the user, "Jump to Cluster in UX," that should link to the new object in the
portal.
The ${{ steps.create.output.endpoint }}
notation is templating syntax for the output of the create
step (steps.create.output
). Our endpoint
field on this steps.create.output
object is filled in by
our custom action in the backend plugin, as is the steps.create.output.object
object, which represents our
new DRP Cluster as returned by the DRP API.
Usage¶
At this point, we have created and registered our template with our Backstage instance.
Before testing, ensure that you have set up authorization with DRP.
Spin up your Backstage instance...
Then click the Create
button from the opened Catalog page. Your new template should be visible. Fill it out and create
the Cluster!
Frontend¶
The frontend plugin is used to display information in the Backstage portal, and uses the backend plugin to communicate with Digital Rebar.
In this tutorial, we will create and set up the frontend plugin so that we can view a list of Clusters in our DRP endpoint, as well as a way to delete clusters.
If you have not yet completed the backend tutorial, please complete that first, as there is functionality required by the frontend!
This tutorial assumes a basic understanding of React.
Info
A sample, reference frontend plugin is available at https://gitlab.com/zfranks/backstage-plugin-digitalrebar.
Creating the plugin¶
The official documentation for creating a frontend plugin is available here, but we will walk through it for our cases.
Start from the root directory of your Backstage instance.
Use the same ID as our backend plugin, digitalrebar
.
This will create a package at plugins/digitalrebar
. The package will be named @internal/plugin-digitalrebar
. You are
free to rename these after this tutorial.
Adding the plugin page¶
Let's start by adding some components.
Create plugins/digitalrebar/src/components/Clusters/View.tsx
with the following source:
import React from 'react';
import {Typography, Grid} from '@material-ui/core';
import {
InfoCard,
Header,
Page,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
} from '@backstage/core-components';
import {ClustersTable} from './Table';
export const ClustersView = () => (
<Page themeId="tool">
<Header title="Welcome to Digital Rebar!">
<HeaderLabel label="Owner" value="RackN"/>
<HeaderLabel label="Lifecycle" value="Alpha"/>
</Header>
<Content>
<ContentHeader title="Digital Rebar">
<SupportButton>Some description goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<ClustersTable/>
</Grid>
</Grid>
</Content>
</Page>
);
This is just React filler that follows Backstage's conventions for plugin pages. It should be reminiscent of the
included boilerplate ExampleComponent
.
Now, create plugins/digitalrebar/src/components/Clusters/Table.tsx
with the following source:
import {Progress, Table, TableColumn} from '@backstage/core-components';
import {configApiRef, errorApiRef, useApi} from '@backstage/core-plugin-api';
import Alert from '@material-ui/lab/Alert';
import {DRMachine} from '@rackn/digitalrebar-api';
import React, {useCallback, useMemo, useState} from 'react';
import useAsync from 'react-use/lib/useAsync';
import DeleteIcon from '@material-ui/icons/Delete';
import AddIcon from '@material-ui/icons/Add';
import RemoveIcon from '@material-ui/icons/Remove';
import {IconButton, Link} from '@material-ui/core';
const ClusterActions: React.FC<{ data: DRMachine; type: 'row' | 'group' }> = ({
data,
}) => {
const config = useApi(configApiRef);
const [loading, setLoading] = useState(false);
const [deleted, setDeleted] = useState(false);
const onDelete = useCallback(() => {
if (loading) return;
setLoading(true);
fetch(
`${config.getString('backend.baseUrl')}/api/drp/clusters/${data.Uuid}`,
{method: 'DELETE'}
)
.then(() => {
setDeleted(true);
})
.finally(() => {
setLoading(false);
});
}, [config, loading, data.Uuid]);
if (deleted) return <i>Deleted.</i>;
if (loading) return <Progress/>;
return (
<IconButton size="small" onClick={onDelete}>
<DeleteIcon/>
</IconButton>
);
};
const ClusterCount: React.FC<{ data: DRMachine; type: 'row' | 'group' }> = ({
data,
}) => {
const config = useApi(configApiRef);
const error = useApi(errorApiRef);
const baseUrl = useMemo(() => config.getString('backend.baseUrl'), [config]);
const [loading, setLoading] = useState(false);
const [count, setCount] = useState<number>(
(data.Params?.['cluster/count'] as number | undefined) ?? 0
);
const onScale = useCallback(
(target: number) => {
if (loading) return;
if (target < 0) return;
setLoading(true);
fetch(`${baseUrl}/api/drp/clusters/${data.Uuid}/scale`, {
method: 'PATCH',
body: JSON.stringify({current: count, count: target}),
headers: {'Content-Type': 'application/json'},
})
.then(async (res) => {
if (res.ok) {
setCount(target);
} else {
error.post({
name: 'Cluster scale error',
message: `Failed to scale the cluster ${data.Name} (${data.Uuid}).`,
});
}
})
.catch(() =>
error.post({
name: 'Cluster scale error',
message: `Failed to scale the cluster ${data.Name} (${data.Uuid}).`,
})
)
.finally(() => setLoading(false));
},
[loading, data, baseUrl, error, count]
);
return (
<>
<IconButton size="small" onClick={() => onScale(count - 1)}>
<RemoveIcon/>
</IconButton>
<span>{count}</span>
<IconButton size="small" onClick={() => onScale(count + 1)}>
<AddIcon/>
</IconButton>
</>
);
};
export const ClustersTable = () => {
const config = useApi(configApiRef);
const endpoint = useMemo(
() => config.getString('digitalrebar.endpoint'),
[config]
);
const {value, loading, error} = useAsync(async (): Promise<DRMachine[]> => {
return await fetch(
`${config.getString('backend.baseUrl')}/api/drp/clusters`
).then((r) => r.json());
}, []);
const columns = useMemo<TableColumn<DRMachine>[]>(
() => [
{
title: 'Name',
render: (data, _) => (
<Link
href={`https://portal.rackn.io/#/e/${endpoint}/clusters/${data.Uuid}`}
>
{data.Name}
</Link>
),
},
{
title: 'Size',
render: (data, type) => <ClusterCount data={data} type={type}/>,
},
{title: 'Broker', field: 'Params.broker/name'},
{
title: 'Actions',
render: (data, type) => <ClusterActions data={data} type={type}/>,
},
],
[endpoint]
);
if (loading) {
return <Progress/>;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<Table
title="Clusters"
options={{search: false, paging: false}}
columns={columns}
data={value ?? []}
/>
);
};
Let's break down each of the components individually.
ClusterActions
¶
This component provides the delete button, allowing us to delete Clusters from the table. Of its source, most notably is
the call to /api/drp/clusters/:uuid
, which we added in our backend plugin.
ClustersTable
¶
A table of Clusters. Includes some basic information about each cluster, as well as the ClusterActions
component
allowing us to remove clusters by their row.
useApi
and useAsync
are hooks provided by Backstage, as well as the Table
component (and its child TableColumn
s)
There is an official Backstage tutorial that goes into
further detail on these pieces.
You'll notice the call to useAsync
uses the GET /clusters
route we added in our backend plugin. The
preceding /drp
was declared when we registered the backend plugin with Backstage.
Registering the plugin with Backstage¶
Conveniently, creating the plugin automatically registers it within Backstage's frontend router (mostly). However, you will need to change the exported component to be the one you created rather than the included example component.
Open plugins/digitalrebar/src/plugin.ts
, and change these lines:
to this:
DRP Authorization¶
Similarly to what you did for the backend plugin, you will need to create a config.d.ts
file for this plugin.
Create the file plugins/digitalrebar/config.d.ts
, and paste the following into it:
export interface Config {
digitalrebar: {
/**
* The endpoint (IP and port) of the DRP instance.
* @visibility frontend
*/
endpoint: string;
/**
* The auth token of the DRP instance.
* @visibility secret
*/
token: string;
};
}
This simply instructs Backstage to check for this config schema when it reads its app-config.yaml
. Finally, you need
to register this schema file with your plugin package. Open plugins/digitalrebar/package.json
and remove the entry
for files
. Then, add the following to the root object:
Register it in another instance¶
If you would like to register it with another Backstage instance after publishing the plugin, check the diffs
in packages/app/src/App.tsx
after creating the plugin. It should be the addition of an import
line and a new Route
entry.
Usage¶
Start your Backstage instance with
and navigate to http://localhost:3000/digitalrebar
to see your changes.
Optionally, create a Cluster with the template you set up in the scaffolder tutorial, and report back to see if it is now in the frontend table. Check to see that the delete icon deletes your Cluster.