Merge remote-tracking branch 'backstage/master' into erikengervall/plugin-release-manager-as-a-service

This commit is contained in:
Erik Engervall
2021-04-13 11:37:38 +02:00
179 changed files with 4019 additions and 1754 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Allow custom directory to be specified for GitHub publish action
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-sonarqube': patch
---
Export isSonarQubeAvailable.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add built-in publish action for creating GitHub pull requests.
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/plugin-badges': minor
'@backstage/plugin-badges-backend': patch
---
Support auth in badge plugin
-15
View File
@@ -1,15 +0,0 @@
---
'@backstage/config-loader': minor
---
Fix bug where `$${...}` was not being escaped to `${...}`
Add support for environment variable substitution in `$include`, `$file` and
`$env` transform values.
- This change allows for including dynamic paths, such as environment specific
secrets by using the same environment variable substitution (`${..}`) already
supported outside of the various include transforms.
- If you are currently using the syntax `${...}` in your include transform values,
you will need to escape the substitution by using `$${...}` instead to maintain
the same behavior.
+30
View File
@@ -0,0 +1,30 @@
---
'@backstage/create-app': patch
---
Fixing dependency resolution for problematic library `graphql-language-service-interface`.
This change might not have to be applied to your local installation, however if you run into this error:
```
Error: Failed to compile.
/tmp/backstage-e2e-uMeycm/test-app/node_modules/graphql-language-service-interface/esm/GraphQLLanguageService.js 100:23
Module parse failed: Unexpected token (100:23)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| }
| let customRules = null;
> if (extensions?.customValidationRules &&
| typeof extensions.customValidationRules === 'function') {
| customRules = extensions.customValidationRules(this._graphQLConfig);
```
You can fix it by adding the following to the root `package.json`.
```json
...
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
...
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
When using OAuth2 authentication the name is now taken from the name property of the JWT instead of the email property
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix for the `file://` protocol check in the `FilePreparer` being too strict, breaking Windows.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/core': patch
'@backstage/core-api': patch
---
Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading
errors visible to the user.
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Add support for configure an LDAP query filter on multiple lines.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/catalog-model': patch
---
Added `stringifyEntityRef`, which always creates a string representation of an entity reference. Also deprecated `serializeEntityRef`, as `stringifyEntityRef` should be used instead.
+31
View File
@@ -0,0 +1,31 @@
---
'@backstage/create-app': patch
'@backstage/plugin-scaffolder': minor
---
Expose the catalog-import route as an external route from the scaffolder.
This will make it possible to hide the "Register Existing Component" button
when you for example are running backstage with `catalog.readonly=true`.
As a consequence of this change you need add a new binding to your createApp call to
keep the button visible. However, if you instead want to hide the button you can safely
ignore the following example.
To bind the external route from the catalog-import plugin to the scaffolder template
index page, make sure you have the appropriate imports and add the following
to the createApp call:
```typescript
import { catalogImportPlugin } from '@backstage/plugin-catalog-import';
const app = createApp({
// ...
bindRoutes({ bind }) {
// ...
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
```
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Bitbucket server needs username to be set as well as the token or appPassword for the publishing process to work.
```yaml
integrations:
bitbucket:
- host: bitbucket.mycompany.com
apiBaseUrl: https://bitbucket.mycompany.com/rest/api/1.0
token: token
username: username
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-import': patch
---
When importing components you will now have the ability to use non Unicode characters in the entity owner field
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/test-utils': patch
---
Remove unnecessary wrapping of elements rendered by `wrapInTestApp` and `renderInTestApp`, which was breaking mount discovery.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog': patch
---
Implemented missing support for the dependsOn/dependencyOf relationships
between `Component` and `Resource` catalog model objects.
Added support for generating the relevant relationships to the
`BuiltinKindsEntityProcessor`, and added simple support for fetching
relationships between `Components` and `Resources` for rendering in the
system diagram. All catalog-model changes backwards compatible.
-8
View File
@@ -1,8 +0,0 @@
---
'@backstage/catalog-model': patch
'@backstage/core': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-scaffolder': patch
---
Add support for multiple links to post-scaffold task summary page
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-org': patch
---
Optimize data fetched for the `OwnershipCard`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Change the response status of metadata endpoints in case a documentation is not
available to `404 NOT FOUND`. This also introduces the JSON based error messages
used by other backends.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Adds a collaborator field to GitHub publish action for multiple users and access levels
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Add borders to TechDocs tables and increase font size. Fixes #5264 and #5276.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core': patch
'@backstage/plugin-api-docs': patch
---
add debounce time attribute for apis-docs for search, giving more time to the users when they are typing.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
exposes undocumented `PageTheme`
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-deployments': patch
---
Add a button to reload the GitHub Deployments card
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Don't use a drag & drop cursor when clicking on disabled `IconLinkVertical`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix bug in pull request sample template.
+2
View File
@@ -13,6 +13,7 @@ Cloudformation
Codecov
Codehilite
Config
Debounce
Discoverability
Dockerfile
Dockerize
@@ -111,6 +112,7 @@ cookiecutter
css
dataflow
deadnaming
debounce
declaratively
destructured
dev
+1
View File
@@ -24,3 +24,4 @@
| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
+4
View File
@@ -150,6 +150,10 @@ integrations:
- host: dev.azure.com
token:
$env: AZURE_TOKEN
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey:
# $env: GCS_PRIVATE_KEY
catalog:
rules:
@@ -0,0 +1,170 @@
# Using AWS Application Load Balancer with Azure Active Directory to authenticate requests
Backstage allows offloading the responsibility of authenticating users to an AWS Application Load Balancer (**ALB**), leveraging the authentication support on ALB.
This tutorial shows how to use authentication on an ALB sitting in front of Backstage.
Azure Active Directory (**AAD**) is used as identity provider but any identity provider supporting OpenID Connect (OIDC) can be used.
It is assumed an ALB is already serving traffic in front of a Backstage instance configured to serve the frontend app from the backend.
## Infrastructure setup
### AAD App
The AAD App is used to execute the authentication flow, serve and refresh the identity token.
Create the AAD App following the steps outlined in `Create a Microsoft App Registration in Microsoft Portal` section from the tutorial [Monorepo App Setup With Authentication][monorepo-app-setup-with-auth].
Instead of `localhost` addresses, use the following values.
- Identifier URI: `https://backstage.yourdomain.com`
- Redirect URI: `https://backstage.yourdomain.com/oauth2/idpresponse`
`Application (client) Id`, `Directory (tenant) ID` and `client secret`values will be used while configuring the ALB.
### ALB
In the AWS console, configure ALB Authentication:
- Edit the ALB rule used to forward the traffic to Backstage and add a new `Authenticate` action. The action will have higher priority compared to the existing `Forward to`.
- Select `OIDC` under `Authenticate`
- Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App.
- Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo`
- Set `Client ID` to the AAD App `Application (client) Id`
- Set `Client secret` to the AAD APP `client secret`
Use the following advanced settings:
- `Session cookie name` = `AWSELBAuthSessionCookie`
- `Session timeout` = `604800` seconds
- `Scope` = `openid profile offline_access`
- `Action on unauthenticated request` = `Autenticate (client reattempt)`
Once you've saved the action, you should see an authentication flow be triggered against AAD when visiting Backstage address at `https://backstage.yourdomain.com`. The flow will not complete successfully as the Backstage app isn't yet configured properly.
## Backstage changes
### Frontend
The Backstage App needs a SignInPage when authentication is required.
When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a dummy SignIn component that can refresh the token.
- edit `packages/app/src/App.tsx`
- import the following two additional definitions from `@backstage/core`: `useApi`, `configApiRef`; these will be used to check whether Backstage is running locally or behind an ALB
- add the following definition just before the app is created (`const app = createApp`):
```ts
const DummySignInComponent: any = (props: any) => {
try {
const config = useApi(configApiRef);
const shouldAuth = !!config.getOptionalConfig('auth.providers.awsalb');
if (shouldAuth) {
fetch(`${window.location.origin}/api/auth/awsalb/refresh`)
.then(data => data.json())
.then(data => {
props.onResult({
userId: data.backstageIdentity.id,
profile: data.profile,
});
});
} else {
props.onResult({
userId: 'guest',
profile: {
email: 'guest@example.com',
displayName: 'Guest',
picture: '',
},
});
}
return <div />;
} catch (err) {
return <div>{err.message}</div>;
}
};
```
- add `DummySignInComponent` as `SignInPage`:
```ts
const app = createApp({
...
components: {
SignInPage: DummySignInComponent,
...
},
...
});
```
### Backend
When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented.
- replace the content of `packages/backend/plugin/auth.ts` with the below
```ts
import {
createRouter,
AuthResponse,
AuthProviderFactoryOptions,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
database,
config,
discovery,
}: PluginEnvironment) {
const identityResolver = (payload: any): Promise<AuthResponse<any>> => {
return Promise.resolve({
providerInfo: {},
profile: {
email: payload.email,
displayName: payload.name,
picture: payload.picture,
},
backstageIdentity: {
id: payload.email,
},
});
};
const providerFactories = {
awsalb: (options: AuthProviderFactoryOptions) =>
defaultAuthProviderFactories.awsalb({ ...options, identityResolver }),
};
return await createRouter({
logger,
config,
database,
discovery,
providerFactories,
});
}
```
### Configuration
Use the following `auth` configuration when running Backstage on AWS:
```yaml
auth:
providers:
awsalb:
issuer:
issuer: https://login.microsoftonline.com/{TENANT_ID}/v2.0
region: { AWS_REGION }
```
Replace `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App and `{AWS_REGION}` with the AWS region identifier where the ALB is deployed (for example: `eu-central-1`).
## Conclusion
Once it's deployed, after going through the AAD authentication flow, Backstage should display the AAD user details.
<!-- links -->
[monorepo-app-setup-with-auth-ms]: https://backstage.io/docs/tutorials/quickstart-app-auth#the-auth-configuration
+1 -1
View File
@@ -22,7 +22,7 @@ Backstage comes with many common authentication providers in the core library:
- [GitLab](gitlab/provider.md)
- [Google](google/provider.md)
- [Okta](okta/provider.md)
- OneLogin
- [OneLogin](onelogin/provider.md)
These built-in providers handle the authentication flow for a particular service
including required scopes, callbacks, etc. These providers are each added to a
+54
View File
@@ -0,0 +1,54 @@
---
id: provider
title: OneLogin Authentication Provider
sidebar_label: OneLogin
description: Adding OneLogin OIDC as an authentication provider in Backstage
---
The Backstage `core-api` package comes with a OneLogin authentication provider
that can authenticate users using OpenID Connect.
## Create an Application on OneLogin
To support OneLogin authentication, you must create an Application:
1. From the OneLogin Admin portal, choose Applications
2. Click `Add App` and select `OpenID Connect`
- Display Name: Backstage (or your custom app name)
3. Click Save
4. Go to the Configuration tab for the Application and set:
- `Login Url`: `http://localhost:3000`
- `Redirect URIs`: `http://localhost:7000/api/auth/onelogin/handler/frame`
5. Click Save
6. Go to the SSO tab for the Application and set:
- `Token Endpoint` > `Authentication Method`: `POST`
7. Click Save
## Configuration
The provider configuration can then be added to your `app-config.yaml` under the
root `auth` configuration:
```yaml
auth:
environment: development
providers:
onelogin:
development:
clientId: ${AUTH_ONELOGIN_CLIENT_ID}
clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET}
issuer: https://<company>.onelogin.com/oidc/2
```
The OneLogin provider is a structure with three configuration keys; **these are
found on the SSO tab** for the OneLogin Application:
- `clientId`: The client ID
- `clientSecret`: The client secret
- `issuer`: The issuer URL
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `oneloginAuthApi` reference and
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
+22 -2
View File
@@ -54,6 +54,8 @@ versions:check Check Backstage package versioning
prepack Prepares a package for packaging before publishing
postpack Restores the changes made by the prepack command
create-github-app Create new GitHub App in your organization (experimental)
help [command] display help for command
```
@@ -190,10 +192,13 @@ output of `backstage-cli backend:bundle` into an image:
FROM node:14-buster-slim
WORKDIR /app
ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend"]
```
@@ -606,3 +611,18 @@ the resulting archive in the target `workspace-dir`.
```text
Usage: backstage-cli build-workspace [options] &lt;workspace-dir&gt;
```
## create-github-app
Scope: `root`
Creates a GitHub App in your GitHub organization. This is an alternative to
token-based [GitHub integration](../integrations/github/locations.md). See
[GitHub Apps for Backstage Authentication](../plugins/github-apps.md).
Launches a browser to create the App through GitHub and saves the result as a
YAML file that can be referenced in the GitHub integration configuration.
```text
Usage: backstage-cli create-github-app &lt;github-org&gt;
```
@@ -519,6 +519,17 @@ consumed by the component, e.g. `artist-api`. This field is optional.
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| [`API`](#kind-api) (default) | Same as this entity, typically `default` | [`consumesApi`, and reverse `apiConsumedBy`](well-known-relations.md#consumesapi-and-apiconsumedby) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the component depends on, e.g. `artists-db`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: Template
The following describes the following entity kind:
@@ -1014,6 +1025,17 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional.
| --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
| [`System`](#kind-system) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) |
### `spec.dependsOn` [optional]
An array of [entity references](#string-references) to the components and
resources that the resource depends on, e.g. `artist-lookup`. This field is
optional.
| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) |
## Kind: System
Describes the following entity kind:
@@ -265,3 +265,15 @@ GITHUB_TOKEN=<token> yarn start
If you've also set up the frontend plugin, so you should be ready to go browse
the templates at [localhost:3000/create](http://localhost:3000/create) now!
### Disabling Docker in Docker situation (Optional)
Software Templates use
[Cookiecutter](https://github.com/cookiecutter/cookiecutter) as templating
library. By default it will use the
[spotify/backstage-cookiecutter](<[spotify/backstage-cookiecutter](https://github.com/backstage/backstage/blob/37e35b910afc7d1270855aed0ec4718aba366c91/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile)>)
docker image.
If you are running backstage from a Docker container and you want to avoid
calling a container inside a container, you can set up Cookiecutter in your own
image, this will use the local installation instead.
+61
View File
@@ -74,3 +74,64 @@ Note that your list of custom themes overrides the default themes. If you still
want to use the default themes, they are exported as `lightTheme` and
`darkTheme` from
[@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
## Example of a custom theme
```ts
const themeOptions = createThemeOptions({
palette: {
...lightTheme.palette,
primary: {
main: '#123456',
},
secondary: {
main: '#123456',
},
error: {
main: '#123456'
},
warning: {
main: '#123456',
},
info: {
main: '#123456',
},
success: {
main: '#123456',
},
background: {
default: '#123456',
paper: '#123456',
},
banner: {
info: '#123456',
error: '#123456'
text: '#123456'
link: '#123456',
},
errorBackground: '#123456'
warningBackground: '#123456'
infoBackground: '#123456'
navigation: {
background: '#123456',
indicator: '#123456'
color: '#123456'
selectedColor: '#123456',
},
},
defaultPageTheme: 'home',
fontFamily: 'Comic Sans',
/* below drives the header colors */
pageTheme: {
home: genPageTheme(['#123456','#123456'], shapes.wave),
documentation: genPageTheme(['#123456','#123456'], shapes.wave2),
tool: genPageTheme(['#123456','#123456'], shapes.round),
service: genPageTheme(['#123456','#123456'], shapes.wave),
website: genPageTheme(['#123456','#123456'], shapes.wave),
library: genPageTheme(['#123456','#123456'] shapes.wave),
other: genPageTheme(['#123456','#123456'], shapes.wave),
app: genPageTheme(['#123456','#123456'], shapes.wave),
apis: genPageTheme(['#123456','#123456'], shapes.wave),
},
});
```
@@ -68,9 +68,9 @@ proxy:
In a standard Backstage app created with
[@backstage/create-app](./create-an-app.md), the sidebar is managed inside
`packages/app/src/components/Root.tsx`. The file exports the entire `Sidebar`
element of your app, which you can extend with additional entries by adding new
`SidebarItem` elements.
`packages/app/src/components/Root/Root.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:
+6
View File
@@ -106,6 +106,12 @@ The install process may also fail if no Python installation is available. Python
is commonly available in most systems already, but if it isn't you can head for
example [here](https://www.python.org/downloads/) to install it.
#### Could not execute command yarn install
Install yarn on your system with `npm install --global yarn` or for more details
refer to the
[prerequisites](https://backstage.io/docs/getting-started/running-backstage-locally#prerequisites)
## Run the app
When the installation is complete you can open the app folder and start the app.
+3
View File
@@ -38,3 +38,6 @@ a structure with up to four elements:
- `apiBaseUrl` (optional): The URL of the Bitbucket API. For self-hosted
installations, it is commonly at `https://<host>/rest/api/1.0`. For
bitbucket.org, this configuration is not needed as it can be inferred.
> Note: If you are using Bitbucket server you MUST set the username as well as
> the token or appPassword.
@@ -0,0 +1,57 @@
---
id: locations
sidebar_label: Locations
title: Google Cloud Storage Locations
# prettier-ignore
description: Setting up an integration with Google Cloud Storage
---
The Backstage catalog can import entities from a yaml file stored in a GCS
(Google Cloud Storage) bucket. To enable the ingestion of said entities the
`GoogleGcs` integration must be enabled first.
## Configuration
To configure the integration add the appropriate credentials to the Backstage
backend. There are two main ways to do this: by explicitly setting a
`clientEmail` and a `privateKey` or by letting the Google Storage SDK discover
the credentials automatically.
### Explicit credentials
Explicit credentials can be set in the following format:
```yaml
integrations:
googleGcs:
clientEmail:
$env: GCS_CLIENT_EMAIL
privateKey:
$env: GCS_PRIVATE_KEY
```
Then make sure the environment variables `GCS_CLIENT_EMAIL` and
`GCS_PRIVATE_KEY` are set when you run Backstage.
### Automatic discovery of Google credentials
Since this integration uses the Google Storage SDK, you can also choose to not
provide any explicit credentials and let the SDK discover them automatically.
One of these discovery methods is to provide an environment variable called
`GOOGLE_APPLICATION_CREDENTIALS` and set it to the file path of your JSON
service account key.
For more details and methods to provide credentials to the Google Storage SDK
you can check [this documentation page][google gcs docs].
## Usage
To use this integration to import entities from a GCS bucket go to the Google
console and browse the file you would like to import. Then copy the
`Authenticated URL` and paste it into the text box in the `register component`
form. This url should look like
`https://storage.cloud.google.com/<bucket>/<path>/catalog-info.yaml`.
[google gcs docs]:
https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs
+1 -1
View File
@@ -292,7 +292,7 @@ class Index extends React.Component {
Build your own software templates
</ActionBlock.Title>
<ActionBlock.Link
href={`https://github.com/backstage/backstage/blob/master/docs/features/software-templates/extending/index.md`}
href={`https://github.com/backstage/backstage/blob/master/docs/features/software-templates/index.md`}
>
Contribute
</ActionBlock.Link>
+7 -1
View File
@@ -142,6 +142,11 @@
"type": "subcategory",
"label": "LDAP",
"ids": ["integrations/ldap/org"]
},
{
"type": "subcategory",
"label": "Google GCS",
"ids": ["integrations/google-cloud-storage/locations"]
}
],
"Plugins": [
@@ -195,7 +200,8 @@
"auth/github/provider",
"auth/gitlab/provider",
"auth/google/provider",
"auth/okta/provider"
"auth/okta/provider",
"auth/onelogin/provider"
]
},
"auth/add-auth-provider",
+3 -1
View File
@@ -38,7 +38,9 @@
"resolutions": {
"**/@roadiehq/**/@backstage/core": "*",
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*"
"**/@roadiehq/**/@backstage/catalog-model": "*",
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
"version": "1.0.0",
"devDependencies": {
+24
View File
@@ -1,5 +1,29 @@
# example-app
## 0.2.23
### Patch Changes
- Updated dependencies [d0b4ebf22]
- Updated dependencies [1279a3325]
- Updated dependencies [4a4681b1b]
- Updated dependencies [97b60de98]
- Updated dependencies [3f96a9d5a]
- Updated dependencies [b051e770c]
- Updated dependencies [f9c75f7a9]
- Updated dependencies [98dd5da71]
- Updated dependencies [97d53f686]
- Updated dependencies [64d2ce700]
- @backstage/plugin-badges@0.2.0
- @backstage/core@0.7.4
- @backstage/catalog-model@0.7.6
- @backstage/plugin-scaffolder@0.8.2
- @backstage/plugin-catalog-import@0.5.2
- @backstage/plugin-catalog@0.5.3
- @backstage/plugin-org@0.3.12
- @backstage/plugin-github-deployments@0.1.2
- @backstage/cli@0.6.7
## 0.2.21
### Patch Changes
+15 -15
View File
@@ -1,17 +1,17 @@
{
"name": "example-app",
"version": "0.2.21",
"version": "0.2.23",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/cli": "^0.6.6",
"@backstage/core": "^0.7.3",
"@backstage/catalog-model": "^0.7.6",
"@backstage/cli": "^0.6.7",
"@backstage/core": "^0.7.4",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-api-docs": "^0.4.9",
"@backstage/plugin-badges": "^0.1.2",
"@backstage/plugin-catalog": "^0.5.1",
"@backstage/plugin-catalog-import": "^0.5.0",
"@backstage/plugin-badges": "^0.2.0",
"@backstage/plugin-catalog": "^0.5.3",
"@backstage/plugin-catalog-import": "^0.5.2",
"@backstage/plugin-catalog-react": "^0.1.3",
"@backstage/plugin-circleci": "^0.2.12",
"@backstage/plugin-cloudbuild": "^0.2.13",
@@ -19,7 +19,7 @@
"@backstage/plugin-explore": "^0.3.2",
"@backstage/plugin-gcp-projects": "^0.2.5",
"@backstage/plugin-github-actions": "^0.4.2",
"@backstage/plugin-github-deployments": "^0.1.1",
"@backstage/plugin-github-deployments": "^0.1.2",
"@backstage/plugin-gitops-profiles": "^0.2.6",
"@backstage/plugin-graphiql": "^0.2.9",
"@backstage/plugin-jenkins": "^0.4.1",
@@ -27,12 +27,12 @@
"@backstage/plugin-kubernetes": "^0.4.2",
"@backstage/plugin-lighthouse": "^0.2.14",
"@backstage/plugin-newrelic": "^0.2.6",
"@backstage/plugin-org": "^0.3.10",
"@backstage/plugin-org": "^0.3.12",
"@backstage/plugin-pagerduty": "0.3.2",
"@backstage/plugin-register-component": "^0.2.12",
"@backstage/plugin-github-release-manager": "^0.1.1",
"@backstage/plugin-rollbar": "^0.3.3",
"@backstage/plugin-scaffolder": "^0.8.0",
"@backstage/plugin-scaffolder": "^0.8.2",
"@backstage/plugin-search": "^0.3.4",
"@backstage/plugin-sentry": "^0.3.8",
"@backstage/plugin-tech-radar": "^0.3.8",
@@ -43,10 +43,10 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.12",
"@roadiehq/backstage-plugin-buildkite": "^0.1.3",
"@roadiehq/backstage-plugin-github-insights": "^0.3.2",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.7.6",
"@roadiehq/backstage-plugin-travis-ci": "^0.4.5",
"@roadiehq/backstage-plugin-buildkite": "^1.0.0",
"@roadiehq/backstage-plugin-github-insights": "^1.0.0",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.0",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -58,7 +58,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.9",
"@backstage/test-utils": "^0.1.10",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+10 -3
View File
@@ -27,7 +27,11 @@ import {
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import {
CatalogImportPage,
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import {
CostInsightsLabelDataflowInstructionsPage,
CostInsightsPage,
@@ -48,8 +52,8 @@ import React from 'react';
import { hot } from 'react-hot-loader/root';
import { Navigate, Route } from 'react-router';
import { apis } from './apis';
import { EntityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import { entityPage } from './components/catalog/EntityPage';
import { providers } from './identityProviders';
import * as plugins from './plugins';
@@ -82,6 +86,9 @@ const app = createApp({
bind(explorePlugin.externalRoutes, {
catalogEntity: catalogPlugin.routes.catalogEntity,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
@@ -96,7 +103,7 @@ const routes = (
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
<EntityPage />
{entityPage}
</Route>
<Route path="/catalog-import" element={<CatalogImportPage />} />
<Route path="/docs" element={<TechdocsPage />} />
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { CICDSwitcher } from './EntityPage';
import { UrlPatternDiscovery, ApiProvider, ApiRegistry } from '@backstage/core';
import {
buildKiteApiRef,
BuildkiteApi,
} from '@roadiehq/backstage-plugin-buildkite';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { EntityLayout } from '@backstage/plugin-catalog';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { cicdContent } from './EntityPage';
import { githubActionsApiRef } from '@backstage/plugin-github-actions';
describe('EntityPage Test', () => {
const entity = {
@@ -29,7 +29,7 @@ describe('EntityPage Test', () => {
metadata: {
name: 'ExampleComponent',
annotations: {
'buildkite.com/project-slug': 'exampleProject/examplePipeline',
'github.com/project-slug': 'example/project',
},
},
spec: {
@@ -39,24 +39,36 @@ describe('EntityPage Test', () => {
},
};
const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com');
const mockedApi = {
listWorkflowRuns: jest.fn().mockResolvedValue([]),
getWorkflow: jest.fn(),
getWorkflowRun: jest.fn(),
reRunWorkflow: jest.fn(),
downloadJobLogsForWorkflowRun: jest.fn(),
} as jest.Mocked<typeof githubActionsApiRef.T>;
const apis = ApiRegistry.from([
[buildKiteApiRef, new BuildkiteApi({ discoveryApi })],
]);
const apis = ApiRegistry.with(githubActionsApiRef, mockedApi);
describe('CICDSwitcher Test', () => {
it('Should render Buildkite View', async () => {
const renderedComponent = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<CICDSwitcher entity={entity} />
</ApiProvider>,
),
describe('cicdContent', () => {
it('Should render GitHub Actions View', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/ci-cd" title="CI-CD">
{cicdContent}
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</ApiProvider>,
);
expect(
renderedComponent.getByText(/exampleProject\/examplePipeline/),
).toBeInTheDocument();
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
await expect(
rendered.findByText('No Workflow Data'),
).resolves.toBeInTheDocument();
expect(rendered.getByText('Create new Workflow')).toBeInTheDocument();
});
});
});
+390 -451
View File
@@ -14,174 +14,96 @@
* limitations under the License.
*/
import {
ApiEntity,
DomainEntity,
Entity,
GroupEntity,
SystemEntity,
UserEntity,
} from '@backstage/catalog-model';
import React, { ReactNode, useMemo, useState } from 'react';
import BadgeIcon from '@material-ui/icons/CallToAction';
import { EmptyState } from '@backstage/core';
import {
ApiDefinitionCard,
ConsumedApisCard,
ConsumingComponentsCard,
EntityApiDefinitionCard,
EntityConsumingComponentsCard,
EntityHasApisCard,
ProvidedApisCard,
ProvidingComponentsCard,
EntityProvidingComponentsCard,
EntityProvidedApisCard,
EntityConsumedApisCard,
} from '@backstage/plugin-api-docs';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
AboutCard,
EntityAboutCard,
EntityHasComponentsCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLayout,
EntityLinksCard,
EntityPageLayout,
EntitySystemDiagramCard,
EntitySwitch,
isComponentType,
isKind,
} from '@backstage/plugin-catalog';
import { EntityProvider, useEntity } from '@backstage/plugin-catalog-react';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
Router as CircleCIRouter,
EntityCircleCIContent,
isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import {
isPluginApplicableToEntity as isCloudbuildAvailable,
Router as CloudbuildRouter,
EntityCloudbuildContent,
isCloudbuildAvailable,
} from '@backstage/plugin-cloudbuild';
import {
isPluginApplicableToEntity as isGitHubActionsAvailable,
RecentWorkflowRunsCard,
Router as GitHubActionsRouter,
EntityGithubActionsContent,
EntityRecentGithubActionsRunsCard,
isGithubActionsAvailable,
} from '@backstage/plugin-github-actions';
import {
isPluginApplicableToEntity as isJenkinsAvailable,
LatestRunCard as JenkinsLatestRunCard,
Router as JenkinsRouter,
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
isJenkinsAvailable,
} from '@backstage/plugin-jenkins';
import { Router as KafkaRouter } from '@backstage/plugin-kafka';
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
import { EntityKafkaContent } from '@backstage/plugin-kafka';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
import {
EmbeddedRouter as LighthouseRouter,
isPluginApplicableToEntity as isLighthouseAvailable,
LastLighthouseAuditCard,
EntityLastLighthouseAuditCard,
EntityLighthouseContent,
isLighthouseAvailable,
} from '@backstage/plugin-lighthouse';
import {
GroupProfileCard,
MembersListCard,
OwnershipCard,
UserProfileCard,
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
EntityUserProfileCard,
} from '@backstage/plugin-org';
import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
EntityPagerDutyCard,
isPagerDutyAvailable,
} from '@backstage/plugin-pagerduty';
import {
EntityRollbarContent,
isRollbarAvailable,
Router as RollbarRouter,
} from '@backstage/plugin-rollbar';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { EntitySentryContent } from '@backstage/plugin-sentry';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
import { EntityTodoContent } from '@backstage/plugin-todo';
import { Button, Grid } from '@material-ui/core';
import {
isPluginApplicableToEntity as isBuildkiteAvailable,
Router as BuildkiteRouter,
EntityBuildkiteContent,
isBuildkiteAvailable,
} from '@roadiehq/backstage-plugin-buildkite';
import {
isPluginApplicableToEntity as isGitHubAvailable,
LanguagesCard,
ReadMeCard,
ReleasesCard,
Router as GitHubInsightsRouter,
EntityGithubInsightsContent,
EntityGithubInsightsLanguagesCard,
EntityGithubInsightsReadmeCard,
EntityGithubInsightsReleasesCard,
isGithubInsightsAvailable,
} from '@roadiehq/backstage-plugin-github-insights';
import {
isPluginApplicableToEntity as isPullRequestsAvailable,
PullRequestsStatsCard,
Router as PullRequestsRouter,
EntityGithubPullRequestsContent,
EntityGithubPullRequestsOverviewCard,
isGithubPullRequestsAvailable,
} from '@roadiehq/backstage-plugin-github-pull-requests';
import {
isPluginApplicableToEntity as isTravisCIAvailable,
RecentTravisCIBuildsWidget,
Router as TravisCIRouter,
EntityTravisCIContent,
EntityTravisCIOverviewCard,
isTravisciAvailable,
} from '@roadiehq/backstage-plugin-travis-ci';
import React, { ReactNode, useMemo, useState } from 'react';
import BadgeIcon from '@material-ui/icons/CallToAction';
export 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 isJenkinsAvailable(entity):
return <JenkinsRouter entity={entity} />;
case isBuildkiteAvailable(entity):
return <BuildkiteRouter entity={entity} />;
case isCircleCIAvailable(entity):
return <CircleCIRouter entity={entity} />;
case isCloudbuildAvailable(entity):
return <CloudbuildRouter entity={entity} />;
case isTravisCIAvailable(entity):
return <TravisCIRouter entity={entity} />;
case isGitHubActionsAvailable(entity):
return <GitHubActionsRouter entity={entity} />;
default:
return (
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
);
}
};
const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
let content: ReactNode;
switch (true) {
case isJenkinsAvailable(entity):
content = <JenkinsLatestRunCard branch="master" variant="gridItem" />;
break;
case isTravisCIAvailable(entity):
content = <RecentTravisCIBuildsWidget entity={entity} />;
break;
case isGitHubActionsAvailable(entity):
content = (
<RecentWorkflowRunsCard entity={entity} limit={4} variant="gridItem" />
);
break;
default:
content = null;
}
if (!content) {
return null;
}
return (
<Grid item sm={6}>
{content}
</Grid>
);
};
export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => {
switch (true) {
case isRollbarAvailable(entity):
return <RollbarRouter entity={entity} />;
default:
return <SentryRouter entity={entity} />;
}
};
const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
const extraMenuItems = useMemo(() => {
@@ -196,9 +118,9 @@ const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
return (
<>
<EntityPageLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
<EntityLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
{props.children}
</EntityPageLayout>
</EntityLayout>
<EntityBadgesDialog
open={badgesDialogOpen}
onClose={() => setBadgesDialogOpen(false)}
@@ -207,350 +129,367 @@ const EntityPageLayoutWrapper = (props: { children?: React.ReactNode }) => {
);
};
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
export const cicdContent = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
<EntityJenkinsContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isBuildkiteAvailable}>
<EntityBuildkiteContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isCircleCIAvailable}>
<EntityCircleCIContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isCloudbuildAvailable}>
<EntityCloudbuildContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isTravisciAvailable}>
<EntityTravisCIContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
</EntitySwitch.Case>
</EntitySwitch>
);
const cicdCard = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
<Grid item sm={6}>
<EntityLatestJenkinsRunCard branch="master" variant="gridItem" />
</Grid>
</EntitySwitch.Case>
<EntitySwitch.Case if={isTravisciAvailable}>
<Grid item sm={6}>
<EntityTravisCIOverviewCard />
</Grid>
</EntitySwitch.Case>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<Grid item sm={6}>
<EntityRecentGithubActionsRunsCard limit={4} variant="gridItem" />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
);
const errorsContent = (
<EntitySwitch>
<EntitySwitch.Case if={isRollbarAvailable}>
<EntityRollbarContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EntitySentryContent />
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<AboutCard entity={entity} variant="gridItem" />
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
{isPagerDutyAvailable(entity) && (
<Grid item xs={12} md={6}>
<EntityProvider entity={entity}>
<PagerDutyCard />
</EntityProvider>
</Grid>
)}
<Grid item xs={12} md={6}>
<EntityLinksCard entity={entity} />
<EntitySwitch>
<EntitySwitch.Case if={isPagerDutyAvailable}>
<Grid item md={6}>
<EntityPagerDutyCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={4} sm={6}>
<EntityLinksCard />
</Grid>
<RecentCICDRunsSwitcher entity={entity} />
{isGitHubAvailable(entity) && (
<>
<Grid item xs={12} md={6}>
<LanguagesCard entity={entity} />
<ReleasesCard entity={entity} />
{cicdCard}
<EntitySwitch>
<EntitySwitch.Case if={e => Boolean(isGithubInsightsAvailable(e))}>
<Grid item md={6}>
<EntityGithubInsightsLanguagesCard />
<EntityGithubInsightsReleasesCard />
</Grid>
<Grid item xs={12} md={6}>
<ReadMeCard entity={entity} maxHeight={350} />
<Grid item md={6}>
<EntityGithubInsightsReadmeCard maxHeight={350} />
</Grid>
</>
)}
{isLighthouseAvailable(entity) && (
<Grid item xs={12} sm={4}>
<LastLighthouseAuditCard variant="gridItem" />
</Grid>
)}
{isPullRequestsAvailable(entity) && (
<Grid item xs={12} sm={4}>
<PullRequestsStatsCard entity={entity} />
</Grid>
)}
<Grid item xs={12} md={6}>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={isLighthouseAvailable}>
<Grid item sm={4}>
<EntityLastLighthouseAuditCard variant="gridItem" />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={e => Boolean(isGithubPullRequestsAvailable(e))}>
<Grid item sm={4}>
<EntityGithubPullRequestsOverviewCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
);
const ComponentApisContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<ProvidedApisCard entity={entity} />
</Grid>
<Grid item xs={12} md={6}>
<ConsumedApisCard entity={entity} />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ComponentApisContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kubernetes/*"
title="Kubernetes"
element={<KubernetesRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/pull-requests"
title="Pull Requests"
element={<PullRequestsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/code-insights"
title="Code Insights"
element={<GitHubInsightsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kafka/*"
title="Kafka"
element={<KafkaRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/lighthouse/*"
title="Lighthouse"
element={<LighthouseRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/kubernetes/*"
title="Kubernetes"
element={<KubernetesRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/pull-requests"
title="Pull Requests"
element={<PullRequestsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/code-insights"
title="Code Insights"
element={<GitHubInsightsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
<EntityLayout.Route path="/errors" title="Errors">
{errorsContent}
</EntityLayout.Route>
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ComponentOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/todos"
title="TODOs"
element={<EntityTodoContent />}
/>
</EntityPageLayoutWrapper>
);
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
case 'website':
return <WebsiteEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
<Grid item md={6}>
<AboutCard entity={entity} />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<ProvidingComponentsCard entity={entity} />
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
<Grid item md={6}>
<ConsumingComponentsCard entity={entity} />
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kafka" title="Kafka">
<EntityKafkaContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const websiteEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/lighthouse" title="Lighthouse">
<EntityLighthouseContent />
</EntityLayout.Route>
<EntityLayout.Route path="/errors" title="Errors">
{errorsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
<EntityLayout.Route path="/pull-requests" title="Pull Requests">
<EntityGithubPullRequestsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/code-insights" title="Code Insights">
<EntityGithubInsightsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const defaultEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
<EntityLayout.Route path="/todos" title="TODOs">
<EntityTodoContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const apiPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12}>
<ApiDefinitionCard apiEntity={entity} />
</Grid>
</Grid>
const userPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const ApiEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ApiOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/definition/*"
title="Definition"
element={<ApiDefinitionContent entity={entity as ApiEntity} />}
/>
</EntityPageLayoutWrapper>
const groupPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityGroupProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
<Grid item xs={12}>
<EntityMembersListCard />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
<EntitySystemDiagramCard />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<UserProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
</Grid>
const systemPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const UserEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<UserOverviewContent entity={entity as UserEntity} />}
/>
</EntityPageLayoutWrapper>
const domainPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<GroupProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12}>
<MembersListCard entity={entity} />
</Grid>
</Grid>
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<GroupOverviewContent entity={entity as GroupEntity} />}
/>
</EntityPageLayoutWrapper>
);
const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
);
const SystemEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<SystemOverviewContent entity={entity as SystemEntity} />}
/>
<EntityPageLayout.Content
path="/diagram/*"
title="Diagram"
element={<EntitySystemDiagramCard />}
/>
</EntityPageLayoutWrapper>
);
const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
);
const DomainEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayoutWrapper>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<DomainOverviewContent entity={entity as DomainEntity} />}
/>
</EntityPageLayoutWrapper>
);
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.kind?.toLocaleLowerCase('en-US')) {
case 'component':
return <ComponentEntityPage entity={entity} />;
case 'api':
return <ApiEntityPage entity={entity} />;
case 'group':
return <GroupEntityPage entity={entity} />;
case 'user':
return <UserEntityPage entity={entity} />;
case 'system':
return <SystemEntityPage entity={entity} />;
case 'domain':
return <DomainEntityPage entity={entity} />;
case 'location':
case 'resource':
case 'template':
default:
return <DefaultEntityPage entity={entity} />;
}
};
+4 -4
View File
@@ -27,19 +27,19 @@ export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
export { travisciPlugin } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests';
export { githubPullRequestsPlugin } from '@roadiehq/backstage-plugin-github-pull-requests';
export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects';
export { plugin as Kubernetes } from '@backstage/plugin-kubernetes';
export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild';
export { plugin as CostInsights } from '@backstage/plugin-cost-insights';
export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights';
export { githubInsightsPlugin } from '@roadiehq/backstage-plugin-github-insights';
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
export { plugin as UserSettings } from '@backstage/plugin-user-settings';
export { plugin as PagerDuty } from '@backstage/plugin-pagerduty';
export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite';
export { buildkitePlugin } from '@roadiehq/backstage-plugin-buildkite';
export { plugin as Search } from '@backstage/plugin-search';
export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
+8
View File
@@ -1,5 +1,13 @@
# @backstage/backend-common
## 0.6.2
### Patch Changes
- b779b5fee: Add UrlReader for Google Cloud Storage
- Updated dependencies [82c66b8cd]
- @backstage/config-loader@0.6.0
## 0.6.1
### Patch Changes
+6 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.6.1",
"version": "0.6.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,9 +31,10 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.4",
"@backstage/config-loader": "^0.5.1",
"@backstage/config-loader": "^0.6.0",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.0.12",
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.2.1",
@@ -56,6 +57,7 @@
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"raw-body": "^2.4.1",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"tar": "^6.0.5",
@@ -71,8 +73,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/test-utils": "^0.1.9",
"@backstage/cli": "^0.6.7",
"@backstage/test-utils": "^0.1.10",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -0,0 +1,94 @@
/*
* 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 { ConfigReader, JsonObject } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { ReadTreeResponseFactory } from './tree';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
import { UrlReaderPredicateTuple } from './types';
describe('GcsUrlReader', () => {
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
return GoogleGcsUrlReader.factory({
config: new ConfigReader(config),
logger: getVoidLogger(),
treeResponseFactory: ReadTreeResponseFactory.create({
config: new ConfigReader({}),
}),
});
};
it('does not create a reader without the googleGcs field', () => {
const entries = createReader({
integrations: {},
});
expect(entries).toHaveLength(0);
});
it('creates a reader with credentials correctly configured', () => {
const entries = createReader({
integrations: {
googleGcs: {
privateKey: '--- BEGIN KEY ---- fakekey --- END KEY ---',
clientEmail: 'someone@example.com',
},
},
});
expect(entries).toHaveLength(1);
});
it('creates a reader with default credentials provider', () => {
const entries = createReader({
integrations: {
googleGcs: {},
},
});
expect(entries).toHaveLength(1);
});
describe('predicates', () => {
const readers = createReader({
integrations: {
googleGcs: {},
},
});
const predicate = readers[0].predicate;
it('returns true for the correct google cloud storage host', () => {
expect(predicate(new URL('https://storage.cloud.google.com'))).toBe(true);
});
it('returns true for a url with the full path and the correct host', () => {
expect(
predicate(
new URL(
'https://storage.cloud.google.com/team1/service1/catalog-info.yaml',
),
),
).toBe(true);
});
it('returns false for the wrong hostname under cloud.google.com', () => {
expect(predicate(new URL('https://storage2.cloud.google.com'))).toBe(
false,
);
});
it('returns false for a partially correct host', () => {
expect(predicate(new URL('https://cloud.google.com'))).toBe(false);
});
it('returns false for a completely different host', () => {
expect(predicate(new URL('https://a.example.com/test'))).toBe(false);
});
});
});
@@ -0,0 +1,105 @@
/*
* 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 { Storage } from '@google-cloud/storage';
import {
ReaderFactory,
ReadTreeResponse,
SearchResponse,
UrlReader,
} from './types';
import getRawBody from 'raw-body';
import {
GoogleGcsIntegrationConfig,
readGoogleGcsIntegrationConfig,
} from '@backstage/integration';
const GOOGLE_GCS_HOST = 'storage.cloud.google.com';
const parseURL = (
url: string,
): { host: string; bucket: string; key: string } => {
const { host, pathname } = new URL(url);
if (host !== GOOGLE_GCS_HOST) {
throw new Error(`not a valid GCS URL: ${url}`);
}
const [, bucket, ...key] = pathname.split('/');
return {
host: host,
bucket,
key: key.join('/'),
};
};
export class GoogleGcsUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, logger }) => {
if (!config.has('integrations.googleGcs')) {
return [];
}
const gcsConfig = readGoogleGcsIntegrationConfig(
config.getConfig('integrations.googleGcs'),
);
let storage: Storage;
if (!gcsConfig.clientEmail || !gcsConfig.privateKey) {
logger.info(
'googleGcs credentials not found in config. Using default credentials provider.',
);
storage = new Storage();
} else {
storage = new Storage({
credentials: {
client_email: gcsConfig.clientEmail || undefined,
private_key: gcsConfig.privateKey || undefined,
},
});
}
const reader = new GoogleGcsUrlReader(gcsConfig, storage);
const predicate = (url: URL) => url.host === GOOGLE_GCS_HOST;
return [{ reader, predicate }];
};
constructor(
private readonly integration: GoogleGcsIntegrationConfig,
private readonly storage: Storage,
) {}
async read(url: string): Promise<Buffer> {
try {
const { bucket, key } = parseURL(url);
return await getRawBody(
this.storage.bucket(bucket).file(key).createReadStream(),
);
} catch (error) {
throw new Error(`unable to read gcs file from ${url}, ${error}`);
}
}
async readTree(): Promise<ReadTreeResponse> {
throw new Error('GcsUrlReader does not implement readTree');
}
async search(): Promise<SearchResponse> {
throw new Error('GcsUrlReader does not implement search');
}
toString() {
const key = this.integration.privateKey;
return `googleGcs{host=${GOOGLE_GCS_HOST},authed=${Boolean(key)}}`;
}
}
@@ -24,6 +24,7 @@ import { GithubUrlReader } from './GithubUrlReader';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { FetchUrlReader } from './FetchUrlReader';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
type CreateOptions = {
/** Root config object */
@@ -70,6 +71,7 @@ export class UrlReaders {
BitbucketUrlReader.factory,
GithubUrlReader.factory,
GitlabUrlReader.factory,
GoogleGcsUrlReader.factory,
FetchUrlReader.factory,
]),
});
+3 -1
View File
@@ -16,11 +16,13 @@ WORKDIR /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 packages/backend/dist/skeleton.tar.gz ./
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
# Then copy the rest of the backend bundle, along with any other files we might want.
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend", "--config", "app-config.yaml"]
+7
View File
@@ -1,5 +1,12 @@
# @backstage/catalog-model
## 0.7.6
### Patch Changes
- 97b60de98: Added `stringifyEntityRef`, which always creates a string representation of an entity reference. Also deprecated `serializeEntityRef`, as `stringifyEntityRef` should be used instead.
- 98dd5da71: Add support for multiple links to post-scaffold task summary page
## 0.7.5
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.7.5",
"version": "0.7.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -39,7 +39,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.4",
"@backstage/cli": "^0.6.7",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -36,6 +36,7 @@ describe('ComponentV1alpha1Validator', () => {
subcomponentOf: 'monolith',
providesApis: ['api-0'],
consumesApis: ['api-0'],
dependsOn: ['resource:resource-0', 'component:component-0'],
system: 'system',
},
};
@@ -160,6 +161,26 @@ describe('ComponentV1alpha1Validator', () => {
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -34,6 +34,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
subcomponentOf?: string;
providesApis?: string[];
consumesApis?: string[];
dependsOn?: string[];
system?: string;
};
}
@@ -32,6 +32,7 @@ describe('ResourceV1alpha1Validator', () => {
spec: {
type: 'database',
owner: 'me',
dependsOn: ['component:component-0', 'resource:resource-0'],
system: 'system',
},
};
@@ -86,6 +87,26 @@ describe('ResourceV1alpha1Validator', () => {
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing dependsOn', async () => {
delete (entity as any).spec.dependsOn;
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty dependsOn', async () => {
(entity as any).spec.dependsOn = [''];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('rejects undefined dependsOn', async () => {
(entity as any).spec.dependsOn = [undefined];
await expect(validator.check(entity)).rejects.toThrow(/dependsOn/);
});
it('accepts no dependsOn', async () => {
(entity as any).spec.dependsOn = [];
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts missing system', async () => {
delete (entity as any).spec.system;
await expect(validator.check(entity)).resolves.toBe(true);
@@ -30,6 +30,7 @@ export interface ResourceEntityV1alpha1 extends Entity {
spec: {
type: string;
owner: string;
dependsOn?: string[];
system?: string;
};
}
@@ -84,6 +84,14 @@
"type": "string",
"minLength": 1
}
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the component depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
@@ -47,6 +47,14 @@
"examples": ["artist-relations-team", "user:john.johnson"],
"minLength": 1
},
"dependsOn": {
"type": "array",
"description": "An array of references to other entities that the resource depends on to function.",
"items": {
"type": "string",
"minLength": 1
}
},
"system": {
"type": "string",
"description": "An entity reference to the system that the resource belongs to.",
+7
View File
@@ -1,5 +1,12 @@
# @backstage/cli
## 0.6.7
### Patch Changes
- Updated dependencies [82c66b8cd]
- @backstage/config-loader@0.6.0
## 0.6.6
### Patch Changes
+1 -1
View File
@@ -11,7 +11,7 @@
"incremental": true,
"isolatedModules": true,
"jsx": "react",
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"],
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020"],
"module": "ESNext",
"moduleResolution": "node",
"noEmit": false,
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.6.6",
"version": "0.6.7",
"private": false,
"publishConfig": {
"access": "public"
@@ -32,7 +32,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.4",
"@backstage/config-loader": "^0.5.1",
"@backstage/config-loader": "^0.6.0",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -116,11 +116,11 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.6.0",
"@backstage/backend-common": "^0.6.2",
"@backstage/config": "^0.1.4",
"@backstage/core": "^0.7.3",
"@backstage/core": "^0.7.4",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.9",
"@backstage/test-utils": "^0.1.10",
"@backstage/theme": "^0.2.5",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
+1 -1
View File
@@ -74,7 +74,7 @@ export const transforms = (options: TransformOptions): Transforms => {
/\.jpe?g$/,
/\.png$/,
/\.frag/,
{ test: /\.svg/, not: [/\.icon\.svg/] },
{ and: [/\.svg/, { not: [/\.icon\.svg/] }] },
/\.xml/,
],
loader: require.resolve('url-loader'),
+16
View File
@@ -1,5 +1,21 @@
# @backstage/config-loader
## 0.6.0
### Minor Changes
- 82c66b8cd: Fix bug where `${...}` was not being escaped to `${...}`
Add support for environment variable substitution in `$include`, `$file` and
`$env` transform values.
- This change allows for including dynamic paths, such as environment specific
secrets by using the same environment variable substitution (`${..}`) already
supported outside of the various include transforms.
- If you are currently using the syntax `${...}` in your include transform values,
you will need to escape the substitution by using `${...}` instead to maintain
the same behavior.
## 0.5.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.5.1",
"version": "0.6.0",
"private": false,
"publishConfig": {
"access": "public",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/core-api
## 0.2.16
### Patch Changes
- 1279a3325: Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading
errors visible to the user.
- 4a4681b1b: Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error.
- b051e770c: Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail.
## 0.2.15
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.2.15",
"version": "0.2.16",
"private": false,
"publishConfig": {
"access": "public",
@@ -42,8 +42,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.6",
"@backstage/test-utils": "^0.1.9",
"@backstage/cli": "^0.6.7",
"@backstage/test-utils": "^0.1.10",
"@backstage/test-utils-core": "^0.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+14
View File
@@ -1,5 +1,19 @@
# @backstage/core
## 0.7.4
### Patch Changes
- 1279a3325: Introduce a `load-chunk` step in the `BootErrorPage` to show make chunk loading
errors visible to the user.
- 4a4681b1b: Improved error messaging for routable extension errors, making it easier to identify the component and mount point that caused the error.
- b051e770c: Fixed a bug with `useRouteRef` where navigating from routes beneath a mount point would often fail.
- 98dd5da71: Add support for multiple links to post-scaffold task summary page
- Updated dependencies [1279a3325]
- Updated dependencies [4a4681b1b]
- Updated dependencies [b051e770c]
- @backstage/core-api@0.2.16
## 0.7.3
### Patch Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.7.3",
"version": "0.7.4",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.4",
"@backstage/core-api": "^0.2.15",
"@backstage/core-api": "^0.2.16",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.5",
"@material-ui/core": "^4.11.0",
@@ -69,8 +69,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.6",
"@backstage/test-utils": "^0.1.9",
"@backstage/cli": "^0.6.7",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
@@ -0,0 +1,127 @@
/*
* Copyright 2021 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 {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Typography,
} from '@material-ui/core';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import CloseIcon from '@material-ui/icons/Close';
import React, { useState } from 'react';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500],
},
}),
);
export default {
title: 'Layout/Dialog',
component: Dialog,
};
export const Default = () => {
const [open, setOpen] = useState(false);
const classes = useStyles();
const openDialog = () => {
setOpen(true);
};
const closeDialog = () => {
setOpen(false);
};
const dialogContent = () => {
return (
<>
<Typography>
This is an example of how to use the Dialog component.
</Typography>
<Typography>
This component is used whenever confirmation of some sort is needed,
such as:
</Typography>
<ul>
<li>
<Typography>
Consent to sensitive matters like GDPR, access, etc;
</Typography>
</li>
<li>
<Typography>
Save, submit, cancel after a form is completed;
</Typography>
</li>
<li>
<Typography>Alert message;</Typography>
</li>
<li>
<Typography>Buttons are optional.</Typography>
</li>
</ul>
<Typography>
The color for the secondary button is the same as the primary.
</Typography>
<pre>color="primary"</pre>
</>
);
};
return (
<>
<Button color="primary" variant="contained" onClick={openDialog}>
Open Dialog
</Button>
<Dialog
open={open}
onClose={closeDialog}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<DialogTitle id="dialog-title">
Dialog Box Title
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={closeDialog}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>{dialogContent()}</DialogContent>
<DialogActions>
<Button color="primary" onClick={closeDialog}>
Secondary action
</Button>
<Button color="primary" onClick={closeDialog}>
Primary action
</Button>
</DialogActions>
</Dialog>
</>
);
};
@@ -35,12 +35,10 @@ const useIconStyles = makeStyles(theme => ({
justifyItems: 'center',
gridGap: 4,
textAlign: 'center',
'&:active': {
cursor: 'grabbing',
},
},
disabled: {
color: 'gray',
cursor: 'default',
},
primary: {
color: theme.palette.primary.main,
@@ -58,6 +58,7 @@ type SetQueryParams<T> = (params: T) => void;
export function useQueryParamState<T>(
stateName: string,
debounceTime: number = 100,
): [T | undefined, SetQueryParams<T>] {
const navigate = useNavigate();
const location = useLocation();
@@ -85,7 +86,7 @@ export function useQueryParamState<T>(
navigate({ ...location, search: `?${queryString}` }, { replace: true });
}
},
100,
debounceTime,
[queryParamState],
);
@@ -45,6 +45,46 @@ export const HomeWithSubtitle = () => (
</Header>
);
export const Apis = () => (
<Page themeId="apis">
<Header title="API catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const Grpc = () => (
<Page themeId="grpc">
<Header title="Grpc catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const AsyncApi = () => (
<Page themeId="asyncapi">
<Header title="Async API catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const Graphql = () => (
<Page themeId="graphql">
<Header title="GraphQL API catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const OpenApi = () => (
<Page themeId="openapi">
<Header title="OpenAPI catalogue" type="tool">
{labels}
</Header>
</Page>
);
export const Tool = () => (
<Page themeId="tool">
<Header title="Stand-alone tool" type="tool">
+211
View File
@@ -1,5 +1,216 @@
# @backstage/create-app
## 0.3.17
### Patch Changes
- 3e7de08af: **Fully migrated the template to the new composability API**
The `create-app` template is now fully migrated to the new composability API, see [Composability System Migration Documentation](https://backstage.io/docs/plugins/composability) for explanations and more details. The final change which is now done was to migrate the `EntityPage` from being a component built on top of the `EntityPageLayout` and several more custom components, to an element tree built with `EntitySwitch` and `EntityLayout`.
To apply this change to an existing plugin, it is important that all plugins that you are using have already been migrated. In this case the most crucial piece is that no entity page cards of contents may require the `entity` prop, and they must instead consume the entity from context using `useEntity`.
Since this change is large with a lot of repeated changes, we'll describe a couple of common cases rather than the entire change. If your entity pages are unchanged from the `create-app` template, you can also just bring in the latest version directly from the [template itself](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx).
The first step of the change is to change the `packages/app/src/components/catalog/EntityPage.tsx` export to `entityPage` rather than `EntityPage`. This will require an update to `App.tsx`, which is the only change we need to do outside of `EntityPage.tsx`:
```diff
-import { EntityPage } from './components/catalog/EntityPage';
+import { entityPage } from './components/catalog/EntityPage';
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
- <EntityPage />
+ {entityPage}
</Route>
```
The rest of the changes happen within `EntityPage.tsx`, and can be split into two broad categories, updating page components, and updating switch components.
#### Migrating Page Components
Let's start with an example of migrating a user page component. The following is the old code in the template:
```tsx
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<UserProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
</Grid>
);
const UserEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<UserOverviewContent entity={entity as UserEntity} />}
/>
</EntityPageLayout>
);
```
There's the main `UserEntityPage` component, and the `UserOverviewContent` component. Let's start with migrating the page contents, which we do by rendering an element rather than creating a component, as well as replace the cards with their new composability compatible variants. The new cards and content components can be identified by the `Entity` prefix.
```tsx
const userOverviewContent = (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
);
```
Now let's migrate the page component, again by converting it into a rendered element instead of a component, as well as replacing the use of `EntityPageLayout` with `EntityLayout`.
```tsx
const userPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{userOverviewContent}
</EntityLayout.Route>
</EntityLayout>
);
```
At this point the `userPage` is quite small, so throughout this migration we have inlined the page contents for all pages. This is an optional step, but may help reduce noise. The final page now looks like this:
```tsx
const userPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
```
#### Migrating Switch Components
Switch components were used to select what entity page components or cards to render, based on for example the kind of entity. For this example we'll focus on the root `EntityPage` switch component, but the process is the same for example for the CI/CD switcher.
The old `EntityPage` looked like this:
```tsx
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.kind?.toLocaleLowerCase('en-US')) {
case 'component':
return <ComponentEntityPage entity={entity} />;
case 'api':
return <ApiEntityPage entity={entity} />;
case 'group':
return <GroupEntityPage entity={entity} />;
case 'user':
return <UserEntityPage entity={entity} />;
case 'system':
return <SystemEntityPage entity={entity} />;
case 'domain':
return <DomainEntityPage entity={entity} />;
case 'location':
case 'resource':
case 'template':
default:
return <DefaultEntityPage entity={entity} />;
}
};
```
In order to migrate to the composability API, we need to make this an element instead of a component, which means we're unable to keep the switch statement as is. To help with this, the catalog plugin provides an `EntitySwitch` component, which functions similar to a regular `switch` statement, which the first match being the one that is rendered. The catalog plugin also provides a number of built-in filter functions to use, such as `isKind` and `isComponentType`.
To migrate the `EntityPage`, we convert the `switch` statement into an `EntitySwitch` element, and each `case` statement into an `EntitySwitch.Case` element. We also move over to use our new element version of the page components, with the result looking like this:
```tsx
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
```
Another example is the `ComponentEntityPage`, which is migrated from this:
```tsx
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
case 'website':
return <WebsiteEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
```
To this:
```tsx
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
```
Note that if you want to conditionally render some piece of content, you can omit the default `EntitySwitch.Case`. If no case is matched in an `EntitySwitch`, nothing will be rendered.
- Updated dependencies [802b41b65]
- Updated dependencies [2b2b31186]
- Updated dependencies [1279a3325]
- Updated dependencies [4a4681b1b]
- Updated dependencies [97b60de98]
- Updated dependencies [3f96a9d5a]
- Updated dependencies [b051e770c]
- Updated dependencies [f9c75f7a9]
- Updated dependencies [ae6250ce3]
- Updated dependencies [98dd5da71]
- Updated dependencies [b779b5fee]
- @backstage/plugin-scaffolder-backend@0.9.5
- @backstage/plugin-auth-backend@0.3.8
- @backstage/core@0.7.4
- @backstage/catalog-model@0.7.6
- @backstage/plugin-scaffolder@0.8.2
- @backstage/plugin-catalog-import@0.5.2
- @backstage/test-utils@0.1.10
- @backstage/plugin-catalog@0.5.3
- @backstage/backend-common@0.6.2
- @backstage/cli@0.6.7
- @backstage/plugin-app-backend@0.3.11
## 0.3.16
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.3.16",
"version": "0.3.17",
"private": false,
"publishConfig": {
"access": "public"
@@ -22,6 +22,10 @@
"create-plugin": "backstage-cli create-plugin --scope internal --no-private",
"remove-plugin": "backstage-cli remove-plugin"
},
"resolutions": {
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
},
"workspaces": {
"packages": [
"packages/*",
@@ -12,14 +12,14 @@ import {
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import {CatalogImportPage, catalogImportPlugin} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import { TechdocsPage } from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import { apis } from './apis';
import { EntityPage } from './components/catalog/EntityPage';
import { entityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import * as plugins from './plugins';
@@ -33,6 +33,9 @@ const app = createApp({
bind(apiDocsPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
},
});
@@ -47,7 +50,7 @@ const routes = (
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
<EntityPage />
{entityPage}
</Route>
<Route path="/docs" element={<TechdocsPage />} />
<Route path="/create" element={<ScaffolderPage />} />
@@ -15,308 +15,248 @@
*/
import React from 'react';
import { Button, Grid } from '@material-ui/core';
import {
ApiEntity,
DomainEntity,
Entity,
GroupEntity,
SystemEntity,
UserEntity,
} from '@backstage/catalog-model';
import { EmptyState } from '@backstage/core';
import {
ApiDefinitionCard,
ConsumedApisCard,
ConsumingComponentsCard,
EntityApiDefinitionCard,
EntityConsumedApisCard,
EntityConsumingComponentsCard,
EntityHasApisCard,
ProvidedApisCard,
ProvidingComponentsCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
AboutCard,
EntityAboutCard,
EntitySystemDiagramCard,
EntityHasComponentsCard,
EntityHasSystemsCard,
EntityPageLayout,
EntityLayout,
EntitySwitch,
isComponentType,
isKind,
} from '@backstage/plugin-catalog';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
isPluginApplicableToEntity as isGitHubActionsAvailable,
Router as GitHubActionsRouter,
isGithubActionsAvailable,
EntityGithubActionsContent,
} from '@backstage/plugin-github-actions';
import {
GroupProfileCard,
MembersListCard,
OwnershipCard,
UserProfileCard,
EntityUserProfileCard,
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
} from '@backstage/plugin-org';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
const cicdContent = (
// This is 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} />;
default:
return (
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
);
}
};
<EntitySwitch>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</EntitySwitch.Case>
const OverviewContent = ({ entity }: { entity: Entity }) => (
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
<EntityAboutCard variant="gridItem" />
</Grid>
</Grid>
);
const ComponentApisContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<ProvidedApisCard entity={entity} />
</Grid>
<Grid item md={6}>
<ConsumedApisCard entity={entity} />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ComponentApisContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
export const ComponentEntityPage = ({ entity }: { entity: Entity }) => {
switch (entity?.spec?.type) {
case 'service':
return <ServiceEntityPage entity={entity} />;
case 'website':
return <WebsiteEntityPage entity={entity} />;
default:
return <DefaultEntityPage entity={entity} />;
}
};
const ApiOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3}>
<Grid item md={6}>
<AboutCard entity={entity} />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<ProvidingComponentsCard entity={entity} />
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
<Grid item md={6}>
<ConsumingComponentsCard entity={entity} />
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
</EntityLayout>
);
const websiteEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
</EntityLayout>
);
const defaultEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
</EntityLayout>
);
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const apiPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12}>
<ApiDefinitionCard apiEntity={entity} />
</Grid>
</Grid>
const userPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const ApiEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<ApiOverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/definition/*"
title="Definition"
element={<ApiDefinitionContent entity={entity as ApiEntity} />}
/>
</EntityPageLayout>
const groupPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<EntityGroupProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
<Grid item xs={12}>
<EntityMembersListCard />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
<EntitySystemDiagramCard />
</EntityLayout.Route>
</EntityLayout>
);
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<UserProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
</Grid>
const systemPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const UserEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<UserOverviewContent entity={entity as UserEntity} />}
/>
</EntityPageLayout>
const domainPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<GroupProfileCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<OwnershipCard entity={entity} variant="gridItem" />
</Grid>
<Grid item xs={12}>
<MembersListCard entity={entity} />
</Grid>
</Grid>
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<GroupOverviewContent entity={entity as GroupEntity} />}
/>
</EntityPageLayout>
);
const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
</Grid>
);
const SystemEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<SystemOverviewContent entity={entity as SystemEntity} />}
/>
</EntityPageLayout>
);
const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<AboutCard entity={entity} variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
);
const DomainEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
path="/*"
title="Overview"
element={<DomainOverviewContent entity={entity as DomainEntity} />}
/>
</EntityPageLayout>
);
export const EntityPage = () => {
const { entity } = useEntity();
switch (entity?.kind?.toLocaleLowerCase('en-US')) {
case 'component':
return <ComponentEntityPage entity={entity} />;
case 'api':
return <ApiEntityPage entity={entity} />;
case 'group':
return <GroupEntityPage entity={entity} />;
case 'user':
return <UserEntityPage entity={entity} />;
case 'system':
return <SystemEntityPage entity={entity} />;
case 'domain':
return <DomainEntityPage entity={entity} />;
case 'location':
case 'resource':
case 'template':
default:
return <DefaultEntityPage entity={entity} />;
}
};
@@ -16,11 +16,13 @@ WORKDIR /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 packages/backend/dist/skeleton.tar.gz ./
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
# Then copy the rest of the backend bundle, along with any other files we might want.
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend", "--config", "app-config.yaml"]
+14
View File
@@ -149,5 +149,19 @@ export interface Config {
*/
baseUrl?: string;
}>;
/** Integration configuration for Google Cloud Storage */
googleGcs?: {
/**
* Service account email used to authenticate requests.
* @visibility backend
*/
clientEmail?: string;
/**
* Service account private key used to authenticate requests.
* @visibility secret
*/
privateKey?: string;
};
};
}
+3 -3
View File
@@ -37,9 +37,9 @@
"luxon": "^1.25.0"
},
"devDependencies": {
"@backstage/cli": "^0.6.4",
"@backstage/config-loader": "^0.5.1",
"@backstage/test-utils": "^0.1.7",
"@backstage/cli": "^0.6.7",
"@backstage/config-loader": "^0.6.0",
"@backstage/test-utils": "^0.1.10",
"@types/jest": "^26.0.7",
"@types/luxon": "^1.25.0",
"msw": "^0.21.2"
@@ -0,0 +1,45 @@
/*
* 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 { Config, ConfigReader } from '@backstage/config';
import {
GoogleGcsIntegrationConfig,
readGoogleGcsIntegrationConfig,
} from './config';
describe('readGoogleGcsIntegrationConfig', () => {
function buildConfig(data: Partial<GoogleGcsIntegrationConfig>): Config {
return new ConfigReader(data);
}
it('reads all values', () => {
const output = readGoogleGcsIntegrationConfig(
buildConfig({
privateKey: 'fake-key',
clientEmail: 'someone@example.com',
}),
);
expect(output).toEqual({
privateKey: 'fake-key',
clientEmail: 'someone@example.com',
});
});
it('does not fail when config is not set', () => {
const output = readGoogleGcsIntegrationConfig(buildConfig({}));
expect(output).toEqual({});
});
});
@@ -0,0 +1,53 @@
/*
* 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 { Config } from '@backstage/config';
/**
* The configuration parameters for a single Google Cloud Storage provider.
*/
export type GoogleGcsIntegrationConfig = {
/**
* Service account email used to authenticate requests.
*/
clientEmail?: string;
/**
* Service account private key used to authenticate requests.
*/
privateKey?: string;
};
/**
* Reads a single Google GCS integration config.
*
* @param config The config object of a single integration
*/
export function readGoogleGcsIntegrationConfig(
config: Config,
): GoogleGcsIntegrationConfig {
if (!config) {
return {};
}
if (!config.has('clientEmail') && !config.has('privateKey')) {
return {};
}
const privateKey = config.getString('privateKey').split('\\n').join('\n');
const clientEmail = config.getString('clientEmail');
return { clientEmail: clientEmail, privateKey: privateKey };
}
@@ -13,18 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityName } from '@backstage/catalog-model';
/**
* Using the path of the TechDocs page URL, return a structured EntityName type object with namespace,
* kind and name of the Entity.
* @param {string} path Example: default/Component/documented-component
*/
export const getEntityNameFromUrlPath = (path: string): EntityName => {
const [namespace, kind, name] = path.split('/');
return {
namespace,
kind,
name,
};
};
export { readGoogleGcsIntegrationConfig } from './config';
export type { GoogleGcsIntegrationConfig } from './config';
+1
View File
@@ -18,6 +18,7 @@ export * from './azure';
export * from './bitbucket';
export * from './github';
export * from './gitlab';
export * from './googleGcs';
export { defaultScmResolveUrl } from './helpers';
export { ScmIntegrations } from './ScmIntegrations';
export type {
+10
View File
@@ -1,5 +1,15 @@
# @backstage/test-utils
## 0.1.10
### Patch Changes
- ae6250ce3: Remove unnecessary wrapping of elements rendered by `wrapInTestApp` and `renderInTestApp`, which was breaking mount discovery.
- Updated dependencies [1279a3325]
- Updated dependencies [4a4681b1b]
- Updated dependencies [b051e770c]
- @backstage/core-api@0.2.16
## 0.1.9
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.9",
"version": "0.1.10",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-api": "^0.2.14",
"@backstage/core-api": "^0.2.16",
"@backstage/test-utils-core": "^0.1.1",
"@backstage/theme": "^0.2.3",
"@material-ui/core": "^4.11.0",
@@ -45,7 +45,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/cli": "^0.6.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
+1
View File
@@ -63,4 +63,5 @@ export const pageTheme: Record<string, PageTheme> = {
library: genPageTheme(colorVariants.rubyRed, shapes.wave),
other: genPageTheme(colorVariants.darkGrey, shapes.wave),
app: genPageTheme(colorVariants.toastyOrange, shapes.wave),
apis: genPageTheme(colorVariants.eveningSea, shapes.wave2),
};
+140 -6
View File
@@ -1,7 +1,5 @@
# API Documentation
WORK IN PROGRESS
This is an extension for the catalog plugin that provides components to discover and display API entities.
APIs define the interface between components, see the [system model](https://backstage.io/docs/features/software-catalog/system-model) for details.
They are defined in machine readable formats and provide a human readable documentation.
@@ -23,9 +21,145 @@ Other formats are displayed as plain text, but this can easily be extended.
To fill the catalog with APIs, [provide entities of kind API](https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api).
To link that a component provides or consumes an API, see the [`providesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional) and [`consumesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional) properties on the Component kind.
## Implementing OAuth 2 Authorization Code flow with Swagger UI
## Getting Started
### Adding `oauth2-redirect.html` to support OAuth2 `redirect_uri` route
> The plugin is already added when using `npx @backstage/create-app` so you can skip these steps.
1. Install the API docs plugin
```bash
# packages/app
yarn add @backstage/plugin-api-docs
```
2. Add the plugin to the app:
```ts
// packages/app/src/plugins.ts
export { apiDocsPlugin } from '@backstage/plugin-api-docs';
```
<Route path="/api-docs" element={<ApiExplorerPage />} />
3. Register the `ApiExplorerPage` at the `/api-docs` path:
```tsx
// packages/app/src/App.tsx
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
<Route path="/api-docs" element={<ApiExplorerPage />} />;
```
4. Add one of the provided widgets to the EntityPage:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import {
EntityAboutCard,
EntityApiDefinitionCard,
EntityConsumingComponentsCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
const apiPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
// ...
export const entityPage = (
<EntitySwitch>
// ...
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
// ...
</EntitySwitch>
);
```
There are other components to discover in [`./src/components`](./src/components) that are also added by the default app.
## Customizations
### Custom API Renderings
You can add support for additional API types by providing a custom implementation for the `apiDocsConfigRef`.
You can also use this to override the rendering of one of the already supported types.
This is an example with a made-up renderer for SQL schemas:
```tsx
// packages/app/src/apis.tsx
import { ApiEntity } from '@backstage/catalog-model';
import {
ApiDefinitionWidget,
apiDocsConfigRef,
defaultDefinitionWidgets,
} from '@backstage/plugin-api-docs';
import { SqlRenderer } from '...';
// ...
export const apis: AnyApiFactory[] = [
// ...
createApiFactory({
api: apiDocsConfigRef,
deps: {},
factory: () => {
// load the default widgets
const definitionWidgets = defaultDefinitionWidgets();
return {
getApiDefinitionWidget: (apiEntity: ApiEntity) => {
// custom rendering for sql
if (apiEntity.spec.type === 'sql') {
return {
type: 'sql',
title: 'SQL',
component: definition => <SqlRenderer definition={definition} />,
} as ApiDefinitionWidget;
}
// fallback to the defaults
return definitionWidgets.find(d => d.type === apiEntity.spec.type);
},
};
},
}),
];
```
### Implementing OAuth 2 Authorization Code flow with Swagger UI
#### Adding `oauth2-redirect.html` to support OAuth2 `redirect_uri` route
The Swagger UI package by expects to have a route to `/oauth2-redirect.html` which processes
the redirect callback for the OAuth2 Authorization Code flow, however, this file is not installed
@@ -34,7 +168,7 @@ by this plugin.
Grab a copy of [oauth2-redirect.html](https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html)
and put it in the `app/public/` directory in order to enable Swagger UI to complete this redirection.
### Configuring your OAuth2 Client
#### Configuring your OAuth2 Client
You'll need to make sure your OAuth2 client has been registered in your OAuth2 Authentication Server (AS)
with the appropriate `redirect_uris`, `scopes` and `grant_types`. For example, if your AS supports
@@ -62,7 +196,7 @@ The above `redirect_uris` are:
- Local Backstage app development: `http://localhost:3000/oauth2-redirect.html`
- Backstage app production: `https://<yourhost>/oauth2-redirect.html`
### Configuring OAuth2 in your OpenAPI 3.0 schema
#### Configuring OAuth2 in your OpenAPI 3.0 schema
To configure [OAuth 2 Authorization Code](https://swagger.io/docs/specification/authentication/oauth2/) flow
in your OpenAPI 3.0 schema you'll need something like this snippet:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 334 KiB

+3 -3
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@asyncapi/react-component": "^0.19.2",
"@backstage/catalog-model": "^0.7.5",
"@backstage/core": "^0.7.3",
"@backstage/core": "^0.7.4",
"@backstage/plugin-catalog-react": "^0.1.4",
"@backstage/theme": "^0.2.5",
"@material-icons/font": "^1.0.2",
@@ -49,9 +49,9 @@
"swagger-ui-react": "^3.37.2"
},
"devDependencies": {
"@backstage/cli": "^0.6.6",
"@backstage/cli": "^0.6.7",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.9",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
@@ -26,7 +26,7 @@ export const ApiExplorerLayout = ({ children }: Props) => {
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
return (
<Page themeId="home">
<Page themeId="apis">
<Header
title="APIs"
subtitle={generatedSubtitle}

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