Merge branch 'master' into scaffold

This commit is contained in:
Fabian Chong
2020-09-10 14:46:46 +08:00
228 changed files with 2821 additions and 2557 deletions
+18
View File
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 5
labels:
- dependencies
- package-ecosystem: npm
directory: '/microsite/'
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 2
labels:
- dependencies
+1 -1
View File
@@ -76,7 +76,7 @@ jobs:
run: yarn lerna -- run lint --since origin/master
- name: type checking and declarations
run: yarn tsc --incremental false
run: yarn tsc:full
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
run: yarn lerna -- run lint
- name: type checking and declarations
run: yarn tsc --incremental false
run: yarn tsc:full
- name: verify type dependencies
run: yarn lint:type-deps
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
run: yarn lerna -- run lint
- name: type checking and declarations
run: yarn tsc --incremental false
run: yarn tsc:full
- name: build
run: yarn build
+5
View File
@@ -8,6 +8,11 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
> Collect changes for the next release below
- Material-UI: Bumped to 4.11.0, which is the version that create-app will
resolve to, because we wanted to get the renaming of ExpansionPanel to
Accordion into place. This gets rid of a lot of console deprecation warnings
in newly scaffolded apps.
- The backend plugin
[service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts)
no longer adds `express.json()` automatically to all routes. While convenient
+11 -1
View File
@@ -12,7 +12,16 @@ backend:
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
'/circleci/api': https://circleci.com/api/v1.1
'/circleci/api':
target: https://circleci.com/api/v1.1
changeOrigin: true
pathRewrite:
'^/proxy/circleci/api/': '/'
headers:
Circle-Token:
$secret:
env: CIRCLECI_AUTH_TOKEN
'/jenkins/api':
target: http://localhost:8080
headers:
@@ -25,6 +34,7 @@ organization:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
sentry:
organization: spotify
+126 -51
View File
@@ -17,7 +17,7 @@ Utility APIs. While the `createPlugin` API is focused on the initialization
plugins and the app, the Utility APIs provide ways for plugins to communicate
during their entire life cycle.
## Usage
## Consuming APIs
Each Utility API is tied to an `ApiRef` instance, which is a global singleton
object without any additional state or functionality, its only purpose is to
@@ -55,51 +55,112 @@ from any component inside Backstage, including the ones in `@backstage/core`.
The only requirement is that they are beneath the `AppProvider` in the react
tree.
## Registering Utility API Implementations
## Supplying APIs
The Backstage App is responsible for providing implementations for all Utility
APIs required by plugins. The example app in this repo registers its APIs inside
[src/apis.ts](/packages/app/src/apis.ts). Here's an example of how to wire up
the `ErrorApi` inside an app:
### API Factories
APIs are registered in the form of `ApiFactories`, which encapsulate the process
of instantiating an API. It is a collection of three things: the `ApiRef` of the
API to instantiate, a list of all required dependencies, and a factory function
that returns a new API instance.
For example, this is the default `ApiFactory` for the `ErrorApi`:
```ts
import {
ApiRegistry,
createApp,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
ConfigApi,
} from '@backstage/core';
const apis = (config: ConfigApi) => {
const builder = ApiRegistry.builder();
// The alert API is a self-contained implementation that shows alerts to the user.
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
// The error API uses the alert API to send error notifications to the user.
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
return builder.build();
};
const app = createApp({
apis,
// ... other config
createApiFactory({
api: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) =>
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
});
```
The `ApiRegistry` is used to register all Utility APIs in the app and associate
them with `ApiRef`s. It implements the `ApiHolder` interface, which enables it
to provide an API implementation given an `ApiRef`.
In this example the `errorApiRef` is our API, which encapsulates the `ErrorApi`
type. The `alertApiRef` is our single dependency, which we give the name
`alertApi`, and is then passed on to the factory function, which returns an
implementation of the `ErrorApi`.
Note that our `ErrorApi` implementation depends on another Utility API, the
`AlertApi`. This is the method with which APIs can depend on other APIs, using
manual dependency injection at the initialization of the app. In general, if you
want to depend on another Utility API in an implementation of an API, you import
the type for that API and make it a constructor parameter.
The `createApiFactory` function is a thin wrapper that enables TypeScript type
inference. You may notice that there are no type annotations in the above
example, and that is because we're able to infer all types from the `ApiRef`s.
TypeScript will make sure that the return value of the `factory` function
matches the type embedded in `api`'s `ApiRef`, in this case the `ErrorApi`. It
will also match the types between the `deps` and the parameters of the `factory`
function, again using the type embedded within the `ApiRef`s.
## Registering API Factories
The responsibility for adding Utility APIs to a Backstage app lies in three
different locations: the Backstage core library, each plugin included in the
app, and the app itself.
### Core APIs
Starting with the Backstage core library, it provides implementation for all of
the core APIs. The core APIs are the ones exported by `@backstage/core`, such as
the `errorApiRef` and `configApiRef`. You can find a full list of them
[here](../reference/utility-apis/README.md).
The core APIs are loaded for any app created with `createApp` from
`@backstage/core`, which means that there is no step that needs to be taken to
include these APIs in an app.
### Plugin APIs
In addition to the core APIs, plugins can define and export their own APIs.
While doing so they should usually also provide default implementations of their
own APIs, for example, the `catalog` plugin exports `catalogApiRef`, and also
supplies a default `ApiFactory` of that API using the `CatalogClient`. There is
one restriction to plugin-provided API Factories: plugins may not supply
factories for core APIs, trying to do so will cause the app to crash.
Plugins supply their APIs through the `apis` option of `createPlugin`, for
example:
```ts
export const plugin = createPlugin({
id: 'techdocs',
apis: [
createApiFactory({
api: techdocsStorageApiRef,
deps: { configApi: configApiRef },
factory({ configApi }) {
return new TechDocsStorageApi({
apiOrigin: configApi.getString('techdocs.storageUrl'),
});
},
}),
],
});
```
### App APIs
Lastly, the app itself is the final point where APIs can be added, and what has
the final say in what APIs will be loaded at runtime. The app may override the
factories for any of the core or plugin APIs, with the exception of the config,
app theme, and identity APIs. These are static APIs that are tied into the
`createApp` implementation, and therefore not possible to override.
Overriding APIs is useful for apps that want to switch out behavior to tailor it
to their environment. In some cases plugins may also export multiple
implementations of the same API, where they each have their own different
requirements on for example backend storage and surrounding environment.
Supplying APIs to the app works just like for plugins:
```ts
const app = createApp({
apis: [
/* ApiFactories */
],
// ... other options
});
```
A common pattern is to export a list of all APIs from `apis.ts`, next to
`App.tsx`. See the [example app in this repo](../../packages/app/src/apis.ts)
for an example.
## Custom implementations of Utility APIs
@@ -127,19 +188,33 @@ implement the `ErrorApi`, as it is checked by the type embedded in the
## Defining custom Utility APIs
The pattern for plugins defining their own Utility APIs is not fully established
yet. The current way is for the plugin to export its own `ApiRef` and type for
the API, along with one or more implementations. It is then up to the app to
import, and register those APIs. See for example the
[lighthouse](/plugins/lighthouse/src/api.ts) or
[graphiql](/plugins/graphiql/src/lib/api/types.ts) plugins for examples of this.
Plugins are free to define their own Utility APIs. Simply define the TypeScript
interface for the API, and create an `ApiRef` using `createApiRef` exported from
`@backstage/core`. Also be sure to provide at least one implementation of the
API, and to declare a default factory for the API in `createPlugin`.
The goal is to make this process a bit smoother, but that requires work in other
parts of Backstage, like configuration management. So it remains as a TODO. If
you have more questions regarding this, or have an idea for an API that you want
to share outside your plugin, hit us up in
[GitHub issues](https://github.com/spotify/backstage/issues/new/choose) or the
[Backstage Discord server](https://discord.gg/EBHEGzX).
Custom Utility APIs can be either public or private, which it is up to the
plugin to choose. Private APIs do not expose an external API surface, and it's
therefore possible to make breaking changes to the API without affecting other
users of the plugin. If an API is made public however, it opens up for other
plugins to make use of the API, and it also makes it possible for users for your
plugin to override the API in the app. It is however important to maintain
backwards compatibility of public APIs, as you may otherwise break apps that are
using your plugin.
To make an API public, simply export the `ApiRef` of the API, and any associated
types. To make an API private, just avoid exporting the `ApiRef`, but still be
sure to supply a default factory to `createPlugin`.
Private APIs are useful for plugins that want to depend on other APIs outside of
React components, but not have to expose an entire API surface to maintain. When
using private APIs, it is fine to use the `typeof` of an implementing class as
the type parameter passed to `createApiRef`, while public APIs should always
define a separate TypeScript interface type.
Plugins may depend on APIs from other plugins, both in React components and as
dependencies to API factories. Do however be sure to not cause circular
dependencies between plugins.
## Architecture
@@ -68,7 +68,7 @@ metadata:
lifecycle: production
example.com/service-discovery-name: frobsawesome
annotations:
circleci.com/project-slug: gh/example-org/frobs-awesome
circleci.com/project-slug: github/example-org/frobs-awesome
spec:
type: service
```
+1 -1
View File
@@ -4,7 +4,7 @@ title: Architecture Decision Records (ADR)
sidebar_label: Overview
---
The substantial architecture decisions made in the Backstage project lives here.
The substantial architecture decisions made in the Backstage project live here.
For more information about ADRs, when to write them, and why, please see
[this blog post](https://engineering.atspotify.com/2020/04/14/when-should-i-write-an-architecture-decision-record/).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 102 KiB

@@ -34,7 +34,7 @@ software catalog API.
"annotations": {
"backstage.io/managed-by-location": "file:/tmp/component-info.yaml",
"example.com/service-discovery": "artistweb",
"circleci.com/project-slug": "gh/example-org/artist-website"
"circleci.com/project-slug": "github/example-org/artist-website"
},
"description": "The place to be, for great artists",
"etag": "ZjU2MWRkZWUtMmMxZS00YTZiLWFmMWMtOTE1NGNiZDdlYzNk",
@@ -66,7 +66,7 @@ metadata:
system: public-websites
annotations:
example.com/service-discovery: artistweb
circleci.com/project-slug: gh/example-org/artist-website
circleci.com/project-slug: github/example-org/artist-website
tags:
- java
spec:
+1 -1
View File
@@ -1,7 +1,7 @@
---
id: software-catalog-overview
title: Backstage Service Catalog (alpha)
sidebar_label: Backstage Service Catalog
sidebar_label: Overview
---
## What is a Service Catalog?
@@ -1,4 +1,7 @@
# Installing in your Backstage App
---
id: installation
title: Installing in your Backstage App
---
The catalog plugin comes in two packages, `@backstage/plugin-catalog` and
`@backstage/plugin-catalog-backend`. Each has their own installation steps,
+2 -1
View File
@@ -1,6 +1,7 @@
---
id: software-templates-index
title: Software Templates
title: Backstage Software Templates
sidebar_label: Overview
---
The Software Templates part of Backstage is a tool that can help you create
@@ -1,4 +1,7 @@
# Installing in your Backstage App
---
id: installation
title: Installing in your Backstage App
---
The scaffolder plugin comes in two packages, `@backstage/plugin-scaffolder` and
`@backstage/plugin-scaffolder-backend`. Each has their own installation steps,
+16 -5
View File
@@ -28,7 +28,7 @@ installed:
To create a new Backstage application for TechDocs, run the following command:
```bash
npx @backstage/cli create-app
npx @backstage/create-app
```
You will then be prompted to enter a name for your application. Once that's
@@ -74,17 +74,28 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs';
### Setting the configuration
TechDocs allows for configuration of the docs storage URL through your
`app-config` file.
`app-config` file. We provide two different values to be configured,
`requestUrl` and `storageUrl`. The `requestUrl` is what the reader will request
its data from, and `storageUrl` is where the backend can find the stored
documentation.
The default storage URL:
The default storage and request URLs:
```yaml
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
```
If you want to configure this to point to another storage URL, change the value
of `storageUrl`.
If you want `techdocs-backend` to manage building and publishing you want
`requestUrl` to point to the default value (or wherever `techdocs-backend` is
hosted). `storageUrl` should be where your publisher publishes your docs. Using
the default `LocalPublish` that is the default value.
If you have a setup where you are not using `techdocs-backend` for managing
building and publishing of your documentation you want to change the
`requestUrl` to point to your storage. In this case `storageUrl` is not
required.
## Run Backstage locally
@@ -3,4 +3,34 @@ id: configure-app-with-plugins
title: Configuring App with plugins
---
## Adding existing plugins to your app
Coming soon!
### Adding a plugin page to the Sidebar
In a standard Backstage app created with
[@backstage/create-app](./create-an-app.md), the sidebar is managed inside
`packages/app/src/sidebar.tsx`. The file exports the entire `Sidebar` element of
your app, which you can extend with additional entries by adding new
`SidebarItem` elements.
For example, if you install the `api-docs` plugin, a matching `SidebarItem`
could be something like this:
```tsx
// Import icon from MUI
import ExtensionIcon from '@material-ui/icons/Extension';
// ... inside the AppSidebar component
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />;
```
You can also use your own SVGs directly as icon components. Just make sure they
are sized according to the Material UI's
[SvgIcon](https://material-ui.com/api/svg-icon/) default of 24x24px, and set the
extension to `.icon.svg`. For example:
```ts
import InternalToolIcon from './internal-tool.icon.svg';
```
+44 -33
View File
@@ -15,7 +15,13 @@ To create a Backstage app, you will need to have
[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12).
With `npx`:
Backstage provides a utility for creating new apps. It guides you through the
initial setup of selecting the name of the app and a database for the backend.
The database options are either SQLite or PostgreSQL, where the latter requires
you to set up a separate database instance. If in doubt, choose SQLite, but
don't worry about the choice, it's easy to change later!
The easiest way to run the create app package is with `npx`:
```bash
npx @backstage/create-app
@@ -25,50 +31,45 @@ This will create a new Backstage App inside the current folder. The name of the
app-folder is the name that was provided when prompted.
<p align='center'>
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/create-app_output.png' width='600' alt='create app'>
<img src='../assets/getting-started/create-app_output.png' width='600' alt='create app'>
</p>
Inside that directory, it will generate all the files and folder structure
needed for you to run your app.
### Folder structure
### General folder structure
Below is a simplified layout of the files and folders generated when creating an
app.
```
app
├── README.md
├── app-config.yaml
├── lerna.json
├── package.json
── prettier.config.js
├── tsconfig.json
── packages
│ └── app
│ ├── package.json
│ ├── tsconfig.json
│ ├── public
│ │ └── ...
│ └── src
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.tsx
│ ├── plugins.ts
│ └── setupTests.ts
└── plugins
└── welcome
├── README.md
├── package.json
├── tsconfig.json
└── src
├── index.ts
├── plugin.test.ts
├── plugin.ts
├── setupTests.ts
└── components
├── Timer
│ └── ...
└── WelcomePage
└── ...
── packages
  ├── app
  ── backend
```
- **app-config.yaml**: Main configuration file for the app. See
[Configuration](https://backstage.io/docs/conf/) for more information.
- **lerna.json**: Contains information about workspaces and other lerna
configuration needed for the monorepo setup.
- **package.json**: Root package.json for the project. _Note: Be sure that you
don't add any npm dependencies here as they probably should be installed in
the intended workspace rather than in the root._
- **packages/**: Lerna leaf packages or "workspaces". Everything here is going
to be a separate package, managed by lerna.
- **packages/app/**: An fully functioning Backstage frontend app, that acts as a
good starting point for you to get to know Backstage.
- **packages/backend/**: We include a backend that helps power features such as
[Authentication](https://backstage.io/docs/auth/),
[Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview),
[Software Templates](https://backstage.io/docs/features/software-templates/software-templates-index)
and [TechDocs](https://backstage.io/docs/features/techdocs/techdocs-overview)
amongst other things.
## Run the app
When the installation is complete you can open the app folder and start the app.
@@ -80,3 +81,13 @@ yarn start
_When `yarn start` is ready it should open up a browser window displaying your
app, if not you can navigate to `http://localhost:3000`._
In most cases you will want to start the backend as well, as it is required for
the catalog to work, along with many other plugins.
To start the backend, open a separate terminal session and run the following in
the root directory:
```bash
yarn workspace backend start
```
@@ -65,6 +65,7 @@ yarn storybook # Start local storybook, useful for working on components in @bac
yarn workspace @backstage/plugin-welcome start # Serve welcome plugin only, also supports --check
yarn tsc # Run typecheck, use --watch for watch mode
yarn tsc:full # Run full type checking, for example without skipLibCheck, use in CI
yarn build # Build published versions of packages, depends on tsc
+5 -5
View File
@@ -1,7 +1,7 @@
# AlertApi
The AlertApi type is defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
The following Utility API implements this type: [alertApiRef](./README.md#alert)
@@ -38,7 +38,7 @@ export type AlertMessage = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
Referenced by: [post](#post), [alert\$](#alert).
@@ -67,7 +67,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [alert\$](#alert).
@@ -86,7 +86,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -109,6 +109,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+8 -8
View File
@@ -1,7 +1,7 @@
# AppThemeApi
The AppThemeApi type is defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50).
[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50).
The following Utility API implements this type:
[appThemeApiRef](./README.md#apptheme)
@@ -76,7 +76,7 @@ export type AppTheme = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24).
[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24).
Referenced by: [getInstalledThemes](#getinstalledthemes).
@@ -87,7 +87,7 @@ export type BackstagePalette = Palette &amp; <a href="#paletteadditions">Palette
</pre>
Defined at
[packages/theme/src/types.ts:67](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/theme/src/types.ts#L67).
[packages/theme/src/types.ts:70](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L70).
Referenced by: [BackstageTheme](#backstagetheme).
@@ -100,7 +100,7 @@ export interface BackstageTheme extends Theme {
</pre>
Defined at
[packages/theme/src/types.ts:70](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/theme/src/types.ts#L70).
[packages/theme/src/types.ts:73](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L73).
Referenced by: [AppTheme](#apptheme).
@@ -129,7 +129,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [activeThemeId\$](#activethemeid).
@@ -148,7 +148,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -204,7 +204,7 @@ type PaletteAdditions = {
</pre>
Defined at
[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/theme/src/types.ts#L23).
[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/theme/src/types.ts#L23).
Referenced by: [BackstagePalette](#backstagepalette).
@@ -227,6 +227,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,16 +1,20 @@
# BackstageIdentityApi
The BackstageIdentityApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L144).
[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L144).
The following Utility APIs implement this type:
- [auth0AuthApiRef](./README.md#auth0auth)
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [microsoftAuthApiRef](./README.md#microsoftauth)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
@@ -62,7 +66,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
@@ -83,6 +87,6 @@ export type BackstageIdentity = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L157).
[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L157).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
+17 -9
View File
@@ -1,13 +1,19 @@
# Config
The Config type is defined at
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/config/src/types.ts#L32).
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L32).
The following Utility API implements this type:
[configApiRef](./README.md#config)
## Members
### has()
<pre>
has(key: string): boolean
</pre>
### keys()
<pre>
@@ -17,13 +23,13 @@ keys(): string[]
### get()
<pre>
get(key: string): <a href="#jsonvalue">JsonValue</a>
get(key?: string): <a href="#jsonvalue">JsonValue</a>
</pre>
### getOptional()
<pre>
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined
getOptional(key?: string): <a href="#jsonvalue">JsonValue</a> | undefined
</pre>
### getConfig()
@@ -106,10 +112,12 @@ These types are part of the API declaration, but may not be unique to this API.
<pre>
export type Config = {
has(key: string): boolean;
keys(): string[];
get(key: string): <a href="#jsonvalue">JsonValue</a>;
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined;
get(key?: string): <a href="#jsonvalue">JsonValue</a>;
getOptional(key?: string): <a href="#jsonvalue">JsonValue</a> | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined;
@@ -132,7 +140,7 @@ export type Config = {
</pre>
Defined at
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/config/src/types.ts#L32).
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L32).
Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig),
[getConfigArray](#getconfigarray),
@@ -145,7 +153,7 @@ export type JsonArray = <a href="#jsonvalue">JsonValue</a>[]
</pre>
Defined at
[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/config/src/types.ts#L18).
[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L18).
Referenced by: [JsonValue](#jsonvalue).
@@ -156,7 +164,7 @@ export type JsonObject = { [key in string]?: <a href="#jsonvalue">JsonValue</a>
</pre>
Defined at
[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/config/src/types.ts#L17).
[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L17).
Referenced by: [JsonValue](#jsonvalue).
@@ -173,7 +181,7 @@ export type JsonValue =
</pre>
Defined at
[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/config/src/types.ts#L19).
[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/config/src/types.ts#L19).
Referenced by: [get](#get), [getOptional](#getoptional),
[JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config).
@@ -0,0 +1,24 @@
# DiscoveryApi
The DiscoveryApi type is defined at
[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30).
The following Utility API implements this type:
[discoveryApiRef](./README.md#discovery)
## Members
### getBaseUrl()
Returns the HTTP base backend URL for a given plugin, without a trailing slash.
This method must always be called just before making a request. as opposed to
fetching the URL when constructing an API client. That is to ensure that more
flexible routing patterns can be supported.
For example, asking for the URL for `auth` may return something like
`https://backstage.example.com/api/auth`
<pre>
getBaseUrl(pluginId: string): Promise&lt;string&gt;
</pre>
+6 -6
View File
@@ -1,7 +1,7 @@
# ErrorApi
The ErrorApi type is defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
The following Utility API implements this type: [errorApiRef](./README.md#error)
@@ -41,7 +41,7 @@ type Error = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
Referenced by: [post](#post), [error\$](#error).
@@ -58,7 +58,7 @@ export type ErrorContext = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
Referenced by: [post](#post), [error\$](#error).
@@ -87,7 +87,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [error\$](#error).
@@ -106,7 +106,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -129,6 +129,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,7 +1,7 @@
# FeatureFlagsApi
The FeatureFlagsApi type is defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41).
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41).
The following Utility API implements this type:
[featureFlagsApiRef](./README.md#featureflags)
+2 -2
View File
@@ -1,7 +1,7 @@
# IdentityApi
The IdentityApi type is defined at
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
The following Utility API implements this type:
[identityApiRef](./README.md#identity)
@@ -76,6 +76,6 @@ export type ProfileInfo = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L172).
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L172).
Referenced by: [getProfile](#getprofile).
+5 -3
View File
@@ -1,7 +1,7 @@
# OAuthApi
The OAuthApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L67).
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L67).
The following Utility APIs implement this type:
@@ -11,6 +11,8 @@ The following Utility APIs implement this type:
- [googleAuthApiRef](./README.md#googleauth)
- [microsoftAuthApiRef](./README.md#microsoftauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
@@ -88,7 +90,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getAccessToken](#getaccesstoken).
@@ -114,6 +116,6 @@ export type OAuthScope = string | string[]
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L38).
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L38).
Referenced by: [getAccessToken](#getaccesstoken).
@@ -1,7 +1,7 @@
# OAuthRequestApi
The OAuthRequestApi type is defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
The following Utility API implements this type:
[oauthRequestApiRef](./README.md#oauthrequest)
@@ -72,7 +72,7 @@ export type AuthProvider = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
Referenced by: [AuthRequesterOptions](#authrequesteroptions),
[PendingAuthRequest](#pendingauthrequest).
@@ -96,7 +96,7 @@ export type AuthRequester&lt;AuthResponse&gt; = (
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
Referenced by: [createAuthRequester](#createauthrequester).
@@ -121,7 +121,7 @@ export type AuthRequesterOptions&lt;AuthResponse&gt; = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
Referenced by: [createAuthRequester](#createauthrequester).
@@ -150,7 +150,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [authRequest\$](#authrequest).
@@ -169,7 +169,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -204,7 +204,7 @@ export type PendingAuthRequest = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
Referenced by: [authRequest\$](#authrequest).
@@ -227,6 +227,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,12 +1,16 @@
# OpenIdConnectApi
The OpenIdConnectApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L104).
[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L104).
The following Utility APIs implement this type:
- [auth0AuthApiRef](./README.md#auth0auth)
- [googleAuthApiRef](./README.md#googleauth)
- [microsoftAuthApiRef](./README.md#microsoftauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
@@ -70,6 +74,6 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getIdToken](#getidtoken).
@@ -1,16 +1,20 @@
# ProfileInfoApi
The ProfileInfoApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L127).
[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L127).
The following Utility APIs implement this type:
- [auth0AuthApiRef](./README.md#auth0auth)
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [microsoftAuthApiRef](./README.md#microsoftauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
@@ -61,7 +65,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getProfile](#getprofile).
@@ -89,6 +93,6 @@ export type ProfileInfo = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L172).
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L172).
Referenced by: [getProfile](#getprofile).
+61 -30
View File
@@ -1,59 +1,77 @@
---
id: README
title: Utility API References
---
# Backstage Core Utility APIs
The following is a list of all Utility APIs defined by `@backstage/core`. They
are available to use by plugins and components, and can be accessed using the
`useApi` hook, also provided by `@backstage/core`. For more information, see
https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.
## alert
### alert
Used to report alerts and forward them to the app
Implemented type: [AlertApi](./AlertApi.md)
ApiRef:
[alertApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
[alertApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
## appTheme
### appTheme
API Used to configure the app theme, and enumerate options
Implemented type: [AppThemeApi](./AppThemeApi.md)
ApiRef:
[appThemeApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74)
[appThemeApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74)
## config
### auth0Auth
Provides authentication towards Auth0 APIs
Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[auth0AuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L273)
### config
Used to access runtime configuration
Implemented type: [Config](./Config.md)
ApiRef:
[configApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ConfigApi.ts#L22)
[configApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ConfigApi.ts#L22)
## error
### discovery
Provides service discovery of backend plugins
Implemented type: [DiscoveryApi](./DiscoveryApi.md)
ApiRef:
[discoveryApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44)
### error
Used to report errors and forward them to the app
Implemented type: [ErrorApi](./ErrorApi.md)
ApiRef:
[errorApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
[errorApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
## featureFlags
### featureFlags
Used to toggle functionality in features across Backstage
Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md)
ApiRef:
[featureFlagsApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58)
[featureFlagsApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58)
## githubAuth
### githubAuth
Provides authentication towards GitHub APIs
@@ -63,9 +81,9 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[githubAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L230)
[githubAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L230)
## gitlabAuth
### gitlabAuth
Provides authentication towards GitLab APIs
@@ -75,9 +93,9 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L260)
[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L260)
## googleAuth
### googleAuth
Provides authentication towards Google APIs and identities
@@ -88,18 +106,31 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[googleAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L213)
[googleAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L213)
## identity
### identity
Provides access to the identity of the signed in user
Implemented type: [IdentityApi](./IdentityApi.md)
ApiRef:
[identityApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/IdentityApi.ts#L54)
[identityApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/IdentityApi.ts#L54)
## oauth2
### microsoftAuth
Provides authentication towards Microsoft APIs and identities
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[microsoftAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L287)
### oauth2
Example of how to use oauth2 custom provider
@@ -108,18 +139,18 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md)
ApiRef:
[oauth2ApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L270)
[oauth2ApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L301)
## oauthRequest
### oauthRequest
An API for implementing unified OAuth flows in Backstage
Implemented type: [OAuthRequestApi](./OAuthRequestApi.md)
ApiRef:
[oauthRequestApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
[oauthRequestApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
## oktaAuth
### oktaAuth
Provides authentication towards Okta APIs
@@ -130,13 +161,13 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[oktaAuthApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L243)
[oktaAuthApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L243)
## storage
### storage
Provides the ability to store data which is unique to the user
Implemented type: [StorageApi](./StorageApi.md)
ApiRef:
[storageApiRef](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
[storageApiRef](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
@@ -1,16 +1,20 @@
# SessionStateApi
The SessionStateApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L201).
[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L201).
The following Utility APIs implement this type:
- [auth0AuthApiRef](./README.md#auth0auth)
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [microsoftAuthApiRef](./README.md#microsoftauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
@@ -52,7 +56,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [sessionState\$](#sessionstate).
@@ -71,7 +75,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -87,7 +91,7 @@ export enum SessionState {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/auth.ts#L192).
[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/auth.ts#L192).
Referenced by: [sessionState\$](#sessionstate).
@@ -110,6 +114,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+6 -6
View File
@@ -1,7 +1,7 @@
# StorageApi
The StorageApi type is defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
The following Utility API implements this type:
[storageApiRef](./README.md#storage)
@@ -79,7 +79,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L53).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
@@ -98,7 +98,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -144,7 +144,7 @@ export interface StorageApi {
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
Referenced by: [forBucket](#forbucket).
@@ -158,7 +158,7 @@ export type StorageValueChange&lt;T = any&gt; = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
@@ -181,6 +181,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/f8780ff32509d0326bc513791ea60846d7614b34/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/82d329555c16af46db9b4e5cd2f44a3cc006a52e/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -34,7 +34,7 @@ You get to take full advantage of a platform that we at Spotify have been using
Just run the backstage-cli:
```bash
npx @backstage/cli create-app
npx @backstage/create-app
```
Name your app, and we will create everything you need:
@@ -50,7 +50,7 @@ yarn start
And you are good to go! 👍
Read the full documentation on how to [create an app](/docs/getting-started/create-an-app.md) on GitHub.
Read the full documentation on how to [create an app](/docs/getting-started/create-an-app) on GitHub.
## What do I get? (Let's get technical...)
@@ -0,0 +1,120 @@
---
title: Announcing TechDocs: Spotifys docs-like-code plugin for Backstage
author: Gary Niemen
authorURL: https://github.com/garyniemen
---
Since we [open sourced Backstage](https://backstage.io/blog/2020/03/16/announcing-backstage), one of the most requested features has been for a technical documentation plugin. Well, good news. The first open source version of TechDocs is here. Now lets start collaborating and making it better, together.
<iframe width="780" height="440" src="https://www.youtube.com/embed/mOLCgdPw1iA" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
<!--truncate-->
Internally, we call it TechDocs. Its the most used plugin at Spotify by far — accounting for about 20% of our Backstage traffic (even though it is just one of 130+ plugins). Its popularity is evidence of something simple: We made documentation so easy to create, find, and use — people actually use it.
We are quite sure the main reason for the success of TechDocs is our docs-like-code approach — engineers write their technical documentation in Markdown files that live together with the code. During CI, a documentation site is created using MkDocs, and all sites are rendered centrally in a Backstage plugin. On top of the static documentation, we incorporate additional metadata about the documentation site — such as owner, open GitHub Issues, Slack support channel, and Stack Overflow Enterprise tags.
![available-templates](assets/announcing-techdocs/docs-in-backstage.png)
But this is just one way to do it. Today were most excited for what the open version of TechDocs can become.
## Okay, lets start collaborating
If you go to [GitHub](https://github.com/spotify/backstage/tree/master/plugins) now, youll find everything you need to start collaborating with us to build out the docs-like-code Backstage plugin — well call it TechDocs in the open as well.
Youll find the code in [techdocs](https://github.com/spotify/backstage/tree/master/plugins/techdocs) (frontend) and [techdocs-backend](https://github.com/spotify/backstage/tree/master/plugins/techdocs-backend). (There are also two separate packages [techdocs-cli](https://github.com/spotify/backstage/tree/master/packages/techdocs-cli) and [techdocs-container](https://github.com/spotify/backstage/tree/master/packages/techdocs-container).)
Youll find issues to work on in the [issues queue](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3A%22docs-like-code%22+label%3A%22help+wanted%22), typically starting with TechDocs: and labeled with docs-like-code, some labeled good first issue. Feel free to add your own issues, of course.
![available-templates](assets/announcing-techdocs/github-issues.png)
What we have on GitHub so far is a first iteration of TechDocs that you can use end-to-end — in other words, from docs written in Markdown in GitHub to a published site on Backstage.
More specifically, with this first iteration, you can:
- Run TechDocs locally and read documentation.
- Configure your entity (e.g. service, website) so that Backstage builds your documentation and serves it in TechDocs. Documentation is displayed on the Docs tab in the Service Catalog and on its own page.
- Get documentation set up for free in your project when you create a new component out of one of the non-experimental templates (labeled with recommended). If you are looking for a standalone documentation project, use the docs-template.
- Choose your own storage solution for the documentation.
- Define your own API to interface with your documentation solution.
For a full overview, including getting started instructions, check out our [TechDocs Documentation](https://backstage.io/docs/features/techdocs/techdocs-overview).
But before you go there, let me tell you a bit about the TechDocs story — and why we believe TechDocs is such a powerful yet simple solution for great documentation.
## The TechDocs story
Here is the TechDocs story. Its not an uncommon one (we have learned from many other companies).
About a year and a half ago, we conducted a company-wide productivity survey. The third largest problem according to all our engineers? Not being able to find the technical information they needed to do their work. And its not surprising. There was no standard way to produce and consume technical documentation, so teams were going their own way — using Confluence, Google Docs, README files, custom built websites, GitHub Pages, and so on and on. And those searching for information were left to hunt for it in all those different places until they found what they were looking for (if they ever did). Worse, if you did happen to find the documentation that you needed, there was no way to know whether the information was up-to-date or correct. In other words, there was no way to know whether you could trust what you found. We did have technical writers at the company, but they were mostly scattered across the company solving documentation problems within their own particular domain.
So this is the fertile soil on which TechDocs was built.
After a Hack Week implementation attracted interest from high up in the company, we formed a cross-functional team made up of technical writers and engineers with the mission to solve internal technical documentation at Spotify. And we started to build TechDocs. We went for a docs-like-code approach, fiercely optimizing for engineers and engineering workflows. We also went for an opinionated approach, telling everybody: This is the standard way to do technical documentation at Spotify. The sense was that engineers appreciated a documentation solution that was in line with their workflow and, after all the frustration of multiple tools, were relieved to be told “this is the way to do it”.
For more information about this journey, take a look at my 20-minute talk from DevRelCon London from last December: [The Heros Journey: How we are solving internal technical documentation at Spotify](https://www.linkedin.com/posts/garyniemen_how-we-are-solving-internal-technical-documentation-activity-6646078605594030080-4L31).
## Key problem areas that we are solving
We have come a long way, fast — both in our implementation and in our thinking. Here are some of the key problem areas that we are addressing. Note that they are in various stages of implementation, and we wont be able to release everything within our minimum plugin. In fact, see this as an appetite taster. What we hope is that we can build together.
### Stuck to unstuck
Very early on, we decided that the main problem we were trying to solve was to help engineers (when using technical documentation) go from stuck to unstuck, and fast. This became our guiding principle. Is what we are building helping engineers get unstuck faster? From this, it follows that we need to promote quality documentation on the one hand, and provide a high level of discoverability on the other. One without the other is not going to cut it.
### Feedback loops
What we want to build is a thriving community of technical documentation creators, contributors, and readers. We want this because, we believe, this is the way to drive up the quality of the documentation. More readers, more feedback, more doc updates. And driving up the quality of the corpus of technical documentation leads to trust which in turn leads to more engagement and, hence, more of a thriving community.
To get this working, we recognised that we need to remove friction from the system — we need to build in efficient feedback loops. For example, help engineers get their doc site up by providing documentation improvement hints and build information as close as possible to where they are already working. And for readers, make it easy to give feedback. And then for doc site owners, ensure that they are notified when there is feedback and incentivised to make the fix.
![available-templates](assets/announcing-techdocs/feedback-loop1.png)
![available-templates](assets/announcing-techdocs/feedback-loop2.png)
### Trust
How do I know whether to trust this piece of documentation? This is a question we want to be able to answer for those using technical documentation in Backstage. Its not an easy nut to crack. It is almost, one could say, the hard problem of technical documentation. For example, some might say last updated is a key factor. But what about stable, good quality documentation that has no need to be updated? What about page views? Yes, this is a good sign that the documentation is being found and viewed, but it doesnt say anything about whether the documentation can be trusted. How about a button: Did this documentation help? This is good, but will people use it? Will we get enough data to show trust? We have lofty ambitions of one day providing a trust score on the doc site informed by a super-intelligent algorithm. But we are not there yet. For now, we have landed on surfacing when the documentation was last updated, top five contributors, the support channel, owning team, and number of open GitHub Issues. But going forward we are definitely up for solving the hard problem. We think theres much more work to be done here and look forward to seeing ideas from the community.
### Discoverability and search
How to find stuff? That is another big question. As mentioned above, its all well and good having quality documentation, but its no use whatsoever if you cant find it. If you know what you are looking for, then you can use a search engine. If you dont know what you are looking for, then you are going to need more — like a well designed information architecture, a user friendly browse experience, and even intelligent suggestions based on your role and what you have searched for previously.
In this problem area, we made use of Elasticsearch, the open source search engine that was already being used in Backstage, to implement documentation search across all documentation sites and per documentation site. In terms of discoverability, we implemented a documentation home page in Backstage that surfaces Spotifys most important documents and uses metrics to list the companys most used doc sites as well as the documentation equivalent of a “your daily mix” playlist.
![available-templates](assets/announcing-techdocs/discover1.png)
![available-templates](assets/announcing-techdocs/discover2.png)
There is much more to do in the area of discoverability and search.
### Use case variations
The standard use case for TechDocs is: One component in Backstage equals one GitHub repository, equals one doc site. This use case comes in two flavours: the repository is a code repository with docs or a docs-only repository. Then, to meet the needs of one large part of the Spotify engineering organisation that uses monorepos (multi-component repositories), we added a third use case. We built an MkDocs plugin that enabled doc site creators to include documentation from doc folders in other parts of the repository. So this use case is: One main component in Backstage equals a monorepo with distributed documentation, equals one doc site.
These three use cases satisfy most of the needs, but we have had plenty of requests for additional use cases, for example, the ability to create multiple doc sites from a multi-component repository and the ability to create one doc site from documentation in multiple repositories.
### Metrics
There are many good arguments for standardizing the way that technical documentation is produced and consumed. One of them is metrics. If we have one way of producing technical documentation (in our case, GitHub Enterprise) and one place where it shows up (in our case, Backstage), we are in a strong position to build up metrics that help all the various stakeholders — for example, us building TechDocs, teams creating documentation sites, and engineers trying to get unstuck. Just imagine how much harder this would be if technical documentation was produced and consumed in a plethora of places, such as Confluence, Google Docs, README files, custom web sites, and GitHub Pages.
One thing we have recently completed is a Manage page in Backstage for doc site owners. Here teams can see all the documentation that they own, the number of GitHub Issues per doc site or page, and last updated. We have also built a large dashboard using the open source analytics software Redash to inform our own product development process.
![available-templates](assets/announcing-techdocs/metrics.png)
Again, there is a lot more that can be done in the area of metrics. Did I mention the trust score?
### Code-like-docs
Code-like-docs, what? Okay, its just my little play on words. This is what I mean. One request that we keep getting is to be able to have code in the documentation fetched from and in sync with code in GitHub. In this way, you can avoid code in the documentation going stale. MkDocs does have an extension for this — but it has some limitations. For example, the code has to be in the /docs folder with the Markdown files. We are working on developing a wider and more flexible solution.
### Golden Paths
At Spotify, we have the concept of [Golden Paths](https://engineering.atspotify.com/2020/08/17/how-we-use-golden-paths-to-solve-fragmentation-in-our-software-ecosystem/) — one for each engineering discipline. My favourite definition of Golden Path is that it is the “opinionated and supported path”. Each Golden Path has an accompanying Golden Path tutorial that walks you through the opinionated and supported path.
The Golden Path tutorials are Spotifys most used and important documents and have shown themselves to be the most challenging to manage within a docs-like-code environment. One reason for this is that they are long, divided into many parts, and ownership is typically spread among many different teams. We have had to make use of GitHub codeowners to handle ownership and had to create datasets and data pipelines to be able to attach GitHub Issues to the specific parts or files that a team owns. Another challenge of the Golden Path tutorials is that parts are often dependent on other parts. We are just starting to look into how we can solve these dependency challenges in order to remove friction for engineers writing tutorial documentation.
---
So thats it for now. As you can see, we have come a long way AND there is much more to do. We are looking forward to continuing our docs-like-code journey out in the open with new, enthusiastic technical documentation friends.
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

+3 -5
View File
@@ -3,12 +3,10 @@ title: GraphiQL
author: Spotify
authorUrl: https://github.com/spotify
category: Debugging
description: Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/lighthouse
description: Integrates GraphiQL as a tool to browse GraphQL API endpoints inside Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/graphiql
iconUrl: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1024px-GraphQL_Logo.svg.png
npmPackageName: '@backstage/plugin-graphiql'
tags:
- graphql
- github
- gitlab
- api
- graphiql
+35
View File
@@ -78,6 +78,41 @@ const Background = props => {
</Block.Container>
</Block>
<Block className="stripe-bottom bg-black-grey">
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.TextBox>
<Block.Title id="techdocs-demo">
Make documentation easy
</Block.Title>
<Block.Paragraph>
Documentation! Everyone needs it, no one wants to create it, and
no one can ever find it. Backstage follows a docs like code
approach: you write documentation in Markdown files right
alongside your code. This makes documentation easier to create,
maintain, find and, you know, actually use. This demo video
showcases Spotifys internal version of TechDocs. Learn more about{' '}
<a href="https://backstage.io/blog/2020/09/08/announcing-tech-docs">
TechDocs
</a>
.
</Block.Paragraph>
<Block.LinkButton href={'https://youtu.be/mOLCgdPw1iA'}>
Watch now
</Block.LinkButton>
</Block.TextBox>
<Block.MediaFrame>
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/mOLCgdPw1iA"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</Block.MediaFrame>
</Block.Container>
</Block>
<Block small className="bg-black-grey">
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.TextBox>
+4 -14
View File
@@ -296,9 +296,7 @@ class Index extends React.Component {
src={`${baseUrl}animations/backstage-techdocs-icon-1.gif`}
/>
<Block.Subtitle>
Backstage TechDocs <span>(Coming Soon)</span>
</Block.Subtitle>
<Block.Subtitle>Backstage TechDocs</Block.Subtitle>
<Block.Title small>Docs like code</Block.Title>
</Block.TextBox>
<Breakpoint
@@ -373,19 +371,11 @@ class Index extends React.Component {
</Block>
<ActionBlock className="stripe bg-teal">
<ActionBlock.Title>Subscribe to our newsletter</ActionBlock.Title>
<ActionBlock.Subtitle>
TechDocs is our most used feature at Spotify. Be the first to know
when{' '}
<a href="https://github.com/spotify/backstage/projects/5">
the open source version
</a>{' '}
ships.
</ActionBlock.Subtitle>
<ActionBlock.Title>Learn more about TechDocs</ActionBlock.Title>
<ActionBlock.Link
href={`https://mailchi.mp/spotify/backstage-community`}
href={`https://backstage.io/docs/features/techdocs/techdocs-overview`}
>
Subscribe
Docs
</ActionBlock.Link>
</ActionBlock>
+4 -2
View File
@@ -31,12 +31,13 @@
]
}
],
"Features": [
"Core Features": [
{
"type": "subcategory",
"label": "Software Catalog",
"ids": [
"features/software-catalog/software-catalog-overview",
"features/software-catalog/installation",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/extending-the-model",
@@ -46,9 +47,10 @@
},
{
"type": "subcategory",
"label": "Software creation templates",
"label": "Software Templates",
"ids": [
"features/software-templates/software-templates-index",
"features/software-templates/installation",
"features/software-templates/adding-templates",
"features/software-templates/extending/extending-index",
"features/software-templates/extending/extending-templater",
+1
View File
@@ -34,6 +34,7 @@ nav:
- API: 'features/software-catalog/api.md'
- Software creation templates:
- Overview: 'features/software-templates/index.md'
- Installation: 'features/software-templates/installation.md'
- Adding templates: 'features/software-templates/adding-templates.md'
- Extending the Scaffolder:
- Overview: 'features/software-templates/extending/index.md'
+1
View File
@@ -10,6 +10,7 @@
"start-backend": "yarn workspace example-backend start",
"build": "lerna run build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
"test": "lerna run test --since origin/master -- --coverage",
+1 -1
View File
@@ -27,7 +27,7 @@
"@backstage/plugin-welcome": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-pull-requests": "0.3.0",
+24 -2
View File
@@ -19,6 +19,7 @@ import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
createRouteRef,
} from '@backstage/core';
import React, { FC } from 'react';
import Root from './components/Root';
@@ -27,6 +28,11 @@ import { apis } from './apis';
import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -52,13 +58,29 @@ const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const deprecatedAppRoutes = app.getRoutes();
const catalogRouteRef = createRouteRef({
path: '/catalog',
title: 'Service Catalog',
});
const AppRoutes = () => (
<Routes>
<Navigate key="/" to="/catalog" />
<Route
path="/catalog/*"
path={`${catalogRouteRef.path}/*`}
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Navigate key="/" to="/catalog" />
<Route path="/docs/*" element={<DocsRouter />} />
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
<Route path="/graphiql" element={<GraphiQLRouter />} />
<Route path="/lighthouse/*" element={<LighthouseRouter />} />
<Route
path="/register-component"
element={<RegisterComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
{...deprecatedAppRoutes}
</Routes>
);
+34 -188
View File
@@ -15,65 +15,18 @@
*/
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ConfigApi,
ErrorApiForwarder,
ErrorAlerter,
featureFlagsApiRef,
FeatureFlags,
discoveryApiRef,
UrlPatternDiscovery,
GoogleAuth,
GithubAuth,
OAuth2,
OktaAuth,
GitlabAuth,
Auth0Auth,
MicrosoftAuth,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
oauth2ApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
auth0AuthApiRef,
microsoftAuthApiRef,
storageApiRef,
WebStorage,
createApiFactory,
configApiRef,
} from '@backstage/core';
import {
lighthouseApiRef,
LighthouseRestApi,
} from '@backstage/plugin-lighthouse';
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { gitOpsApiRef, GitOpsRestApi } from '@backstage/plugin-gitops-profiles';
import {
graphQlBrowseApiRef,
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
import {
techdocsStorageApiRef,
TechDocsStorageApi,
} from '@backstage/plugin-techdocs';
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
import { GCPClient, GCPApiRef } from '@backstage/plugin-gcp-projects';
import {
GithubActionsClient,
githubActionsApiRef,
} from '@backstage/plugin-github-actions';
import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins';
import {
TravisCIApi,
@@ -84,143 +37,36 @@ import {
githubPullRequestsApiRef,
} from '@roadiehq/backstage-plugin-github-pull-requests';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
export const apis = [
// TODO(Rugvip): migrate to use /api
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
),
}),
createApiFactory({
api: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
factory: ({ errorApi, githubAuthApi }) =>
GraphQLEndpoints.from([
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com/api/graphql',
}),
GraphQLEndpoints.github({
id: 'github',
title: 'GitHub',
errorApi,
githubAuthApi,
}),
]),
}),
const backendUrl = config.getString('backend.baseUrl');
const techdocsUrl = config.getString('techdocs.storageUrl');
const builder = ApiRegistry.builder();
const discoveryApi = builder.add(
discoveryApiRef,
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
);
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
);
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(GCPApiRef, new GCPClient());
builder.add(
circleCIApiRef,
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
);
builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`));
builder.add(githubActionsApiRef, new GithubActionsClient());
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, LighthouseRestApi.fromConfig(config));
builder.add(travisCIApiRef, new TravisCIApi());
builder.add(githubPullRequestsApiRef, new GithubPullRequestsClient());
const oauthRequestApi = builder.add(
oauthRequestApiRef,
new OAuthRequestManager(),
);
builder.add(
googleAuthApiRef,
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
microsoftAuthApiRef,
MicrosoftAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
oktaAuthApiRef,
OktaAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
auth0AuthApiRef,
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
oauth2ApiRef,
OAuth2.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
techRadarApiRef,
new TechRadar({
width: 1500,
height: 800,
}),
);
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
builder.add(
graphQlBrowseApiRef,
GraphQLEndpoints.from([
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com/api/graphql',
}),
GraphQLEndpoints.github({
id: 'github',
title: 'GitHub',
errorApi,
githubAuthApi,
}),
]),
);
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
builder.add(
techdocsStorageApiRef,
new TechDocsStorageApi({
apiOrigin: techdocsUrl,
}),
);
return builder.build();
};
// TODO: move to plugins
createApiFactory(travisCIApiRef, new TravisCIApi()),
createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()),
];
@@ -18,9 +18,7 @@ import React, { FC, useContext } from 'react';
import PropTypes from 'prop-types';
import { Link, makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension';
import BuildIcon from '@material-ui/icons/BuildRounded';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
@@ -91,7 +89,6 @@ const Root: FC<{}> = ({ children }) => (
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
<SidebarItem icon={ExploreIcon} to="explore" text="Explore" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
@@ -99,7 +96,6 @@ const Root: FC<{}> = ({ children }) => (
<SidebarDivider />
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
<SidebarItem icon={BuildIcon} to="circleci" text="CircleCI" />
<SidebarItem
icon={graphiQLRouteRef.icon!}
to={graphiQLRouteRef.path}
@@ -13,9 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Router as GitHubActionsRouter,
isPluginApplicableToEntity as isGitHubActionsAvailable,
} from '@backstage/plugin-github-actions';
import {
Router as CircleCIRouter,
isPluginApplicableToEntity as isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
import { Router as GitHubActionsRouter } from '@backstage/plugin-github-actions';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import React from 'react';
import {
AboutCard,
@@ -24,6 +32,25 @@ import {
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
import { WarningPanel } from '@backstage/core';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
switch (true) {
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
default:
return (
<WarningPanel title="CI/CD switcher:">
No CI/CD is available for this entity. Check corresponding
annotations!
</WarningPanel>
);
}
};
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
@@ -43,7 +70,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
@@ -55,6 +82,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
title="API"
element={<ApiDocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -68,16 +100,20 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
title="Sentry"
element={<SentryRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
@@ -85,6 +121,11 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
+7 -2
View File
@@ -2,10 +2,15 @@ FROM node:12
WORKDIR /usr/src/app
# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
# The skeleton contains the package.json of each package in the monorepo,
# and along with yarn.lock and the root package.json, that's enough to run yarn install.
ADD yarn.lock package.json skeleton.tar ./
RUN yarn install --frozen-lockfile --production
# This will copy the contents of the dist-workspace when running the build-image command.
# Do not use this Dockerfile outside of that command, as it will copy in the source code instead.
COPY . .
RUN yarn install --frozen-lockfile --production
CMD ["node", "packages/backend"]
+1
View File
@@ -25,6 +25,7 @@
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": false,
"skipLibCheck": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
@@ -41,6 +41,7 @@ export default async (cmd: Command) => {
...appConfigs,
{ src: paths.resolveTarget('Dockerfile'), dest: 'Dockerfile' },
],
skeleton: 'skeleton.tar',
});
console.log(`Dist workspace ready at ${tempDistWorkspace}`);
+5 -1
View File
@@ -42,7 +42,11 @@ export async function serveBundle(options: ServeOptions) {
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
historyApiFallback: true,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
clientLogLevel: 'warning',
stats: 'errors-warnings',
https: url.protocol === 'https:',
+30 -2
View File
@@ -15,10 +15,14 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath, relative as relativePath } from 'path';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { paths } from '../paths';
import { run } from '../run';
import tar from 'tar';
import tar, { CreateOptions } from 'tar';
import { tmpdir } from 'os';
type LernaPackage = {
@@ -53,6 +57,12 @@ type Options = {
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar';
};
/**
@@ -89,6 +99,24 @@ export async function createDistWorkspace(
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.location);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
@@ -24,7 +24,7 @@
"dependencies": {
"@backstage/core": "^{{backstageVersion}}",
"@backstage/theme": "^{{backstageVersion}}",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
+1 -1
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
"prop-types": "^15.7.2",
@@ -0,0 +1,70 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
import { createApiRef } from './ApiRef';
const aRef = createApiRef<number>({ id: 'a', description: '' });
const aFactory1 = { api: aRef, deps: {}, factory: () => 1 };
const aFactory2 = { api: aRef, deps: {}, factory: () => 2 };
const bRef = createApiRef<string>({ id: 'b', description: '' });
const bFactory = { api: bRef, deps: {}, factory: () => 'x' };
const cRef = createApiRef<string>({ id: 'c', description: '' });
const cFactory = { api: cRef, deps: {}, factory: () => 'y' };
describe('ApiFactoryRegistry', () => {
it('should be empty when created', () => {
const registry = new ApiFactoryRegistry();
expect(registry.getAllApis()).toEqual(new Set());
});
it('should register a factory', () => {
const registry = new ApiFactoryRegistry();
expect(registry.register('default', aFactory1)).toBe(true);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.getAllApis()).toEqual(new Set([aRef]));
});
it('should prioritize factories based on scope', () => {
const registry = new ApiFactoryRegistry();
expect(registry.register('default', aFactory1)).toBe(true);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.register('default', aFactory2)).toBe(false);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.register('app', aFactory2)).toBe(true);
expect(registry.get(aRef)).toBe(aFactory2);
expect(registry.register('default', aFactory1)).toBe(false);
expect(registry.get(aRef)).toBe(aFactory2);
expect(registry.register('static', aFactory1)).toBe(true);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.register('static', aFactory2)).toBe(false);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.register('app', aFactory2)).toBe(false);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.getAllApis()).toEqual(new Set([aRef]));
});
it('should register multiple factories without conflict', () => {
const registry = new ApiFactoryRegistry();
expect(registry.register('static', aFactory1)).toBe(true);
expect(registry.register('default', bFactory)).toBe(true);
expect(registry.register('app', cFactory)).toBe(true);
expect(registry.get(aRef)).toBe(aFactory1);
expect(registry.get(bRef)).toBe(bFactory);
expect(registry.get(cRef)).toBe(cFactory);
expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef]));
});
});
@@ -0,0 +1,83 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiFactoryHolder,
ApiFactory,
AnyApiRef,
AnyApiFactory,
} from './types';
import { ApiRef } from './ApiRef';
type ApiFactoryScope =
| 'default' // Default factories registered by core and plugins
| 'app' // Factories registered in the app, overriding default ones
| 'static'; // APIs that can't be overridden, e.g. config
enum ScopePriority {
default = 10,
app = 50,
static = 100,
}
type FactoryTuple = {
priority: number;
factory: AnyApiFactory;
};
/**
* ApiFactoryRegistry is an ApiFactoryHolder implementation that enables
* registration of API Factories with different scope.
*
* Each scope has an assigned priority, where factories registered with
* higher priority scopes override ones with lower priority.
*/
export class ApiFactoryRegistry implements ApiFactoryHolder {
private readonly factories = new Map<AnyApiRef, FactoryTuple>();
/**
* Register a new API factory. Returns true if the factory was added
* to the registry.
*
* A factory will not be added to the registry if there is already
* an existing factory with the same or higher priority.
*/
register<Api, Deps extends { [name in string]: unknown }>(
scope: ApiFactoryScope,
factory: ApiFactory<Api, Deps>,
) {
const priority = ScopePriority[scope];
const existing = this.factories.get(factory.api);
if (existing && existing.priority >= priority) {
return false;
}
this.factories.set(factory.api, { priority, factory });
return true;
}
get<T>(api: ApiRef<T>): ApiFactory<T, { [x: string]: unknown }> | undefined {
const tuple = this.factories.get(api);
if (!tuple) {
return undefined;
}
return tuple.factory as ApiFactory<T, { [x: string]: unknown }>;
}
getAllApis(): Set<AnyApiRef> {
return new Set(this.factories.keys());
}
}
@@ -0,0 +1,177 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiResolver } from './ApiResolver';
import { createApiRef } from './ApiRef';
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
const aRef = createApiRef<number>({ id: 'a', description: '' });
const bRef = createApiRef<string>({ id: 'b', description: '' });
const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' });
function createRegistry() {
const registry = new ApiFactoryRegistry();
registry.register('default', {
api: aRef,
deps: {},
factory: () => 1,
});
registry.register('default', {
api: bRef,
deps: {},
factory: () => 'b',
});
registry.register('default', {
api: cRef,
deps: { b: bRef },
factory: ({ b }) => ({ x: 'x', b }),
});
return registry;
}
function createLongCyclicRegistry() {
const registry = new ApiFactoryRegistry();
registry.register('default', {
api: aRef,
deps: { b: bRef },
factory: () => 1,
});
registry.register('default', {
api: bRef,
deps: { c: cRef },
factory: () => 'b',
});
registry.register('default', {
api: cRef,
deps: { a: aRef },
factory: () => ({ x: 'x' }),
});
return registry;
}
function createShortCyclicRegistry() {
const registry = new ApiFactoryRegistry();
registry.register('default', {
api: aRef,
deps: { a: aRef },
factory: () => 1,
});
registry.register('default', {
api: bRef,
deps: { c: cRef },
factory: () => 'b',
});
registry.register('default', {
api: cRef,
deps: { b: bRef },
factory: () => ({ x: 'x' }),
});
return registry;
}
describe('ApiResolver', () => {
it('should be created empty', () => {
const resolver = new ApiResolver(new ApiFactoryRegistry());
expect(resolver.get(aRef)).toBe(undefined);
expect(resolver.get(bRef)).toBe(undefined);
expect(resolver.get(cRef)).toBe(undefined);
});
it('should instantiate APIs', () => {
const resolver = new ApiResolver(createRegistry());
expect(resolver.get(aRef)).toBe(1);
expect(resolver.get(bRef)).toBe('b');
expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' });
expect(resolver.get(cRef)).toBe(resolver.get(cRef));
});
it('should detect long dependency cycles', () => {
const resolver = new ApiResolver(createLongCyclicRegistry());
expect(() => resolver.get(aRef)).toThrow(
'Circular dependency of api factory for apiRef{a}',
);
// Second call for same ref should still throw
expect(() => resolver.get(aRef)).toThrow(
'Circular dependency of api factory for apiRef{a}',
);
expect(() => resolver.get(bRef)).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => resolver.get(cRef)).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
});
it('should detect short dependency cycles', () => {
const resolver = new ApiResolver(createShortCyclicRegistry());
expect(() => resolver.get(aRef)).toThrow(
'Circular dependency of api factory for apiRef{a}',
);
expect(() => resolver.get(bRef)).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => resolver.get(cRef)).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
});
it('should validate a factory holder', () => {
expect(() => {
ApiResolver.validateFactories(createRegistry(), [aRef, bRef, cRef]);
}).not.toThrow();
});
it('should find dependency cycles with validation', () => {
const short = createShortCyclicRegistry();
expect(() =>
ApiResolver.validateFactories(short, short.getAllApis()),
).toThrow('Circular dependency of api factory for apiRef{a}');
expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => ApiResolver.validateFactories(short, [cRef])).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
const long = createLongCyclicRegistry();
expect(() =>
ApiResolver.validateFactories(long, long.getAllApis()),
).toThrow('Circular dependency of api factory for apiRef{a}');
expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
});
it('should only call factory func once', () => {
const registry = new ApiFactoryRegistry();
const factory = jest.fn().mockReturnValue(2);
registry.register('default', {
api: aRef,
deps: {},
factory,
});
const resolver = new ApiResolver(registry);
expect(factory).toHaveBeenCalledTimes(0);
expect(resolver.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
expect(resolver.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
});
});
@@ -15,38 +15,54 @@
*/
import { ApiRef } from './ApiRef';
import { TypesToApiRefs, AnyApiRef, ApiHolder, ApiFactory } from './types';
import {
ApiHolder,
ApiFactoryHolder,
AnyApiRef,
TypesToApiRefs,
} from './types';
export class ApiTestRegistry implements ApiHolder {
export class ApiResolver implements ApiHolder {
private readonly apis = new Map<AnyApiRef, unknown>();
private factories = new Map<
AnyApiRef,
ApiFactory<unknown, unknown, unknown>
>();
private savedFactories = new Map<
AnyApiRef,
ApiFactory<unknown, unknown, unknown>
>();
/**
* Validate factories by making sure that each of the apis can be created
* without hitting any circular dependencies.
*/
static validateFactories(
factories: ApiFactoryHolder,
apis: Iterable<AnyApiRef>,
) {
for (const api of apis) {
const heap = [api];
const allDeps = new Set<AnyApiRef>();
while (heap.length) {
const apiRef = heap.shift()!;
const factory = factories.get(apiRef);
if (!factory) {
continue;
}
for (const dep of Object.values(factory.deps)) {
if (dep === api) {
throw new Error(`Circular dependency of api factory for ${api}`);
}
if (!allDeps.has(dep)) {
allDeps.add(dep);
heap.push(dep);
}
}
}
}
}
constructor(private readonly factories: ApiFactoryHolder) {}
get<T>(ref: ApiRef<T>): T | undefined {
return this.load(ref);
}
register<A, I, D>(factory: ApiFactory<A, I, D>): ApiTestRegistry {
this.factories.set(factory.implements, factory);
return this;
}
reset() {
this.factories = this.savedFactories;
this.apis.clear();
}
save(): ApiTestRegistry {
this.savedFactories = new Map(this.factories);
return this;
}
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
const impl = this.apis.get(ref);
if (impl) {
@@ -58,16 +74,11 @@ export class ApiTestRegistry implements ApiHolder {
return undefined;
}
if (loading.includes(factory.implements)) {
throw new Error(
`Circular dependency of api factory for ${factory.implements}`,
);
if (loading.includes(factory.api)) {
throw new Error(`Circular dependency of api factory for ${factory.api}`);
}
const deps = this.loadDeps(ref, factory.deps, [
...loading,
factory.implements,
]);
const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]);
const api = factory.factory(deps);
this.apis.set(ref, api);
return api as T;
@@ -1,154 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiTestRegistry } from './ApiTestRegistry';
import { createApiRef } from './ApiRef';
describe('ApiTestRegistry', () => {
const aRef = createApiRef<number>({ id: 'a', description: '' });
const bRef = createApiRef<string>({ id: 'b', description: '' });
const cRef = createApiRef<string>({ id: 'c', description: '' });
it('should be created', () => {
const registry = new ApiTestRegistry();
expect(registry.get(aRef)).toBe(undefined);
expect(registry.get(bRef)).toBe(undefined);
expect(registry.get(cRef)).toBe(undefined);
});
it('should register a factory', () => {
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
expect(registry.get(aRef)).toBe(3);
expect(registry.get(bRef)).toBe(undefined);
expect(registry.get(cRef)).toBe(undefined);
});
it('should remove factories when resetting', () => {
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
expect(registry.get(aRef)).toBe(3);
registry.reset();
expect(registry.get(aRef)).toBe(undefined);
});
it('should keep saved factories when resetting', () => {
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
registry.save();
registry.register({ implements: bRef, deps: {}, factory: () => 'x' });
expect(registry.get(aRef)).toBe(3);
expect(registry.get(bRef)).toBe('x');
registry.reset();
expect(registry.get(aRef)).toBe(3);
expect(registry.get(bRef)).toBe(undefined);
});
it('should register factories with dependencies', () => {
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
const cDeps = Object.create(
{ c: cRef },
{ a: { enumerable: true, value: aRef } },
);
cDeps.b = bRef;
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
registry.register({
implements: bRef,
deps: { dep: aRef },
factory: ({ dep }) => `hello ${dep}`,
});
registry.register({
implements: cRef,
deps: cDeps,
factory: ({ a, b }) => b.repeat(a),
});
expect(registry.get(aRef)).toBe(3);
expect(registry.get(bRef)).toBe('hello 3');
expect(registry.get(cRef)).toBe('hello 3hello 3hello 3');
});
it('should not allow cyclic dependencies', () => {
const registry = new ApiTestRegistry();
registry.register({
implements: aRef,
deps: { b: bRef },
factory: () => 1,
});
registry.register({
implements: bRef,
deps: { c: cRef },
factory: () => 'b',
});
registry.register({
implements: cRef,
deps: { a: aRef },
factory: () => 'c',
});
expect(() => registry.get(aRef)).toThrow(
'Circular dependency of api factory for apiRef{a}',
);
expect(() => registry.get(bRef)).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => registry.get(cRef)).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
});
it('should throw error if dependency is not available', () => {
const registry = new ApiTestRegistry();
registry.register({
implements: aRef,
deps: { b: bRef },
factory: () => 1,
});
expect(() => registry.get(aRef)).toThrow(
'No API factory available for dependency apiRef{b} of dependent apiRef{a}',
);
expect(registry.get(bRef)).toBe(undefined);
expect(registry.get(cRef)).toBe(undefined);
});
it('should only call factory func once', () => {
const registry = new ApiTestRegistry();
const factory = jest.fn().mockReturnValue(2);
registry.register({ implements: aRef, deps: {}, factory });
expect(factory).toHaveBeenCalledTimes(0);
expect(registry.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
expect(registry.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
});
it('should call factory again after reset', () => {
const registry = new ApiTestRegistry();
const factory = jest.fn().mockReturnValue(2);
registry.register({ implements: aRef, deps: {}, factory });
registry.save();
expect(factory).toHaveBeenCalledTimes(0);
expect(registry.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
expect(registry.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(1);
registry.reset();
expect(registry.get(aRef)).toBe(2);
expect(factory).toHaveBeenCalledTimes(2);
});
});
@@ -299,7 +299,11 @@ export const microsoftAuthApiRef = createApiRef<
* Provides authentication for custom identity providers.
*/
export const oauth2ApiRef = createApiRef<
OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
SessionStateApi &
BackstageIdentityApi
>({
id: 'core.auth.oauth2',
description: 'Example of how to use oauth2 custom provider',
+25 -4
View File
@@ -14,15 +14,36 @@
* limitations under the License.
*/
import { ApiFactory } from './types';
import { ApiFactory, TypesToApiRefs } from './types';
import { ApiRef } from './ApiRef';
/**
* Used to infer types for a standalone ApiFactory that isn't immediately passed
* to another function.
* This function doesn't actually do anything, it's only used to infer types.
*/
export function createApiFactory<Api, Impl, Deps>(
factory: ApiFactory<Api, Impl, Deps>,
): ApiFactory<Api, Impl, Deps> {
export function createApiFactory<
Api,
Impl extends Api,
Deps extends { [name in string]: unknown }
>(factory: ApiFactory<Api, Deps>): ApiFactory<Api, Deps>;
export function createApiFactory<Api>(
api: ApiRef<Api>,
instance: Api,
): ApiFactory<Api, {}>;
export function createApiFactory<
Api,
Deps extends { [name in string]: unknown }
>(
factory: ApiFactory<Api, Deps> | ApiRef<Api>,
instance?: Api,
): ApiFactory<Api, Deps> {
if ('id' in factory) {
return {
api: factory,
deps: {} as TypesToApiRefs<Deps>,
factory: () => instance!,
};
}
return factory;
}
@@ -42,6 +42,7 @@ type CreateOptions = {
environment?: string;
provider?: AuthProvider & { id: string };
defaultScopes?: string[];
};
export type OAuth2Response = {
@@ -61,8 +62,6 @@ const DEFAULT_PROVIDER = {
icon: OAuth2Icon,
};
const SCOPE_PREFIX = '';
class OAuth2
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
static create({
@@ -70,6 +69,7 @@ class OAuth2
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
defaultScopes = [],
}: CreateOptions) {
const connector = new DefaultAuthConnector({
discoveryApi,
@@ -93,11 +93,7 @@ class OAuth2
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
]),
defaultScopes: new Set(defaultScopes),
sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,
sessionShouldRefresh: (session: OAuth2Session) => {
const expiresInSec =
-1
View File
@@ -16,7 +16,6 @@
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export { ApiTestRegistry } from './ApiTestRegistry';
export * from './ApiRef';
export * from './types';
export * from './helpers';
+13 -5
View File
@@ -16,13 +16,13 @@
import { ApiRef } from './ApiRef';
export type AnyApiRef = ApiRef<any>;
export type AnyApiRef = ApiRef<unknown>;
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
export type TypesToApiRefs<T> = { [key in keyof T]: ApiRef<T[key]> };
export type ApiRefsToTypes<T extends { [key in any]: ApiRef<any> }> = {
export type ApiRefsToTypes<T extends { [key in string]: ApiRef<unknown> }> = {
[key in keyof T]: ApiRefType<T[key]>;
};
@@ -30,8 +30,16 @@ export type ApiHolder = {
get<T>(api: ApiRef<T>): T | undefined;
};
export type ApiFactory<Api, Impl, Deps> = {
implements: ApiRef<Api>;
export type ApiFactory<Api, Deps extends { [name in string]: unknown }> = {
api: ApiRef<Api>;
deps: TypesToApiRefs<Deps>;
factory(deps: Deps): Impl extends Api ? Impl : never;
factory(deps: Deps): Api;
};
export type AnyApiFactory = ApiFactory<unknown, { [key in string]: unknown }>;
export type ApiFactoryHolder = {
get<T>(
api: ApiRef<T>,
): ApiFactory<T, { [key in string]: unknown }> | undefined;
};
+81 -36
View File
@@ -26,7 +26,6 @@ import {
BackstageApp,
AppComponents,
AppConfigLoader,
Apis,
SignInResult,
SignInPageProps,
} from './types';
@@ -42,7 +41,6 @@ import { AppThemeProvider } from './AppThemeProvider';
import { IconComponent, SystemIcons, SystemIconKey } from '../icons';
import {
ApiHolder,
ApiProvider,
ApiRegistry,
AppTheme,
@@ -51,18 +49,22 @@ import {
configApiRef,
ConfigReader,
useApi,
AnyApiFactory,
ApiHolder,
} from '../apis';
import { ApiAggregator } from '../apis/ApiAggregator';
import { useAsync } from 'react-use';
import { AppIdentity } from './AppIdentity';
import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry';
import { ApiResolver } from '../apis/ApiResolver';
type FullAppOptions = {
apis: Apis;
apis: Iterable<AnyApiFactory>;
icons: SystemIcons;
plugins: BackstagePlugin[];
components: AppComponents;
themes: AppTheme[];
configLoader?: AppConfigLoader;
defaultApis: Iterable<AnyApiFactory>;
};
function useConfigLoader(
@@ -101,31 +103,27 @@ function useConfigLoader(
}
export class PrivateAppImpl implements BackstageApp {
private apis?: ApiHolder = undefined;
private apiHolder?: ApiHolder;
private configApi?: ConfigApi;
private readonly apis: Iterable<AnyApiFactory>;
private readonly icons: SystemIcons;
private readonly plugins: BackstagePlugin[];
private readonly components: AppComponents;
private readonly themes: AppTheme[];
private readonly configLoader?: AppConfigLoader;
private readonly defaultApis: Iterable<AnyApiFactory>;
private readonly identityApi = new AppIdentity();
private apisOrFactory: Apis;
constructor(options: FullAppOptions) {
this.apisOrFactory = options.apis;
this.apis = options.apis;
this.icons = options.icons;
this.plugins = options.plugins;
this.components = options.components;
this.themes = options.themes;
this.configLoader = options.configLoader;
}
getApis(): ApiHolder {
if (!this.apis) {
throw new Error('Tried to access APIs before app was loaded');
}
return this.apis;
this.defaultApis = options.defaultApis;
}
getPlugins(): BackstagePlugin[] {
@@ -186,12 +184,12 @@ export class PrivateAppImpl implements BackstageApp {
}
}
const FeatureFlags = this.apis && this.apis.get(featureFlagsApiRef);
if (FeatureFlags) {
FeatureFlags.registeredFeatureFlags = registeredFeatureFlags;
const featureFlags = this.getApiHolder().get(featureFlagsApiRef);
if (featureFlags) {
featureFlags.registeredFeatureFlags = registeredFeatureFlags;
}
routes.push(<Route element={<NotFoundErrorPage />} />);
routes.push(<Route path="/*" element={<NotFoundErrorPage />} />);
return routes;
}
@@ -210,28 +208,14 @@ export class PrivateAppImpl implements BackstageApp {
);
if ('node' in loadedConfig) {
// Loading or error
return loadedConfig.node;
}
const configApi = loadedConfig.api;
const appApis = ApiRegistry.from([
[appThemeApiRef, appThemeApi],
[configApiRef, configApi],
[identityApiRef, this.identityApi],
]);
if (!this.apis) {
if ('get' in this.apisOrFactory) {
this.apis = this.apisOrFactory;
} else {
this.apis = this.apisOrFactory(configApi);
}
}
const apis = new ApiAggregator(this.apis, appApis);
this.configApi = loadedConfig.api;
return (
<ApiProvider apis={apis}>
<ApiProvider apis={this.getApiHolder()}>
<AppContextProvider app={this}>
<AppThemeProvider>{children}</AppThemeProvider>
</AppContextProvider>
@@ -306,6 +290,67 @@ export class PrivateAppImpl implements BackstageApp {
return AppRouter;
}
private getApiHolder(): ApiHolder {
if (this.apiHolder) {
return this.apiHolder;
}
const registry = new ApiFactoryRegistry();
registry.register('static', {
api: appThemeApiRef,
deps: {},
factory: () => AppThemeSelector.createWithStorage(this.themes),
});
registry.register('static', {
api: configApiRef,
deps: {},
factory: () => {
if (!this.configApi) {
throw new Error(
'Tried to access config API before config was loaded',
);
}
return this.configApi;
},
});
registry.register('static', {
api: identityApiRef,
deps: {},
factory: () => this.identityApi,
});
for (const factory of this.defaultApis) {
registry.register('default', factory);
}
for (const plugin of this.plugins) {
for (const factory of plugin.getApis()) {
if (!registry.register('default', factory)) {
throw new Error(
`Plugin ${plugin.getId()} tried to register duplicate or forbidden API factory for ${
factory.api
}`,
);
}
}
}
for (const factory of this.apis) {
if (!registry.register('app', factory)) {
throw new Error(
`Duplicate or forbidden API factory for ${factory.api} in app`,
);
}
}
ApiResolver.validateFactories(registry, registry.getAllApis());
this.apiHolder = new ApiResolver(registry);
return this.apiHolder;
}
verify() {
const pluginIds = new Set<string>();
+5 -14
View File
@@ -17,8 +17,8 @@
import { ComponentType } from 'react';
import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
import { BackstagePlugin } from '../plugin';
import { ApiHolder } from '../apis';
import { AppTheme, ConfigApi, ProfileInfo } from '../apis/definitions';
import { AnyApiFactory } from '../apis';
import { AppTheme, ProfileInfo } from '../apis/definitions';
import { AppConfig } from '@backstage/config';
export type BootErrorPageProps = {
@@ -77,16 +77,12 @@ export type AppComponents = {
*/
export type AppConfigLoader = () => Promise<AppConfig[]>;
// TODO(Rugvip): Temporary workaround for accessing config when instantiating APIs, we might want to do this differently
export type Apis = ApiHolder | ((config: ConfigApi) => ApiHolder);
export type AppOptions = {
/**
* A holder of all APIs available in the app.
*
* Use for example ApiRegistry or ApiTestRegistry.
* A collection of ApiFactories to register in the application to either
* add add new ones, or override factories provided by default or by plugins.
*/
apis?: Apis;
apis?: Iterable<AnyApiFactory>;
/**
* Supply icons to override the default ones.
@@ -138,11 +134,6 @@ export type AppOptions = {
};
export type BackstageApp = {
/**
* Get the holder for all APIs available in the app.
*/
getApis(): ApiHolder;
/**
* Returns all plugins registered for the app.
*/
+6
View File
@@ -24,9 +24,11 @@ import {
} from './types';
import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
export type PluginConfig = {
id: string;
apis?: Iterable<AnyApiFactory>;
register?(hooks: PluginHooks): void;
};
@@ -65,6 +67,10 @@ export class PluginImpl {
return this.config.id;
}
getApis(): Iterable<AnyApiFactory> {
return this.config.apis ?? [];
}
output(): PluginOutput[] {
if (this.storedOutput) {
return this.storedOutput;
+2
View File
@@ -16,6 +16,7 @@
import { ComponentType } from 'react';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
export type RouteOptions = {
// Whether the route path must match exactly, defaults to true.
@@ -70,4 +71,5 @@ export type PluginOutput =
export type BackstagePlugin = {
getId(): string;
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
};
+1 -1
View File
@@ -32,7 +32,7 @@
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
+3 -2
View File
@@ -17,7 +17,6 @@
import React, { FC } from 'react';
import privateExports, {
AppOptions,
ApiRegistry,
defaultSystemIcons,
BootErrorPageProps,
AppConfigLoader,
@@ -26,6 +25,7 @@ import { BrowserRouter, MemoryRouter } from 'react-router-dom';
import { ErrorPage } from '../layout/ErrorPage';
import { Progress } from '../components/Progress';
import { defaultApis } from './defaultApis';
import { lightTheme, darkTheme } from '@backstage/theme';
import { AppConfig, JsonObject } from '@backstage/config';
@@ -94,7 +94,7 @@ export function createApp(options?: AppOptions) {
);
};
const apis = options?.apis ?? ApiRegistry.from([]);
const apis = options?.apis ?? [];
const icons = { ...defaultSystemIcons, ...options?.icons };
const plugins = options?.plugins ?? [];
const components = {
@@ -127,6 +127,7 @@ export function createApp(options?: AppOptions) {
components,
themes,
configLoader,
defaultApis,
});
app.verify();
@@ -0,0 +1,135 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
alertApiRef,
errorApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
featureFlagsApiRef,
FeatureFlags,
discoveryApiRef,
GoogleAuth,
GithubAuth,
OAuth2,
OktaAuth,
GitlabAuth,
Auth0Auth,
MicrosoftAuth,
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
oauth2ApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
auth0AuthApiRef,
microsoftAuthApiRef,
storageApiRef,
WebStorage,
createApiFactory,
configApiRef,
UrlPatternDiscovery,
} from '@backstage/core-api';
export const defaultApis = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
),
}),
createApiFactory(alertApiRef, new AlertApiForwarder()),
createApiFactory({
api: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) =>
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
}),
createApiFactory({
api: storageApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory(featureFlagsApiRef, new FeatureFlags()),
createApiFactory(oauthRequestApiRef, new OAuthRequestManager()),
createApiFactory({
api: googleAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GoogleAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
MicrosoftAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GithubAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: oktaAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
OktaAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GitlabAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
Auth0Auth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
api: oauth2ApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
OAuth2.create({ discoveryApi, oauthRequestApi }),
}),
];
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import { Typography, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, Typography } from '@material-ui/core';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import React from 'react';
const useErrorOutlineStyles = makeStyles<BackstageTheme>(theme => ({
root: {
@@ -60,9 +60,10 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
type Props = {
message?: React.ReactNode;
title?: string;
children?: React.ReactNode;
};
export const WarningPanel: FC<Props> = props => {
export const WarningPanel = (props: Props) => {
const classes = useStyles(props);
const { title, message, children } = props;
return (
+26 -14
View File
@@ -14,12 +14,13 @@
* limitations under the License.
*/
import React, { FC, ReactNode } from 'react';
import React, { ReactNode } from 'react';
import {
Card,
CardActions,
CardContent,
CardHeader,
CardHeaderProps,
Divider,
withStyles,
makeStyles,
@@ -29,22 +30,26 @@ import { ErrorBoundary } from '../ErrorBoundary';
import { BottomLink, BottomLinkProps } from '../BottomLink';
const useStyles = makeStyles(theme => ({
header: {
padding: theme.spacing(2, 2, 2, 2.5),
},
noPadding: {
padding: 0,
'&:last-child': {
paddingBottom: 0,
},
},
header: {
padding: theme.spacing(2, 2, 2, 2.5),
},
headerTitle: {
fontWeight: 700,
},
headerSubheader: {
paddingTop: theme.spacing(1),
},
headerAvatar: {},
headerAction: {},
headerContent: {},
}));
const BoldHeader = withStyles(theme => ({
title: { fontWeight: 700 },
subheader: { paddingTop: theme.spacing(1) },
}))(CardHeader);
const CardActionsTopRight = withStyles(theme => ({
root: {
display: 'inline-block',
@@ -130,7 +135,7 @@ type Props = {
cardStyle?: object;
children?: ReactNode;
headerStyle?: object;
headerProps?: object;
headerProps?: CardHeaderProps;
actionsClassName?: string;
actions?: ReactNode;
cardClassName?: string;
@@ -139,7 +144,7 @@ type Props = {
noPadding?: boolean;
};
export const InfoCard: FC<Props> = ({
export const InfoCard = ({
title,
subheader,
divider,
@@ -155,7 +160,7 @@ export const InfoCard: FC<Props> = ({
actionsTopRight,
className,
noPadding,
}) => {
}: Props): JSX.Element => {
const classes = useStyles();
/**
@@ -186,8 +191,15 @@ export const InfoCard: FC<Props> = ({
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<>
<BoldHeader
className={classes.header}
<CardHeader
classes={{
root: classes.header,
title: classes.headerTitle,
subheader: classes.headerSubheader,
avatar: classes.headerAvatar,
action: classes.headerAction,
content: classes.headerContent,
}}
title={title}
subheader={subheader}
style={{ display: 'inline-block', ...headerStyle }}
@@ -39,6 +39,7 @@ proxy:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
lighthouse:
baseUrl: http://localhost:3003
@@ -9,6 +9,7 @@
"start": "yarn workspace app start",
"build": "lerna run build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
"test": "lerna run test --since origin/master -- --coverage",
@@ -4,7 +4,7 @@
"private": true,
"bundled": true,
"dependencies": {
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
@@ -4,12 +4,17 @@ import {
AlertDisplay,
OAuthRequestDialog,
SidebarPage,
createRouteRef,
} from '@backstage/core';
import { apis } from './apis';
import * as plugins from './plugins';
import { AppSidebar } from './sidebar';
import { Route, Routes, Navigate } from 'react-router';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { EntityPage } from './components/catalog/EntityPage';
const app = createApp({
@@ -21,6 +26,12 @@ const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const deprecatedAppRoutes = app.getRoutes();
const catalogRouteRef = createRouteRef({
path: '/catalog',
title: 'Service Catalog',
});
const App: FC<{}> = () => (
<AppProvider>
<AlertDisplay />
@@ -29,11 +40,20 @@ const App: FC<{}> = () => (
<SidebarPage>
<AppSidebar />
<Routes>
<Navigate key="/" to="/catalog" />
<Route
path="/catalog/*"
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Navigate key="/" to="/catalog" />
<Route path="/docs/*" element={<DocsRouter />} />
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
<Route
path="/register-component"
element={<RegisterComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
{deprecatedAppRoutes}
</Routes>
</SidebarPage>
@@ -1,89 +1,17 @@
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ConfigApi,
ErrorApiForwarder,
ErrorAlerter,
discoveryApiRef,
UrlPatternDiscovery,
oauthRequestApiRef,
OAuthRequestManager,
storageApiRef,
WebStorage,
createApiFactory,
configApiRef,
} from '@backstage/core';
import {
lighthouseApiRef,
LighthouseRestApi,
} from '@backstage/plugin-lighthouse';
import {
GithubActionsClient,
githubActionsApiRef,
} from '@backstage/plugin-github-actions';
import {
techdocsStorageApiRef,
TechDocsStorageApi,
} from '@backstage/plugin-techdocs';
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
const backendUrl = config.getString('backend.baseUrl');
const techdocsStorageUrl = config.getString('techdocs.storageUrl');
const builder = ApiRegistry.builder();
const discoveryApi = builder.add(
discoveryApiRef,
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
);
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
);
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(oauthRequestApiRef, new OAuthRequestManager());
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(githubActionsApiRef, new GithubActionsClient());
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
builder.add(
circleCIApiRef,
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
);
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
builder.add(
techRadarApiRef,
new TechRadar({
width: 1500,
height: 800,
}),
);
builder.add(
techdocsStorageApiRef,
new TechDocsStorageApi({ apiOrigin: techdocsStorageUrl }),
);
return builder.build();
};
export const apis = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
),
}),
];
@@ -13,8 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Router as GitHubActionsRouter,
isPluginApplicableToEntity as isGitHubActionsAvailable,
} from '@backstage/plugin-github-actions';
import {
Router as CircleCIRouter,
isPluginApplicableToEntity as isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
import { Router as GitHubActionsRouter } from '@backstage/plugin-github-actions';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import React from 'react';
import {
EntityPageLayout,
@@ -22,9 +31,33 @@ import {
AboutCard,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
import { WarningPanel } from '@backstage/core';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
switch (true) {
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
default:
return (
<WarningPanel title="CI/CD switcher:">
No CI/CD is available for this entity. Check corresponding
annotations!
</WarningPanel>
);
}
};
const OverviewContent = ({ entity }: { entity: Entity }) => (
<AboutCard entity={entity} />
<Grid container spacing={3}>
<Grid item>
<AboutCard entity={entity} />
</Grid>
</Grid>
);
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
@@ -37,13 +70,18 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ApiDocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -57,7 +95,12 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<GitHubActionsRouter entity={entity} />}
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -69,6 +112,11 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -2,7 +2,6 @@ import React from 'react';
import HomeIcon from '@material-ui/icons/Home';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import ExploreIcon from '@material-ui/icons/Explore';
import BuildIcon from '@material-ui/icons/BuildRounded';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
@@ -23,7 +22,6 @@ export const AppSidebar = () => (
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="./" text="Home" />
<SidebarItem icon={ExploreIcon} to="explore" text="Explore" />
<SidebarItem icon={LibraryBooks} to="/docs" text="Docs" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
<SidebarDivider />
@@ -9,7 +9,6 @@
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": ".",
"skipLibCheck": true
"rootDir": "."
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
@@ -1,108 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
alertApiRef,
errorApiRef,
ErrorApiForwarder,
AlertApi,
createApiFactory,
ErrorAlerter,
AlertApiForwarder,
oauthRequestApiRef,
OAuthRequestManager,
UrlPatternDiscovery,
discoveryApiRef,
GoogleAuth,
googleAuthApiRef,
GithubAuth,
githubAuthApiRef,
GitlabAuth,
gitlabAuthApiRef,
Auth0Auth,
auth0AuthApiRef,
} from '@backstage/core';
// TODO(rugvip): We should likely figure out how to reuse all of these between apps
// and plugin serve with minimal boilerplate. For example we might move everything
// to DI, and provide factories for the default implementations, so this just becomes
// a list of things like `[ErrorApiForwarder.factory, AlertApiDialog.factory]`.
export const alertApiFactory = createApiFactory({
implements: alertApiRef,
deps: {},
factory: (): AlertApi => new AlertApiForwarder(),
});
export const errorApiFactory = createApiFactory({
implements: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) =>
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
});
export const oauthRequestApiFactory = createApiFactory({
implements: oauthRequestApiRef,
deps: {},
factory: () => new OAuthRequestManager(),
});
export const discoveryApiFactory = createApiFactory({
implements: discoveryApiRef,
deps: {},
factory: () =>
UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`),
});
export const googleAuthApiFactory = createApiFactory({
implements: googleAuthApiRef,
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
}),
});
export const githubAuthApiFactory = createApiFactory({
implements: githubAuthApiRef,
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GithubAuth.create({
discoveryApi,
oauthRequestApi,
}),
});
export const gitlabAuthApiFactory = createApiFactory({
implements: gitlabAuthApiRef,
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
}),
});
export const auth0AuthApiFactory = createApiFactory({
implements: auth0AuthApiRef,
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
factory: ({ discoveryApi, oauthRequestApi }) =>
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
}),
});
@@ -0,0 +1,43 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { useApi, configApiRef } from '@backstage/core';
import { createDevApp } from './render';
const anyEnv = (process.env = { ...process.env }) as any;
describe('DevAppBuilder', () => {
it('should be able to render a component in a dev app', async () => {
anyEnv.APP_CONFIG = [
{ context: 'test', data: { app: { title: 'Test App' } } },
];
const MyComponent = () => {
const configApi = useApi(configApiRef);
return <div>My App: {configApi.getString('app.title')}</div>;
};
const DevApp = createDevApp()
.addRootChild(<MyComponent />)
.build();
const rendered = render(<DevApp />);
expect(await rendered.findByText('My App: Test App')).toBeInTheDocument();
});
});
+6 -32
View File
@@ -26,12 +26,10 @@ import {
SidebarSpacer,
ApiFactory,
createPlugin,
ApiTestRegistry,
ApiHolder,
AlertDisplay,
OAuthRequestDialog,
AnyApiFactory,
} from '@backstage/core';
import * as defaultApiFactories from './apiFactories';
import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied';
// TODO(rugvip): export proper plugin type from core that isn't the plugin class
@@ -43,7 +41,7 @@ type BackstagePlugin = ReturnType<typeof createPlugin>;
*/
class DevAppBuilder {
private readonly plugins = new Array<BackstagePlugin>();
private readonly factories = new Array<ApiFactory<any, any, any>>();
private readonly apis = new Array<AnyApiFactory>();
private readonly rootChildren = new Array<ReactNode>();
/**
@@ -57,10 +55,10 @@ class DevAppBuilder {
/**
* Register an API factory to add to the app
*/
registerApiFactory<Api, Impl, Deps>(
factory: ApiFactory<Api, Impl, Deps>,
registerApi<Api, Deps extends { [name in string]: unknown }>(
factory: ApiFactory<Api, Deps>,
): DevAppBuilder {
this.factories.push(factory);
this.apis.push(factory);
return this;
}
@@ -79,7 +77,7 @@ class DevAppBuilder {
*/
build(): ComponentType<{}> {
const app = createApp({
apis: this.setupApiRegistry(this.factories),
apis: this.apis,
plugins: this.plugins,
});
@@ -170,30 +168,6 @@ class DevAppBuilder {
);
}
// Set up an API registry that merges together default implementations with ones provided through config.
private setupApiRegistry(
providedFactories: ApiFactory<any, any, any>[],
): ApiHolder {
const providedApis = new Set(
providedFactories.map(factory => factory.implements),
);
// Exlude any default API factory that we receive a factory for in the config
const defaultFactories = Object.values(defaultApiFactories).filter(
factory => !providedApis.has(factory.implements),
);
const allFactories = [...defaultFactories, ...providedFactories];
// Use a test registry with dependency injection so that the consumer
// can override APIs but still depend on the default implementations.
const registry = new ApiTestRegistry();
for (const factory of allFactories) {
registry.register(factory);
}
return registry;
}
private findPluginPaths(plugins: BackstagePlugin[]) {
const paths = new Array<string>();
+1 -1
View File
@@ -33,7 +33,7 @@
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -14,12 +14,7 @@
* limitations under the License.
*/
import {
ErrorApi,
ErrorContext,
errorApiRef,
Observable,
} from '@backstage/core-api';
import { ErrorApi, ErrorContext, Observable } from '@backstage/core-api';
type Options = {
collect?: boolean;
@@ -40,12 +35,6 @@ const nullObservable = {
};
export class MockErrorApi implements ErrorApi {
static factory = {
implements: errorApiRef,
deps: {},
factory: () => new MockErrorApi(),
};
private readonly errors = new Array<ErrorWithContext>();
private readonly waiters = new Set<Waiter>();
@@ -17,7 +17,6 @@
import {
Observable,
StorageApi,
storageApiRef,
StorageValueChange,
} from '@backstage/core-api';
import ObservableImpl from 'zen-observable';
@@ -25,12 +24,6 @@ import ObservableImpl from 'zen-observable';
export type MockStorageBucket = { [key: string]: any };
export class MockStorageApi implements StorageApi {
static factory = {
implements: storageApiRef,
deps: {},
factory: () => MockStorageApi.create(),
};
private readonly namespace: string;
private readonly data: MockStorageBucket;
@@ -49,9 +49,6 @@ describe('wrapInTestApp', () => {
expect.stringMatching(
/^Warning: An update to %s inside a test was not wrapped in act\(...\)/,
),
expect.stringMatching(
/^Warning: An update to %s inside a test was not wrapped in act\(...\)/,
),
]);
});
@@ -24,7 +24,7 @@ import privateExports, {
} from '@backstage/core-api';
import { RenderResult } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils-core';
import { createMockApiRegistry } from './mockApiRegistry';
import { mockApis } from './mockApis';
const { PrivateAppImpl } = privateExports;
@@ -58,10 +58,9 @@ export function wrapInTestApp(
options: TestAppOptions = {},
): ReactElement {
const { routeEntries = ['/'] } = options;
const apis = createMockApiRegistry();
const app = new PrivateAppImpl({
apis,
apis: [],
components: {
NotFoundErrorPage,
BootErrorPage,
@@ -80,6 +79,7 @@ export function wrapInTestApp(
variant: 'light',
},
],
defaultApis: mockApis,
});
let Wrapper: ComponentType;
@@ -14,14 +14,14 @@
* limitations under the License.
*/
import { ApiTestRegistry } from '@backstage/core-api';
import {
storageApiRef,
errorApiRef,
createApiFactory,
} from '@backstage/core-api';
import { MockErrorApi, MockStorageApi } from './apis';
export function createMockApiRegistry(): ApiTestRegistry {
const registry = new ApiTestRegistry();
registry.register(MockErrorApi.factory);
registry.register(MockStorageApi.factory);
return registry;
}
export const mockApis = [
createApiFactory(errorApiRef, new MockErrorApi()),
createApiFactory(storageApiRef, MockStorageApi.create()),
];
+1 -1
View File
@@ -28,7 +28,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@material-ui/core": "^4.9.1"
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21"
+3 -3
View File
@@ -15,7 +15,7 @@
*/
import { createTheme } from './baseTheme';
import { blue, yellow } from '@material-ui/core/colors';
import { yellow } from '@material-ui/core/colors';
export const lightTheme = createTheme({
palette: {
@@ -39,7 +39,7 @@ export const lightTheme = createTheme({
},
},
primary: {
main: blue[500],
main: '#2E77D0',
},
banner: {
info: '#2E77D0',
@@ -95,7 +95,7 @@ export const darkTheme = createTheme({
},
},
primary: {
main: blue[500],
main: '#2E77D0',
},
banner: {
info: '#2E77D0',

Some files were not shown because too many files have changed in this diff Show More