Skip to main content

Creating a Custom No-Code UI for a Third-Party Integration

Overview

This tutorial guides you through building a No-Code UI for a custom integration. A No-Code UI provides a more user-friendly way to configure your fact collector options directly through the Soundcheck interface, rather than manually editing YAML configuration files.

Soundcheck offers a No-Code UI configuration option for every integration developed by Spotify. To create a No-Code UI for your custom integration, you can leverage frontend extensions available in the new frontend system.

note

You cannot directly edit, extend, or replace the No-Code UI for integrations pre-built and shipped by Soundcheck. If you need a custom No-Code UI for an existing Soundcheck integration, you must build a custom integration for that fact collector along with a new No-Code UI, then disable the pre-built one. See Step 5: Configure Integration Extension for how to disable a pre-built integration.

What you'll build: A No-Code UI for the ExamplePagerDutyFactCollector (the custom PagerDuty integration we built in the Creating a Custom Third-Party Integration tutorial). This No-Code UI allows users to configure the integration directly from the Soundcheck Integrations page, rather than editing YAML files. We'll also disable the pre-shipped PagerDuty integration to use our custom one instead.

Time to complete: ~20 minutes

📖 What are integration extensions?

Integration extensions are the pages that appear when you click "Configure" on an Integration within the Soundcheck Integrations tab. The "Configure" button will only display for an integration when an integration extension is installed for that Third-Party Integration.

note

It's currently not possible to change the content of the integration table's rows.

The following integration extensions are provided by Soundcheck and enabled by default:

app-config.yaml
app:
extensions:
- integration:soundcheck/azure # Azure DevOps Integration
- integration:soundcheck/bigquery # BigQuery Integration
- integration:soundcheck/catalog # Software Catalog Integration
- integration:soundcheck/datadog # Datadog Integration
- integration:soundcheck/data-registry # Data Registry Integration
- integration:soundcheck/github # GitHub Integration
- integration:soundcheck/gitlab # GitLab Integration
- integration:soundcheck/http # HTTP Integration
- integration:soundcheck/jira # Jira Integration
- integration:soundcheck/kubernetes # Kubernetes Integration
- integration:soundcheck/newrelic # NewRelic Integration
- integration:soundcheck/pagerduty # PagerDuty Integration
- integration:soundcheck/scm # Source Code Management Integration
- integration:soundcheck/sonarqube # SonarQube Integration

Prerequisites

Before starting, ensure you have:

  • Backstage migrated to the new frontend system (Portal uses the new frontend system exclusively)

    note

    Either a complete transition or a hybrid migration to the new frontend system is supported. If using a hybrid migration, complete Steps 1–6 first, then follow the Additional Steps for Hybrid Frontend Migrations section.

  • A Third-Party Integration that is configurable (implements ConfigurableFactCollector interface). Most integrations developed by Spotify are configurable. Exceptions: Tech Insights and Soundcheck (they will be configurable in the future)

  • Soundcheck plugin modules and the corresponding Third-Party Integration backend module installed in your Backstage instance. See Creating a Custom Third-Party Integration for how to create one


Implementation

Step 1: Create the Frontend Plugin

Use the Backstage CLI to create a new frontend plugin for your integration:

yarn backstage-cli new

When prompted, enter:

PromptValue
What do you want to create?frontend-plugin
Enter the ID of the pluginsoundcheck-integrations

You'll see: "Successfully created frontend-plugin."

note

The new frontend system is still in alpha and therefore not yet the default when creating frontend plugins. Frontend plugins should follow the migration guide to create an alpha export (this will be covered in the next steps).

You may remove the ExampleComponent & ExampleFetchComponent and their exports/imports that were created by the CLI.


Step 2: Create the alpha.tsx File

Create an alpha.tsx file that will contain your plugin declaration compatible with the new frontend system:

plugins/soundcheck-integrations/src/alpha.tsx
import { convertLegacyRouteRefs } from '@backstage/core-compat-api';
import { createFrontendPlugin, ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { rootRouteRef } from './routes';


import { IntegrationPageBlueprint } from '@spotify/backstage-plugin-soundcheck';

// extension reference: integration:soundcheck-integrations/example-pagerduty
export const pagerDutyIntegration: ExtensionDefinition = IntegrationPageBlueprint.make({
name: 'example-pagerduty', // extension name
params: {
integrationId: 'example-pagerduty', // IMPORTANT: The value must match the ID of the corresponding fact collector
loader: async () =>
import('./components/IntegrationComponent').then(
m => <m.IntegrationComponent collectorId="example-pagerduty" />, // React component created in the previous step
),
},
});

export default createFrontendPlugin({
pluginId: 'soundcheck-integrations',
routes: convertLegacyRouteRefs({
root: rootRouteRef,
}),
extensions: [pagerDutyIntegration],
});
note

Ensure integrationId and collectorId match the ID of the corresponding fact collector (the custom fact collector you have created).

📖 Integration Extension Configuration Options

The IntegrationPageBlueprint supports the following configuration options:

OptionRequiredDescription
nameNoExtension name, should be unique for all integration extensions. Must be provided if your plugin implements more than one integration extension
disabledNoA boolean flag, if set to true it disables the extension by default
params.integrationIdYesID of the corresponding fact collector
params.loaderYesA function that returns the custom implementation of the No-Code UI (React component)
params.pathNoA path to the integration page. Default value is /soundcheck/integrations/:integrationId
params.routeRefNoA routing reference to expose a path in Backstage's routing system

The extension reference follows the pattern <kind>:<namespace>/<name> where:

PartDescription
kindDefaulted to integration for extensions created from IntegrationPageBlueprint extension blueprint
namespaceID of your frontend plugin that implements the extension (example: soundcheck-integrations)
nameExtension name (example: pagerduty)

Step 3: Update package.json

Update your package.json file in the soundcheck-integrations plugin:

  • Include alpha.tsx file in exports and typesVersions
  • Include @spotify/backstage-plugin-soundcheck, @backstage/core-compat-api and @backstage/frontend-plugin-api modules in dependencies
plugins/soundcheck-integrations/package.json
{
"name": "@internal/backstage-plugin-soundcheck-integrations",
"description": "Custom Soundcheck Integrations.",
"version": "0.1.0",
"backstage": {
"role": "frontend-plugin"
},
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.tsx",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
"src/alpha.tsx"
],
"package.json": [
"package.json"
]
}
},
"dependencies": {
"@backstage/core-compat-api": "backstage:^",
"@backstage/frontend-plugin-api": "backstage:^",
// minimum version that supports custom No-Code UI is 0.17.0
"@spotify/backstage-plugin-soundcheck": "workspace:^",
}
}
note

The backstage:^ version syntax requires the Backstage Yarn plugin to be installed in your project. This plugin automatically resolves the correct package version based on your Backstage release.

Then run:

yarn install
📖 Soundcheck frontend exports for No-Code UI

The Soundcheck frontend module exports the following constants and functions for creating a custom No-Code UI:

ExportDescription
IntegrationPageBlueprintExtension blueprint for the custom integration config page
useGetCollectorsReact hook that fetches collector configurations from the database and/or YAML by their IDs. See Appendix: React Hooks Reference
useUpdateCollectorConfigReact hook that saves collector configuration to the database. See Appendix: React Hooks Reference

Step 4: Create the Integration React Component

Implement a React component that will display the configuration options for the Third-Party Integration:

plugins/soundcheck-integrations/src/components/IntegrationComponent.tsx
import { Content } from '@backstage/core-components';
import { Button, Grid, TextField, Typography } from '@material-ui/core';
import {
useGetCollectors,
useUpdateCollectorConfig,
} from '@spotify/backstage-plugin-soundcheck';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

export const IntegrationComponent = ({
collectorId,
}: {
collectorId: string;
}) => {
const navigate = useNavigate();
const navigateBack = () => navigate('/soundcheck/integrations');

// fetching integration data from the database and/or YAML
const { data } = useGetCollectors([collectorId]);
const { mutateAsync: updateCollectorConfig } = useUpdateCollectorConfig();

const collector = data?.[0];
const [config, setConfig] = useState<string>();

useEffect(() => {
if (collector?.config) {
setConfig(JSON.stringify(collector.config, null, '\t'));
}
}, [collector]);

if (!collector)
return (
<Typography variant="h1">{collectorId} Integration Not Found</Typography>
);

const disabled = !collector.isEditable || !collector.isConfigurable;

const onSave = async () => {
// saving integration data to the database
const result = await updateCollectorConfig({
collectorId: collectorId,
config: config ? JSON.parse(config) : {},
});
if (result) navigateBack();
};

return (
<Content>
<Grid container direction="column" spacing={2}>
<Grid item>
<Typography variant="h1">
{collector.name ?? collector.id} Integration
</Typography>
<Typography variant="subtitle2">{collector.description}</Typography>
</Grid>
<Grid item>
<TextField
label="JSON Configuration"
placeholder="Enter JSON configuration"
variant="outlined"
disabled={disabled}
fullWidth
multiline
minRows={5}
maxRows={25}
value={config}
onChange={(event) => setConfig(event.target.value)}
/>
</Grid>
<Grid item>
<Grid container spacing={1}>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={disabled}
onClick={onSave}
>
Save
</Button>
</Grid>
<Grid item>
<Button
variant="text"
color="primary"
disabled={disabled}
onClick={navigateBack}
>
Cancel
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</Content>
);
};
note

This is a demo component that doesn't implement any validation and it's not meant to be used as-is in production.


Step 5: Configure Integration Extension

While extensions derived from the IntegrationPageBlueprint blueprint do not offer custom configuration options, they do universally support the disabled option (note that integration extensions are enabled by default).

Add the following configuration to disable the pre-shipped PagerDuty integration and enable the No-Code UI for the custom PagerDuty integration you just built:

app-config.yaml
app:
extensions:
# Disable the pre-shipped PagerDuty integration
- integration:soundcheck/pagerduty:
disabled: true
# Enable the custom PagerDuty integration No-Code UI
- integration:soundcheck-integrations/example-pagerduty

Step 6: Verify Your Integration

  1. Save your changes and run your Backstage instance
  2. Navigate to SoundcheckIntegrations
  3. Confirm your custom integration shows a "Configure" button

Clicking the "Configure" button will take you to the No-Code UI that you created in Step 4.

🎉 Congratulations! Your custom No-Code UI is now running.


Additional Steps for Hybrid Frontend Migrations

If you have completed a full transition to the new frontend system and have frontend feature discovery enabled, no additional steps are required—your plugins will be discovered and registered automatically.

However, if you have done a hybrid migration to the new frontend system (feature discovery is not enabled), you need to manually register the features by following these additional steps.

Hybrid Step 1: Add Dependencies to Your App

Add the following dependencies to your packages/app/package.json and run yarn install:

packages/app/package.json
{
"dependencies": {
"@internal/backstage-plugin-soundcheck-integrations": "workspace:^",
"@spotify/backstage-plugin-soundcheck": "^0.19.8"
}
}
yarn install
note

Make sure the dependency name for your custom integration is correct. If you have followed this tutorial with no changes, the name above is correct.

Hybrid Step 2: Remove the Soundcheck Route

Remove the Soundcheck Route from packages/app/src/App.tsx as this will now be handled by the new frontend system.

packages/app/src/App.tsx
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="catalog" />} />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route path="/docs" element={<TechDocsIndexPage />} />
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
path="/catalog-import"
element={
<RequirePermission permission={catalogEntityCreatePermission}>
<CatalogImportPage />
</RequirePermission>
}
/>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route path="/notifications" element={<NotificationsPage />} />
<Route path='/soundcheck' element={<SoundcheckRoutingPage title='Soundcheck' />} />
</FlatRoutes>
);

Hybrid Step 3: Register the Plugins

Import the Soundcheck plugin and your custom integration, then add them to the features array in createApp in App.tsx:

packages/app/src/App.tsx
import soundCheckPlugin from '@spotify/backstage-plugin-soundcheck/alpha';
import examplePagerDutyIntegration from '@internal/backstage-plugin-soundcheck-integrations/alpha';

const app = createApp({
features: [
// ... other features
soundCheckPlugin,
examplePagerDutyIntegration,
],
// ... other options
});
note

Make sure the import path for your custom integration is correct. If you have followed this tutorial with no changes, the path above is correct.

📖 Full example: App.tsx with hybrid migration
packages/app/src/App.tsx
import React from 'react';
import { createApp } from '@backstage/frontend-defaults';
import { convertLegacyApp } from '@backstage/core-compat-api';

// Import the Soundcheck plugin and your custom integration
import soundCheckPlugin from '@spotify/backstage-plugin-soundcheck/alpha';
import examplePagerDutyIntegration from '@internal/backstage-plugin-soundcheck-integrations/alpha';

// Your legacy app routes (excluding the Soundcheck route which is now handled by the new frontend system)
const legacyApp = convertLegacyApp(
// ... your legacy routes
);

const app = createApp({
features: [
legacyApp,
soundCheckPlugin,
examplePagerDutyIntegration,
// ... other features
],
});

export default app.createRoot();

Hybrid Step 4: Verify Your Integration

  1. Save your changes and run your Backstage instance
  2. Navigate to SoundcheckIntegrations
  3. Confirm your custom integration shows a "Configure" button

🎉 Congratulations! Soundcheck will now register your No-Code UI plugin. You should be able to click the "Configure" button for your custom integration and access the configuration page you built.


Appendix: React Hooks Reference

The Soundcheck frontend module provides the following React hooks for building custom No-Code UI components.

useGetCollectors

📖 useGetCollectors - Fetch collector configurations

React hook that fetches collector configurations from the database (collector table) and/or YAML by their IDs. If the collector's configuration is found in both database and YAML and the YAML configuration is a plain object (not an array), the configurations will be merged (it's recommended to configure secrets in YAML only and don't expose secret configuration in No-Code UI for security reasons). Otherwise, the database configuration takes precedence. Collector's YAML configuration can be found under soundcheck.collectors.<integrationId> in app-config.yaml file.

Usage example:

import { useGetCollectors } from '@spotify/backstage-plugin-soundcheck';

const { data } = useGetCollectors(['pagerduty']);
const collector = data?.[0];

Collector type definition:

import { JsonObject } from '@backstage/types';

export type Collector = {
// Integration ID provided in the backend implementation of the fact collector.
// Implemented in `FactCollector.id`
id: string;
// Integration name provided in the backend implementation of the fact collector.
// Implemented in `FactCollector.name`
name?: string;
// Integration description provided in the backend implementation of the fact collector.
// Implemented in `FactCollector.description`
description?: string;
// A flag that's set to true if the collector's backend implements `ConfigurableFactCollector` interface.
isConfigurable: boolean;
// A flag that's set to true if the collector is configurable and the configuration in the YAML file is a plain object (not an array).
isEditable: boolean;
// The names of the facts that this collector can collect.
// Implemented in `FactCollector.getFactNames()`
factNames: string[];
// Current collector configuration as JSON.
// Implemented in `ConfigurableFactCollector.getConfig()`
config?: JsonObject;
// JSON schema for the collectors configuration (can be used for validation).
// Implemented in `ConfigurableFactCollector.getConfigSchema()`
configSchema?: JsonObject;
// Current collector configuration as `CollectionConfig[]` array.
// Implemented in `FactCollector.getCollectionConfigs()`
collectionConfigs: {
// The facts to which this collection configuration applies.
factRefs: string[];
// A filter specifying which entities to collect the specified facts for.
filter?: JsonObject;
// The frequency at which the specified facts should be collected.
frequency?: JsonObject;
// If the collected facts should be cached, and if so for how long.
cache?: JsonObject;
// A filter specifying which entities to exclude from collection.
exclude?: JsonObject;
}[];
};

Example response:

{
"id": "pagerduty",
"name": "PagerDuty",
"description": "Collects facts from your PagerDuty instance.",
"isConfigurable": true,
"isEditable": true,
"factNames": [
"pagerduty:default/service",
"pagerduty:default/standards",
"pagerduty:default/incidents"
],
"config": {
"collects": [
{
"type": "Service",
"frequency": {
"hours": 1
},
"filter": [
{
"kind": ["Component"]
}
],
"cache": false
},
{
"type": "Standards",
"frequency": {
"hours": 1
},
"filter": [
{
"kind": ["Component"]
}
],
"cache": false
},
{
"factName": "incidents",
"type": "Incidents",
"statuses": ["triggered", "acknowledged", "resolved"],
"frequency": {
"hours": 1
},
"filter": [
{
"kind": ["Component"]
}
],
"cache": false
}
]
},
"collectionConfigs": [
{
"factRefs": ["pagerduty:default/service"],
"filter": [
{
"kind": ["Component"]
}
],
"frequency": {
"hours": 1
},
"cache": false,
"exclude": null
},
{
"factRefs": ["pagerduty:default/standards"],
"filter": [
{
"kind": ["Component"]
}
],
"frequency": {
"hours": 1
},
"cache": false,
"exclude": null
},
{
"factRefs": ["pagerduty:default/incidents"],
"filter": [
{
"kind": ["Component"]
}
],
"frequency": {
"hours": 1
},
"cache": false,
"exclude": null
}
]
}

useUpdateCollectorConfig

📖 useUpdateCollectorConfig - Save collector configuration

React hook that saves collector configuration to the database (collector table). No-Code UI configuration is always stored in the database as a raw JSON object. The configuration JSON object should only include the actual collector's configuration (aka what you'd configure under soundcheck.collectors.<integrationId> in app-config.yaml file as YAML config). The configuration should be a valid JSON object, otherwise an error will be thrown by the backend.

On top of persisting the config to the database the hook also calls ConfigurableFactCollector.setConfig() method implemented by the fact collector, so the fact collector will be aware of the updated config right away.

Usage example:

import { useUpdateCollectorConfig } from '@spotify/backstage-plugin-soundcheck';

const { mutateAsync: updateCollectorConfig } = useUpdateCollectorConfig();

const onSave = async () => {
const result = await updateCollectorConfig({
collectorId: 'pagerduty', // must be a valid integrationId
// must be a valid JSON configuration object
config: {
collects: [
{
type: 'Service',
cache: false,
filer: [
{
kind: ['Component'],
},
],
frequency: {
hours: 1,
},
},
],
},
});
if (result) console.log('Success!');
};