Merge remote-tracking branch 'backstage/master' into erikengervall/plugin-release-manager-as-a-service
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Allow custom directory to be specified for GitHub publish action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Export isSonarQubeAvailable.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add built-in publish action for creating GitHub pull requests.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-badges': minor
|
||||
'@backstage/plugin-badges-backend': patch
|
||||
---
|
||||
|
||||
Support auth in badge plugin
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
},
|
||||
...
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Fix for the `file://` protocol check in the `FilePreparer` being too strict, breaking Windows.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Add support for configure an LDAP query filter on multiple lines.
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/test-utils': patch
|
||||
---
|
||||
|
||||
Remove unnecessary wrapping of elements rendered by `wrapInTestApp` and `renderInTestApp`, which was breaking mount discovery.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Optimize data fetched for the `OwnershipCard`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
exposes undocumented `PageTheme`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-github-deployments': patch
|
||||
---
|
||||
|
||||
Add a button to reload the GitHub Deployments card
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Don't use a drag & drop cursor when clicking on disabled `IconLinkVertical`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Fix bug in pull request sample template.
|
||||
@@ -13,6 +13,7 @@ Cloudformation
|
||||
Codecov
|
||||
Codehilite
|
||||
Config
|
||||
Debounce
|
||||
Discoverability
|
||||
Dockerfile
|
||||
Dockerize
|
||||
@@ -111,6 +112,7 @@ cookiecutter
|
||||
css
|
||||
dataflow
|
||||
deadnaming
|
||||
debounce
|
||||
declaratively
|
||||
destructured
|
||||
dev
|
||||
|
||||
@@ -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 team’s engineering dependencies. |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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] <workspace-dir>
|
||||
```
|
||||
|
||||
## 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 <github-org>
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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 />} />
|
||||
|
||||
+209
-269
@@ -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"]
|
||||
|
||||
Vendored
+14
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
+2
-14
@@ -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';
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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
Reference in New Issue
Block a user