Merge branch 'master' of https://github.com/backstage/backstage into pr-draft

This commit is contained in:
Lykke Axlin
2021-10-08 08:26:01 +02:00
695 changed files with 15611 additions and 3853 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/backend-common': patch
---
The `subscribe` method on the `Config` returned by `loadBackendConfig` is now forwarded through `getConfig` and `getOptionalConfig`.
-40
View File
@@ -1,40 +0,0 @@
---
'@backstage/create-app': patch
---
Added the default `ScmAuth` implementation to the app.
To apply this change to an existing app, head to `packages/app/apis.ts`, import `ScmAuth` from `@backstage/integration-react`, and add a `ScmAuth.createDefaultApiFactory()` to your list of APIs:
```diff
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
+ ScmAuth,
} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
...
+ ScmAuth.createDefaultApiFactory(),
...
];
```
If you have integrations towards SCM providers other than the default ones (github.com, gitlab.com, etc.), you will want to create a custom `ScmAuth` factory instead, for example like this:
```ts
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed a bug where internal references within the catalog were broken when new entities where added through entity providers, such as registering a new location or adding one in configuration. These broken references then caused some entities to be incorrectly marked as orphaned and prevented refresh from working properly.
-36
View File
@@ -1,36 +0,0 @@
---
'@backstage/plugin-catalog-import': minor
---
Switched to using the `ScmAuthApi` for authentication rather than GitHub auth. If you are instantiating your `CatalogImportClient` manually you now need to pass in an instance of `ScmAuthApi` instead.
Also be sure to register the `scmAuthApiRef` from the `@backstage/integration-react` in your app:
```ts
import { ScmAuth } from '@backstage/integration-react';
// in packages/app/apis.ts
const apis = [
// ... other APIs
ScmAuth.createDefaultApiFactory();
// OR
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
]
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
The `create-plugin` command now prefers dependency versions ranges that are already in the lockfile.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/integration': patch
---
Support selective GitHub app installation for GHE
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/backend-common': patch
---
Fix an issue where filtering in search doesn't work correctly for Bitbucket.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Added a check for the TechDocs annotation on the entity
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
Stop forcing `target="_blank"` in the `SupportButton` but instead use the default logic of the `Link` component, that opens external targets in a new window and relative targets in the same window.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli': patch
---
Fix duplication checks to stop looking for the old core packages, and to allow some explicitly
-89
View File
@@ -1,89 +0,0 @@
---
'@backstage/integration-react': patch
---
Added `ScmAuthApi` along with the implementation `ScmAuth`. The `ScmAuthApi` provides methods for client-side authentication towards multiple different source code management services simultaneously.
When requesting credentials you supply a URL along with the same options as the other `OAuthApi`s, and optionally a request for additional high-level scopes.
For example like this:
```ts
const { token } = await scmAuthApi.getCredentials({
url: 'https://ghe.example.com/backstage/backstage',
additionalScope: {
repoWrite: true,
},
});
```
The instantiation of the API can either be done with a default factory that adds support for the public providers (github.com, gitlab.com, etc.):
```ts
// in packages/app/apis.ts
ScmAuth.createDefaultApiFactory();
```
Or with a more custom setup that can add support for additional providers, for example like this:
```ts
createApiFactory({
api: scmAuthApiRef,
deps: {
gheAuthApi: gheAuthApiRef,
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi, gheAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
ScmAuth.forGithub(gheAuthApi, {
host: 'ghe.example.com',
}),
),
});
```
The additional `gheAuthApiRef` utility API can be defined either inside the app itself if it's only used for this purpose, for inside an internal common package for APIs, such as `@internal/apis`:
```ts
const gheAuthApiRef: ApiRef<OAuthApi & ProfileInfoApi & SessionApi> =
createApiRef({
id: 'internal.auth.ghe',
});
```
And then implemented using the `GithubAuth` class from `@backstage/core-app-api`:
```ts
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
provider: {
id: 'ghe',
icon: ...,
title: 'GHE'
},
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
})
```
Finally you also need to add and configure another GitHub provider to the `auth-backend` using the provider ID `ghe`:
```ts
// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts
providerFactories: {
ghe: createGithubProvider(),
},
```
Other providers follow the same steps, but you will want to use the appropriate auth API implementation in the frontend, such as for example `GitlabAuth`.
-11
View File
@@ -1,11 +0,0 @@
---
'@backstage/plugin-catalog-backend': minor
---
Introduced a new `CatalogProcessorCache` that is available to catalog processors. It allows arbitrary values to be saved that will then be visible during the next run. The cache is scoped to each individual processor and entity, but is shared across processing steps in a single processor.
The cache is available as a new argument to each of the processing steps, except for `validateEntityKind` and `handleError`.
This also introduces an optional `getProcessorName` to the `CatalogProcessor` interface, which is used to provide a stable identifier for the processor. While it is currently optional it will move to be required in the future.
The breaking part of this change is the modification of the `state` field in the `EntityProcessingRequest` and `EntityProcessingResult` types. This is unlikely to have any impact as the `state` field was previously unused, but could require some minor updates.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
---
Added a check for the Kubernetes annotation on the entity
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
Update OAuth refresh handler to pass updated refresh token to ensure cookie is updated with new value.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog': patch
---
Update AboutCard to only render refresh button if the entity is managed by an url location.
+17
View File
@@ -34,5 +34,22 @@ module.exports = {
onNonMatchingHeader: 'replace',
},
],
'no-restricted-syntax': [
'error',
{
message:
"Avoid using .toLowerCase(), use .toLocaleLowerCase('en-US') instead. " +
'This rule can sometimes be ignored when converting text to be displayed to the user.',
selector:
"CallExpression[arguments.length=0] > MemberExpression[property.name='toLowerCase']",
},
{
message:
"Avoid using .toUpperCase(), use .toLocaleUpperCase('en-US') instead. " +
'This rule can sometimes be ignored when converting text to be displayed to the user.',
selector:
"CallExpression[arguments.length=0] > MemberExpression[property.name='toUpperCase']",
},
],
},
};
+1
View File
@@ -23,3 +23,4 @@
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
/.changeset/techdocs-* @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
+2
View File
@@ -38,6 +38,8 @@ labels: bug
<!--- Include as many relevant details about the environment you experienced the bug in -->
<!-- ProTip: You can use `yarn backstage-cli info` command in your Backstage App for this section. -->
- NodeJS Version (v14):
- Operating System and Version (e.g. Ubuntu 14.04):
- Browser Information:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: documentation quality check
uses: errata-ai/vale-action@v1.3.0
uses: errata-ai/vale-action@v1.4.0
# Whitelist excluding ADOPTERS, CHANGELOG and OWNERS (no exclude flag exists)
with:
files: '[".changeset", ".github", "contrib", "docs", "microsite", "packages", "plugins", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "GOVERNANCE.md", "README.md"]'
+3
View File
@@ -133,3 +133,6 @@ site
# Sensitive credentials
*-credentials.yaml
# e2e tests
cypress/cypress/*
+2
View File
@@ -52,3 +52,5 @@
| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. |
| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. |
| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. |
+37 -1
View File
@@ -262,7 +262,9 @@ catalog:
# Backstage example groups and users
- type: file
target: ../catalog-model/examples/acme-corp.yaml
# Backstage end-to-end tests of TechDocs
- type: file
target: ../../cypress/e2e-fixture.catalog.info.yaml
scaffolder:
# Use to customize default commit author info used when new components are created
# defaultAuthor:
@@ -358,6 +360,10 @@ auth:
clientId: ${AUTH_ONELOGIN_CLIENT_ID}
clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET}
issuer: ${AUTH_ONELOGIN_ISSUER}
bitbucket:
development:
clientId: ${AUTH_BITBUCKET_CLIENT_ID}
clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET}
costInsights:
engineerCost: 200000
products:
@@ -382,6 +388,31 @@ costInsights:
default: true
MSC:
name: Monthly Subscribers
currencies:
engineers:
label: 'Engineers 🛠'
unit: 'engineer'
usd:
label: 'US Dollars 💵'
kind: 'USD'
unit: 'dollar'
prefix: '$'
rate: 1
carbonOffsetTons:
label: 'Carbon Offset Tons ♻️⚖️s'
kind: 'CARBON_OFFSET_TONS'
unit: 'carbon offset ton'
rate: 3.5
beers:
label: 'Beers 🍺'
kind: 'BEERS'
unit: 'beer'
rate: 4.5
pintsIceCream:
label: 'Pints of Ice Cream 🍦'
kind: 'PINTS_OF_ICE_CREAM'
unit: 'ice cream pint'
rate: 5.5
homepage:
clocks:
- label: UTC
@@ -400,3 +431,8 @@ jenkins:
baseUrl: https://jenkins.example.com
username: backstage-bot
apiKey: 123456789abcdef0123456789abcedf012
azureDevOps:
host: dev.azure.com
token: my-token
organization: my-company
+1
View File
@@ -11,6 +11,7 @@ module.exports = {
bundledDependencies: false,
},
],
'jest/valid-expect': 'off',
'jest/expect-expect': 'off',
'no-restricted-syntax': 'off',
},
+4 -2
View File
@@ -2,7 +2,9 @@
"baseUrl": "http://localhost:7000",
"integrationFolder": "./src/integration",
"supportFile": "./src/support",
"fixturesFolder": "./src/fixures",
"fixturesFolder": "./src/fixtures",
"pluginsFile": "./src/plugins",
"defaultCommandTimeout": 10000
"defaultCommandTimeout": 10000,
"viewportHeight": 900,
"viewportWidth": 1440
}
+11
View File
@@ -0,0 +1,11 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: techdocs-e2e-fixture
description: Used for end-to-end tests of TechDocs in Backstage.
annotations:
backstage.io/techdocs-ref: dir:./fixtures
spec:
type: service
lifecycle: experimental
owner: user:guest
+3
View File
@@ -0,0 +1,3 @@
# Home page
This is a basic documentation used for end-to-end tests.
+109
View File
@@ -0,0 +1,109 @@
# Sub-page 1
## Section 1.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 1.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 1.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
+121
View File
@@ -0,0 +1,121 @@
# Sub-page 3
## Section 3.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 3.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 3.2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 3.2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 3.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
+146
View File
@@ -0,0 +1,146 @@
# Sub-page 2
## Section 2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 2.2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 2.2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 2.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
[Link to Section 1.1](sub-page-one.md#section-11)
+10
View File
@@ -0,0 +1,10 @@
site_name: e2e Fixture Documentation
site_description: Documentation used for end-to-end tests of TechDocs in Backstage.
nav:
- Home: index.md
- Sub-page 1: sub-page-one.md
- Sub-page 2: sub-page-two.md
- Nested Sub-pages:
- Sub-page 3: sub-page-three.md
plugins:
- techdocs-core
@@ -0,0 +1,188 @@
/*
* 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.
*/
/// <reference types="cypress" />
import 'os';
describe('TechDocs', () => {
beforeEach(() => {
cy.loginAsGuest();
cy.mockSockJSNode();
cy.interceptTechDocsAPICalls();
});
describe('Navigating to TechDocs', () => {
it('should navigate to the TechDocs home page via the primary navigation bar', () => {
cy.visit('/');
cy.wait(500);
cy.get('[data-testid="sidebar-root"]')
.get('div')
.get('a[href="/docs"]')
.click();
cy.contains('Documentation');
});
it('should navigate to the TechDocs home page from the URL', () => {
cy.visit('/docs');
cy.wait(500);
cy.contains('Documentation');
});
it('should navigate to a specific TechDocs entity from the "Overview" tab', () => {
cy.visit('/docs');
cy.contains('techdocs-e2e-fixture')
.parents()
.eq(2)
.contains('Read Docs')
.click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture',
);
});
});
it('should navigate to a specific TechDocs entity page from a URL', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture');
cy.waitHomePage();
cy.contains('e2e Fixture Documentation');
cy.contains(
'Documentation used for end-to-end tests of TechDocs in Backstage.',
);
cy.getTechDocsShadowRoot().contains('Home page');
});
it('should navigate to a specific TechDocs section from a URL', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture/sub-page-two');
cy.waitSectionTwoPage();
cy.window().its('scrollY').should('equal', 0);
cy.getTechDocsShadowRoot().within(() => {
cy.contains('Sub-page 2');
});
});
it('should navigate to a specific TechDocs fragment from a URL', () => {
cy.visit(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-two#section-23',
);
cy.waitSectionTwoPage();
// This is used to test the post-render behavior of the techdocs Reader
cy.wait(500);
cy.getTechDocsShadowRoot().within(() => {
cy.isInViewport('#section-23');
});
});
it('should navigate to a wrong TechDocs entity page from a URL', () => {
cy.visit('/docs/default/Component/wrong-component');
cy.get('[data-testid=error]').should('be.visible');
});
});
describe('Navigating within TechDocs', () => {
it('should navigate to a specific TechDocs page via the navigation bar', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture');
cy.waitHomePage();
cy.getTechDocsShadowRoot().within(() => {
cy.getTechDocsNavigation()
.find('> div > div > [data-md-level="0"] > ul > li:nth-child(2) > a')
.click();
cy.contains('Sub-page 1');
cy.window().its('scrollY').should('eq', 0);
});
});
describe('Navigating within a TechDocs page', () => {
beforeEach(() => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture/sub-page-two');
cy.waitSectionTwoPage();
});
it('should navigate to a specific fragment within the page via the table of contents - Level 1', () => {
return cy.getTechDocsShadowRoot().within(() => {
// Section 3
cy.getTechDocsTableOfContents().within(() => {
cy.get('> div > div > nav > ul > li:nth-child(3) > a').click();
});
cy.isInViewport('#section-23');
});
});
it('should navigate to a specific fragment within the page via the table of contents - Level 2', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.isNotInViewport('#sub-section-222');
// Section 2.2
cy.getTechDocsTableOfContents()
.find(
'> div > div > nav > ul > li:nth-child(2) > nav > ul > li:nth-child(2) > a',
)
.click();
cy.isInViewport('#sub-section-222');
});
});
it('should navigate to a specific TechDocs page fragment from a link', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-content > article')
.contains('Link to Section 1.1')
.click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-one/',
);
expect(loc.hash).to.eq('#section-11');
});
});
});
it('should navigate to the next page within a TechDocs entity', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-footer-nav__link--next').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-three/',
);
});
});
});
it('should navigate to the previous page within a TechDocs entity', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-footer-nav__link--prev').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-one/',
);
});
});
});
});
});
});
@@ -0,0 +1,31 @@
/*
* 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.
*/
/// <reference types="cypress" />
import 'os';
describe('Login', () => {
it('should render the login page', () => {
cy.visit('/');
cy.contains('Select a sign-in method');
});
it('should be able to login', () => {
cy.get('button').contains('Enter').click();
cy.url().should('include', '/catalog');
cy.contains('artist-lookup');
});
});
@@ -0,0 +1,35 @@
/*
* 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.
*/
/// <reference types="cypress" />
import 'os';
describe('Logout', () => {
before(() => {
cy.loginAsGuest();
});
it('should be able to logout', () => {
cy.visit('/settings');
cy.get('[data-testid="user-settings-menu"]').click();
return cy
.get('[data-testid="sign-out"]')
.click()
.then(() => {
return expect(
localStorage.getItem('@backstage/core:SignInPage:provider'),
).to.be.null;
});
});
});
+116
View File
@@ -0,0 +1,116 @@
/*
* 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.
*/
/* eslint-disable jest/no-standalone-expect */
/// <reference types="cypress" />
import 'os';
Cypress.Commands.add('loginAsGuest', () => {
cy.visit('/', {
onLoad: (win: Window) =>
win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'),
});
});
Cypress.Commands.add('getTechDocsShadowRoot', () => {
cy.get('[data-testid="techdocs-content-shadowroot"]').shadow();
});
Cypress.Commands.add('isNotInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.config(`viewportHeight`);
const rect = $el[0].getBoundingClientRect();
if (bottom) {
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
}
});
});
Cypress.Commands.add('isInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.config(`viewportHeight`);
const rect = $el[0].getBoundingClientRect();
if (bottom) {
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
}
});
});
Cypress.Commands.add('getTechDocsTableOfContents', () => {
cy.get('[data-md-component="toc"]');
});
Cypress.Commands.add('getTechDocsNavigation', () => {
cy.get('[data-md-component="navigation"]');
});
Cypress.Commands.add('mockSockJSNode', () => {
cy.intercept('GET', '**/sockjs-node/info**', {
body: {
websocket: true,
origins: ['*:*'],
cookie_needed: false,
entropy: 2882389500,
},
});
});
Cypress.Commands.add('interceptTechDocsAPICalls', () => {
cy.intercept(
'GET',
'**/techdocs/metadata/entity/default/Component/techdocs-e2e-fixture',
).as('entityMetadata');
cy.intercept(
'GET',
'**/techdocs/metadata/techdocs/default/Component/techdocs-e2e-fixture',
).as('techdocsMetadata');
cy.intercept(
'GET',
'**/techdocs/sync/default/Component/techdocs-e2e-fixture',
).as('syncEntity');
cy.intercept(
'GET',
'**/techdocs/static/docs/default/Component/techdocs-e2e-fixture/sub-page-two/index.html',
).as('sectionTwoHTML');
cy.intercept(
'GET',
'**/techdocs/static/docs/default/Component/techdocs-e2e-fixture/index.html',
).as('homeHTML');
});
Cypress.Commands.add('waitSectionTwoPage', () => {
cy.wait([
'@entityMetadata',
'@syncEntity',
'@techdocsMetadata',
'@sectionTwoHTML',
]);
});
Cypress.Commands.add('waitHomePage', () => {
cy.wait(['@entityMetadata', '@syncEntity', '@techdocsMetadata', '@homeHTML']);
});
+1 -9
View File
@@ -14,12 +14,4 @@
* limitations under the License.
*/
/// <reference types="cypress" />
Cypress.Commands.add('loginAsGuest', () => {
cy.visit('/', {
onLoad: (win: Window) =>
win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'),
});
});
export {};
import './commands';
+50
View File
@@ -22,5 +22,55 @@ declare namespace Cypress {
* @example cy.loginAsGuests
*/
loginAsGuest(): Chainable<Element>;
/**
* Get the TechDocs shadow root element
* @example cy.getTechDocsShadowRoot
*/
getTechDocsShadowRoot(): Chainable<Element>;
/**
* Mock TechDocs backend API
* @example cy.mockTechDocs
*/
mockTechDocs(): void;
/**
* Get the TechDocs table of contents element
* @example cy.getTechDocsShadowRoot
*/
getTechDocsTableOfContents(): Chainable<Element>;
/**
* Get the TechDocs navigation element
* @example cy.getTechDocsNavigation
*/
getTechDocsNavigation(): Chainable<Element>;
/**
* Intercept the TechDocs API calls
* @example cy.interceptTechDocsAPICalls
*/
interceptTechDocsAPICalls(): Chainable<Element>;
/**
* Mock SockJS-Node call
* @example cy.mockSockJSNode
*/
mockSockJSNode(): Chainable<Element>;
/**
* Wait TechDocs API response for home page
* @example cy.waitHomePage
*/
waitHomePage(): Chainable<Element>;
/**
* Wait TechDocs API response for Section 2 page
* @example cy.waitSectionTwoPage
*/
waitSectionTwoPage(): Chainable<Element>;
/**
* Check if the element is in viewport
* @example cy.isInViewport
*/
isInViewport(element: string): Chainable<Element>;
/**
* Check if the element is not in viewport
* @example cy.isNotInViewport
*/
isNotInViewport(element: string): Chainable<Element>;
}
}
+52
View File
@@ -0,0 +1,52 @@
---
id: provider
title: Bitbucket Authentication Provider
sidebar_label: Bitbucket
description: Adding Bitbucket OAuth as an authentication provider in Backstage
---
The Backstage `core-plugin-api` package comes with a Bitbucket authentication
provider that can authenticate users using Bitbucket Cloud. This does **NOT**
work with Bitbucket Server.
## Create an OAuth Consumer in Bitbucket
To add Bitbucket Cloud authentication, you must create an OAuth Consumer.
Go to `https://bitbucket.org/<your-project-name>/workspace/settings/api` .
Click Add Consumer.
Settings for local development:
- Application name: Backstage (or your custom app name)
- Callback URL: `http://localhost:7000/api/auth/bitbucket`
- Other are optional
- (IMPORTANT) **Permissions: Account - Read, Workspace membership - Read**
## Configuration
The provider configuration can then be added to your `app-config.yaml` under the
root `auth` configuration:
```yaml
auth:
environment: development
providers:
bitbucket:
development:
clientId: ${AUTH_BITBUCKET_CLIENT_ID}
clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET}
```
The Bitbucket provider is a structure with two configuration keys:
- `clientId`: The Key that you generated in Bitbucket, e.g.
`b59241722e3c3b4816e2`
- `clientSecret`: The Secret tied to the generated Key.
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `bitbucketAuthApi` 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).
+4
View File
@@ -50,6 +50,10 @@ The GitHub provider is a structure with three configuration keys:
- `clientSecret`: The client secret tied to the generated client ID.
- `enterpriseInstanceUrl` (optional): The base URL for a GitHub Enterprise
instance, e.g. `https://ghe.<company>.com`. Only needed for GitHub Enterprise.
- `callbackUrl` (optional): The callback url that GitHub will use when
initiating an OAuth flow, e.g.
`https://your-intermediate-service.com/handler`. Only needed if Backstage is
not the immediate receiver (e.g. one OAuth app for many backstage instances).
## Adding the provider to the Backstage frontend
+17 -1
View File
@@ -57,6 +57,7 @@ postpack Restores the changes made by the prepack command
create-github-app Create new GitHub App in your organization (experimental)
info Show helpful information for debugging and reporting bugs
help [command] display help for command
```
@@ -423,7 +424,10 @@ This command uses a default Jest configuration that is included in the CLI,
which is set up with similar goals for speed, scale, and working within a
monorepo. The configuration sets the `src` as the root directory, enforces the
`.test.` infix for tests, and uses `src/setupTests.ts` as the test setup
location.
location. The included configuration also supports test execution at the root of
a yarn workspaces monorepo by automatically creating one grouped configuration
that includes all packages that have `backstage-cli test` in their package
`test` script.
If needed, the configuration can be extended using a `"jest"` field in
`package.json`, both within the target package and the monorepo root, with
@@ -647,3 +651,15 @@ YAML file that can be referenced in the GitHub integration configuration.
```text
Usage: backstage-cli create-github-app &lt;github-org&gt;
```
## info
Scope: `root`
Outputs debug information which is useful when opening an issue. Outputs system
information, node.js and npm versions, CLI version and type (inside backstage
repo or a created app), all `@backstage/*` package dependency versions.
```text
Usage: backstage-cli info
```
+4 -3
View File
@@ -13,9 +13,10 @@ which during validation is stitched together into a single schema.
## Schema Collection and Definition
Schemas are collected from all packages and dependencies in each repo that are a
part of the Backstage ecosystem, including transitive dependencies. The current
definition of "part of the ecosystem" is that a package has at least one
dependency in the `@backstage` namespace, but this is subject to change.
part of the Backstage ecosystem, including the root package and transitive
dependencies. The current definition of "part of the ecosystem" is that a
package has at least one dependency in the `@backstage` namespace or a
`"configSchema"` field in `package.json`, but this is subject to change.
Each package is searched for a schema at a single point of entry, a top-level
`"configSchema"` field in `package.json`. The field can either contain an
@@ -90,8 +90,7 @@ does so!
## Creating a Catalog Data Reader Processor
The recommended way of instantiating the catalog backend classes is to use the
[`CatalogBuilder`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/CatalogBuilder.ts),
as illustrated in the
`CatalogBuilder`, as illustrated in the
[example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts).
We will create a new
[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/ingestion/processors/types.ts)
@@ -171,3 +170,105 @@ export default async function createPlugin(
Start up the backend - it should now start reading from the previously
registered location and you'll see your entities start to appear in Backstage.
## Caching processing results
The catalog periodically refreshes entities in the catalog, and in doing so it
calls out to external systems to fetch changes. This can be taxing for upstream
services and large deployments may get rate limited if too many requests are
sent. Luckily many external systems provide ETag support to check for changes
which usually doesn't count towards the quota and saves resources both
internally and externally.
The catalog has built in support for leveraging ETags when refreshing external
locations in GitHub. This example aims to demonstrate how to add the same
behavior for `system-x` that we implemented earlier.
```ts
import { UrlReader } from '@backstage/backend-common';
import { Entity, LocationSpec } from '@backstage/catalog-model';
import {
results,
CatalogProcessor,
CatalogProcessorEmit,
CatalogProcessorCache,
CatalogProcessorParser,
} from '@backstage/plugin-catalog-backend';
// It's recommended to always bump the CACHE_KEY version if you make
// changes to the processor implementation or CacheItem.
const CACHE_KEY = 'v1';
// Our cache item contains the ETag used in the upstream request
// as well as the processing result used when the Etag matches.
// Bump the CACHE_KEY version if you make any changes to this type.
type CacheItem = {
etag: string;
entity: Entity;
};
export class SystemXReaderProcessor implements CatalogProcessor {
constructor(private readonly reader: UrlReader) {}
// It's recommended to give the processor a unique name.
getProcessorName() {
return 'system-x-processor';
}
async readLocation(
location: LocationSpec,
_optional: boolean,
emit: CatalogProcessorEmit,
_parser: CatalogProcessorParser,
cache: CatalogProcessorCache,
): Promise<boolean> {
// Pick a custom location type string. A location will be
// registered later with this type.
if (location.type !== 'system-x') {
return false;
}
const cacheItem = await cache.get<CacheItem>(CACHE_KEY);
try {
// This assumes an URL reader that returns the response together with the ETag.
// We send the ETag from the previous run if it exists.
// The previous ETag will be set in the headers for the outgoing request and system-x
// is going to throw NOT_MODIFIED (HTTP 304) if the ETag matches.
const response = await this.reader.readUrl?.(location.target, {
etag: cacheItem?.etag,
});
if (!response) {
// readUrl is currently optional to implement so we have to check if we get a response back.
throw new Error(
'No URL reader that can parse system-x targets installed',
);
}
// ETag is optional in the response but we need it to cache the result.
if (!response.etag) {
throw new Error(
'No ETag returned from system-x, cannot use response for caching',
);
}
// For this example the JSON payload is a single entity.
const entity: Entity = JSON.parse(response.buffer.toString());
emit(results.entity(location, entity));
// Update the cache with the new ETag and entity used for the next run.
await cache.set<CacheItem>(CACHE_KEY, {
etag: response.etag,
entity,
});
} catch (error) {
if (error.name === 'NotModifiedError' && cacheItem) {
// The ETag matches and we have a cached value from the previous run.
emit(results.entity(location, cacheItem.entity));
}
const message = `Unable to read ${location.type}, ${error}`;
emit(results.generalError(location, message));
}
return true;
}
}
```
@@ -6,7 +6,7 @@ description: How to write your own actions
If you're wanting to extend the functionality of the Scaffolder, you can do so
by writing custom actions which can be used along side our
[built-in actions](./builtin-actions.md)
[built-in actions](./builtin-actions.md).
### Writing your Custom Action
@@ -60,7 +60,7 @@ close over the `TemplateAction`. Take a look at our
[built-in actions](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/actions/builtin)
for reference.
We set the type generic to `{ contents: string, filename: string}` which is
We set the type generic to `{ contents: string, filename: string }` which is
there to set the type on the handler `ctx` `inputs` property so we get good type
checking. This could be generated from the next part of this guide, the `input`
schema, but it's not supported right now. Feel free to contribute 🚀 👍.
@@ -68,12 +68,12 @@ schema, but it's not supported right now. Feel free to contribute 🚀 👍.
The `createTemplateAction` takes an object which specifies the following:
- `id` - a unique ID for your custom action. We encourage you to namespace these
in someway so they wont collide with future built-in actions that we may ship
with the `scaffolder-backend` plugin.
in some way so that they won't collide with future built-in actions that we
may ship with the `scaffolder-backend` plugin.
- `schema.input` - A JSON schema for input values to your function
- `schema.output` - A JSON schema for values which are outputted from the
function using `ctx.output`
- `handler` the actual code which is run part of the action, with a context.
- `handler` - the actual code which is run part of the action, with a context
#### The context object
@@ -81,7 +81,7 @@ When the action `handler` is called, we provide you a `context` as the only
argument. It looks like the following:
- `ctx.baseUrl` - a string where the template is located
- `ctx.logger` - a winston logger for additional logging inside your action
- `ctx.logger` - a Winston logger for additional logging inside your action
- `ctx.logStream` - a stream version of the logger if needed
- `ctx.workspacePath` - a string of the working directory of the template run
- `ctx.input` - an object which should match the JSON schema provided in the
@@ -97,7 +97,7 @@ spec:
entityRef: '{{ steps.register.output.entityRef }}'
```
Let's dive in an pick apart what each of these sections do and what they are.
Let's dive in and pick apart what each of these sections do and what they are.
### `spec.parameters` - `FormStep | FormStep[]`
@@ -256,11 +256,11 @@ use `ui:widget: password` or set some properties of `ui:backstage`:
#### The Repository Picker
So in order to make working with repository providers easier, we've built a
custom picker that can be used by overriding the `ui:field` option in the
`uiSchema` for a `string` field. Instead of displaying a text input block it
will render our custom component that we've built which makes it easy to select
a repository provider, and insert a project or owner, and repository name.
In order to make working with repository providers easier, we've built a custom
picker that can be used by overriding the `ui:field` option in the `uiSchema`
for a `string` field. Instead of displaying a text input block it will render
our custom component that we've built which makes it easy to select a repository
provider, and insert a project or owner, and repository name.
You can see it in the above full example which is a separate step and it looks a
little like this:
@@ -323,9 +323,9 @@ template. These follow the same standard format:
name: '{{ parameters.name }}'
```
By default we ship some built in actions that you can take a look at
[here](./builtin-actions.md), or you can create your own custom actions by
looking at the docs [here](./writing-custom-actions.md)
By default we ship some [built in actions](./builtin-actions.md) that you can
take a look at, or you can
[create your own custom actions](./writing-custom-actions.md).
### Outputs
@@ -359,5 +359,5 @@ As you can see above in the `Outputs` section, `actions` and `steps` can also
output things. You can grab that output using `steps.$stepId.output.$property`.
You can read more about all the `inputs` and `outputs` defined in the actions in
code part of the `JSONSchema`, or you can read more about our built in ones
[here](./builtin-actions.md).
code part of the `JSONSchema`, or you can read more about our
[built in actions](./builtin-actions.md).
+78
View File
@@ -142,6 +142,84 @@ const AppRoutes = () => {
};
```
## How to customize the TechDocs reader page?
Similar to how it is possible to customize the TechDocs Home, it is also
possible to customize the TechDocs Reader Page. It is done in your `app`
package. By default, you might see something like this in your `App.tsx`:
```tsx
const AppRoutes = () => {
<Route path="/docs/:namespace/:kind/:name/*" element={<TechDocsReaderPage />}>
{techDocsPage}
</Route>;
};
```
The `techDocsPage` is a default techdocs reader page which lives in
`packages/app/src/components/techdocs`. It includes the following without you
having to set anything up.
```tsx
<TechDocsPage>
{({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => (
<>
<TechDocsPageHeader
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityRef={entityRef}
/>
<Content data-testid="techdocs-content">
<Reader onReady={onReady} entityRef={entityRef} />
</Content>
</>
)}
</TechDocsPage>
```
If you would like to compose your own `techDocsPage`, you can do so by replacing
the children of TechDocsPage with something else. Maybe you are _just_
interested in replacing the Header:
```tsx
<TechDocsPage>
{({ entityRef, onReady }) => (
<>
<Header type="documentation" title="Custom Header" />
<Content data-testid="techdocs-content">
<Reader onReady={onReady} entityRef={entityRef} />
</Content>
</>
)}
</TechDocsPage>
```
Or maybe you want to disable the in-context search
```tsx
<TechDocsPage>
{({ entityRef, onReady }) => (
<>
<Header type="documentation" title="Custom Header" />
<Content data-testid="techdocs-content">
<Reader onReady={onReady} entityRef={entityRef} withSearch={false} />
</Content>
</>
)}
</TechDocsPage>
```
Or maybe you want to replace the entire TechDocs Page.
```tsx
<TechDocsPage>
<Header type="documentation" title="Custom Header" />
<Content data-testid="techdocs-content">
<p>my own content</p>
</Content>
</TechDocsPage>
```
## How to migrate from TechDocs Alpha to Beta
> This guide only applies to the "recommended" TechDocs deployment method (where
+74 -3
View File
@@ -37,9 +37,7 @@ exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme)
in combination with
[createTheme](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme)
from [@material-ui/core](https://www.npmjs.com/package/@material-ui/core). See
the
[@backstage/theme source](https://github.com/backstage/backstage/tree/master/packages/theme/src)
and the implementation of the `createTheme` function for how this is done.
the "Overriding Backstage and Material UI css rules" section below.
You can also create a theme from scratch that matches the `BackstageTheme` type
exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
@@ -136,6 +134,79 @@ const themeOptions = createThemeOptions({
});
```
## Overriding Backstage and Material UI components styles
When creating a custom theme you would be applying different values to
component's css rules that use the theme object. For example, a Backstage
component's styles might look like this:
```ts
const useStyles = makeStyles<BackstageTheme>(
theme => ({
header: {
padding: theme.spacing(3),
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
backgroundImage: theme.page.backgroundImage,
},
}),
{ name: 'BackstageHeader' },
);
```
Notice how the `padding` is getting its value from `theme.spacing`, that means
that setting a value for spacing in your custom theme would affect this
component padding property and the same goes for `backgroundImage` which uses
`theme.page.backgroundImage`. However, the `boxShadow` property doesn't
reference any value from the theme, that means that creating a custom theme
wouldn't be enough to alter the `box-shadow` property or to add css rules that
aren't already defined like a margin. For these cases you should also create an
override.
```ts
import { createApp } from '@backstage/core-app-api';
import { BackstageTheme, lightTheme } from '@backstage/theme';
/**
* The `@backstage/core-components` package exposes this type that
* contains all Backstage and `material-ui` components that can be
* overridden along with the classes key those components use.
*/
import { BackstageOverrides } from '@backstage/core-components';
export const createCustomThemeOverrides = (
theme: BackstageTheme,
): BackstageOverrides => {
return {
BackstageHeader: {
header: {
width: 'auto',
margin: '20px',
boxShadow: 'none',
borderBottom: `4px solid ${theme.palette.primary.main}`,
},
},
};
};
const app = createApp({
apis: ...,
plugins: ...,
themes: [{
id: 'my-theme',
title: 'My Custom Theme',
variant: 'light',
theme: {
...lightTheme,
overrides: {
// These are the overrides that Backstage applies to `material-ui` components
...lightTheme.overrides,
// These are your custom overrides, either to `material-ui` or Backstage components.
...createCustomThemeOverrides(lightTheme),
},
},
}]
});
```
## Custom Logo
In addition to a custom theme, you can also customize the logo displayed at the
+2 -2
View File
@@ -27,7 +27,7 @@ point building on top of the previous one:
and the new APIs can be used in parallel. This deprecation must have been
released for at least two weeks before the deprecated API is removed in a
minor version bump.
- **3** - The time limit for the deprecation is 3 months instead of two weeks.
- **3** - The time limit for the deprecation is 3 months instead of two days.
TL;DR:
@@ -279,7 +279,7 @@ Integrates GraphiQL as a tool to browse GraphQL API endpoints inside Backstage.
Stability: `1`
### `graphql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/graphql/)
### `graphql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/graphql-backend/)
A backend plugin that provides
+77 -64
View File
@@ -1,42 +1,39 @@
---
id: composability
title: Composability System Migration
title: Composability System
# prettier-ignore
description: Documentation and migration instructions for new composability APIs.
description: Documentation for the Backstage plugin composability APIs.
---
## Summary
This page describes the new composability system that was recently introduced in
Backstage, and it does so from the perspective of the existing patterns and
APIs. As the new system is solidified and existing code is ported, this page
will be removed and replaced with a more direct description of the composability
system. For now, the primary purpose of this documentation is to aid in the
migration of existing plugins, but it does cover the migration of apps as well.
This page describes the composability system that helps bring together content
from a multitude of plugins into one Backstage application.
The core principle of the new composability system is that plugins should have
clear boundaries and connections. It should isolate crashes within a plugin, but
allow navigation between them. It should allow for plugins to be loaded only
when needed, and enable plugins to provide extension points for other plugins to
The core principle of the composability system is that plugins should have clear
boundaries and connections. It should isolate crashes within a plugin, but allow
navigation between them. It should allow for plugins to be loaded only when
needed, and enable plugins to provide extension points for other plugins to
build upon. The composability system is also built with an app-first mindset,
prioritizing simplicity and clarity in the app over that in the plugins and core
APIs.
The new composability system isn't a single new API surface. It is a collection
of patterns, primitives, new APIs, and old APIs used in new ways. At the core is
the new concept of extensions, which are exported by plugins for use in the app.
There is also a new primitive called component data, which assists in the
conversion to a more declarative app. The `RouteRef`s now have a clear purpose
as well, and can be used route to pages in a flexible way.
The composability system isn't a single API surface. It is a collection of
patterns, primitives, and APIs. At the core is the concept of **extensions**,
which are exported by plugins for use in the app. There is also a primitive
called component data, which helps keep the structure of the app more
declarative. There are also `RouteRef`s that help route between pages in a
flexible way, which is especially important when bringing together different
open source plugins.
## New Concepts
## Concepts
This section is a brief look into all the new and updated concepts that were put
in place to support the new composability system.
This section is a brief look into all the concepts that help support the
composability system.
### Component Data
Component data is a new composability primitive that is introduced as a way to
Component data is a composability primitive that is introduced as a way to
provide a new data dimension for React components. Data is attached to React
components using a key, and is then readable from any JSX elements created with
those components, using the same key, as illustrated by the following example:
@@ -59,7 +56,7 @@ inspected, while our component data adds more structured access and simplifies
evolution by allowing for multiple different versions of a piece of data to be
used and interpreted at once.
The initial use-case for component data is to support route and plugin discovery
One of the use-cases for component data is to support route and plugin discovery
through elements in the app. Through this we allow for the React element tree in
the app to be the source of truth, both for which plugins are used, as well as
all top-level plugin routes in the app. The use of component data is not limited
@@ -101,9 +98,10 @@ supply a `RouteRef` as `mountPoint`. The mount point will be the handle of the
component for the outside world, and is used by other components and plugins
that wish to link to the routable component.
As of now there are only two extension creation functions, but it is possible to
add more of them in the future, both in the core library and in plugins that
wish to provide an extension point for other plugins to build upon. Extensions
As of now there are only two extension creation functions in the core library,
but more may be added in the future. There are also some plugins that provide
ways to extend functionality through their own extensions, like
`createScaffolderFieldExtension` from `@backstage/plugin-scaffolder`. Extensions
are also not tied to React, and can both be used to model generic JavaScript
concepts, as well as potentially bridge to rendering libraries and web
frameworks other than React.
@@ -192,14 +190,30 @@ const App = () => (
);
```
### New Routing System
### Naming Patterns
A big piece of what is enabled by moving over to this new composability system
is to make `RouteRef`s useful. The `RouteRef`s no longer have their own path, in
fact the only required parameter is currently a `title`. Instead of assigning a
path to each `RouteRef` and possibly overriding these paths in the app, the
concrete `path` for each `RouteRef` is discovered based on the element tree in
the app. Let's consider the following example:
There are a couple of naming patters to adhere to as you build plugins, which
helps clarify the intent and usage of the exports.
| Description | Pattern | Examples |
| --------------------- | --------------- | ---------------------------------------------- |
| Top-level Pages | \*Page | CatalogIndexPage, SettingsPage, LighthousePage |
| Entity Tab Content | Entity\*Content | EntityJenkinsContent, EntityKubernetesContent |
| Entity Overview Card | Entity\*Card | EntitySentryCard, EntityPagerDutyCard |
| Entity Conditional | is\*Available | isPagerDutyAvailable, isJenkinsAvailable |
| Plugin Instance | \*Plugin | jenkinsPlugin, catalogPlugin |
| Utility API Reference | \*ApiRef | configApiRef, catalogApiRef |
### Routing System
The routing system of Backstage relies heavily on the composability system. It
uses `RouteRef`s to represent routing targets in the app, which at runtime will
be bound to a concrete `path`, but provides a level of indirection to help mix
together different plugins that otherwise wouldn't know how to route to each
other.
The concrete `path` for each `RouteRef` is discovered based on the element tree
in the app. Let's consider the following example:
```tsx
const appRoutes = (
@@ -216,12 +230,10 @@ extension it has a `RouteRef` assigned as its mount point, which we'll refer to
as `fooPageRouteRef`.
Given the above example, the `fooPageRouteRef` will be associated with the
`'/foo'` route. The path is no longer accessible via the `path` property of the
`RouteRef` though, as the routing structure is tied to the app's react tree. We
instead use the new `useRouteRef` hook if we want to create a concrete link to
the page. The `useRouteRef` hook takes a single `RouteRef` as its only
parameter, and returns a function that is called to create the URL. For example
like this:
`'/foo'` route. If we want to route to the `FooPage`, we can use the
`useRouteRef` hook to create a concrete link to the page. The `useRouteRef` hook
takes a single `RouteRef` as its only parameter, and returns a function that is
called to create the URL. For example like this:
```tsx
const MyComponent = () => {
@@ -230,16 +242,15 @@ const MyComponent = () => {
};
```
Now let's assume that we want to link from the `BarPage` to the `FooPage`.
Before the introduction of the new composability system, we would do this by
importing the `fooPageRouteRef` exported by the `fooPlugin`. This created an
unnecessary dependency on the plugin, and also provided little flexibility in
allowing the app to tie plugins together, with the links instead being dictated
by the plugins themselves. To solve this, we introduce `ExternalRouteRef`s. Much
like regular route references, they can be passed to `useRouteRef` to create
concrete URLs, but they can not be used as mount points in routable component
and instead have to be associated with a target route using route bindings in
the app.
Now let's assume that we want to link from the `BarPage` to the `FooPage`. We
don't want to reference the `fooPageRouteRef` directly from our `barPlugin`,
since that would create an unnecessary dependency on the `fooPlugin`. It would
also provided little flexibility in allowing the app to tie plugins together,
with the links instead being dictated by the plugins themselves. To solve this,
we use `ExternalRouteRef`s. Much like regular route references, they can be
passed to `useRouteRef` to create concrete URLs, but they can not be used as
mount points in routable component and instead have to be associated with a
target route using route bindings in the app.
We create a new `ExternalRouteRef` inside the `barPlugin`, using a neutral name
that describes its role in the plugin rather than a specific plugin page that it
@@ -305,6 +316,14 @@ in a different file than the one that creates the plugin instance, for example a
top-level `routes.ts`. This is to avoid circular imports when you use the route
references from other parts of the same plugin.
Another thing to note is that this indirection in the routing is particularly
useful for open source plugins that need to leave flexibility in how they are
integrated. For plugins that you build internally for your own Backstage
application, you can choose to go the route of direct imports or even use
concrete routes directly. Although there can be some benefits to using the full
routing system even in internal plugins. It can help you structure your routes,
and as you will see further down it also helps you manage route parameters.
### Optional External Routes
When creating an `ExternalRouteRef` it is possible to mark it as optional:
@@ -338,7 +357,7 @@ const MyComponent = () => {
### Parameterized Routes
A new addition to `RouteRef`s is the possibility of adding named and typed
A feature of `RouteRef`s is the possibility of adding named and typed
parameters. Parameters are declared at creation, and will enforce presence of
the parameters in the path in the app, and require them as a parameter when
using `useRouteRef`.
@@ -415,21 +434,12 @@ const MyPage = () => (
);
```
### New Catalog Components
### Catalog Components
The established pattern for selecting what plugins should be available on each
catalog page is to use custom components in the app, with logic embedded in the
render function. Typically this takes form as a component that either receives
the entity via props or uses the `useEntity` hook to retrieve the selected
entity. A `switch` or `if` / `else if` chain is then used to select what
children should be rendered based on information in the entity.
This pattern will no longer work with the new composability system, and in
general is very difficult to build any form of declarative model around, as it
depends on runtime execution. To help replace existing code, a new
`EntitySwitch` component has been added to the `@backstage/catalog` plugin,
which grabs the selected entity from a context, and selects at most one element
to render using a list of `EntitySwitch.Case` children.
To help structure the catalog entity pages in your app and choose what content
to render in different scenarios, the `@backstage/catalog` plugin provides an
`EntitySwitch` component. It works by selecting at most one element to render
using a list of `EntitySwitch.Case` children.
For example, if you want all entities of kind `"Template"` to be rendered with a
`MyTemplate` component, and all other entities to be rendered with a `MyOther`
@@ -475,6 +485,9 @@ new `EntityLayout` component. It is a tweaked version and replacement for the
`EntityPageLayout` component, and is introduced more in depth in the app
migration section below.
**NOTE**: The rest of this documentation covers how to migrate older
applications to the new composability system described above.
## Porting Existing Plugins
There are a couple of high-level steps to porting an existing plugin to the new
@@ -0,0 +1,208 @@
---
id: using-backstage-proxy-within-plugin
title: Using the Backstage Proxy from Within a Plugin
# prettier-ignore
description: Guide on how to create a set of API bindings that interface with a backend via the backstage proxy
---
This guide walks you through setting up a simple proxy to an existing API that
is deployed externally to backstage and sending requests to that API from within
a backstage frontend plugin.
If your plugin requires access to an API, backstage offers
[3 options](../plugins/call-existing-api.md):
1. you can
[access the API directly](../plugins/call-existing-api.md#issuing-requests-directly),
1. you can create a [backend plugin](../plugins/backend-plugin.md) if you are
implementing the API alongside your frontend plugin
1. you can configure backstage to proxy to an already existing API.
**Table of Contents**
- [Setting up the backstage proxy](#setting-up-the-backstage-proxy)
- [Calling an API using the backstage proxy](#calling-an-api-using-the-backstage-proxy)
- [Defining the API client interface](#defining-the-api-client-interface)
- [Creating the API client](#creating-the-api-client)
- [Bundling your ApiRef with your plugin](#bundling-your-apiref-with-your-plugin)
- [Using the API in your components](#using-your-plugin-in-your-components)
# Setting up the backstage proxy
Let's say your plugin's API is hosted at _https://api.myawesomeservice.com/v1_,
and you want to be able to access it within backstage at
`/api/proxy/<your-proxy-uri>`, and add a default header called
`X-Custom-Source`. You will need to add the following to `app-config.yaml`:
```yaml
proxy:
'/<your-proxy-uri>':
target: https://api.myawesomeservice.com/v1
headers:
X-Custom-Source: backstage
```
You can find more details about the proxy config options in the
[proxying section](../plugins/proxying.md).
# Calling an API using the backstage proxy
If you followed the previous steps, you should now be able to access your API by
calling `${backend-url}/api/proxy/<your-proxy-uri>`. The reason why
`backend-url` is referenced is because the backstage backend creates and runs
the proxy. Backstage is structured in such a way that you could run the
backstage frontend independently of the backend. So when calling your API you
need to prepend the backend url to your http call.
The recommended pattern for calling out to services is to wrap your calls in a
[Utility API](../api/utility-apis.md). This section describes the steps to wrap
your API client in a Utility API, which are:
- use [`createApiRef`](../reference/core-plugin-api.createapiref.md) to create a
new [`ApiRef`](../reference/core-plugin-api.apiref.md)
- register an [`ApiFactory`](../reference/core-plugin-api.apifactory.md) with
your plugin using
[`createApiFactory`](../reference/core-plugin-api.createapifactory.md). This
will wrap your API implementation, associate your `ApiRef` with your
implementation and tell backstage how to instantiate it
- finally, you can use your API in your components by calling
[`useApi`](../reference/core-plugin-api.useapi.md)
## Defining the API client interface
Continuing from the previous example, let's assume that
_https://api.myawesomeservice.com/v1_ has the following endpoints:
| Method | Description |
| :----------------------- | :---------------------- |
| `GET /users` | Returns a list of users |
| `GET /users/{userId}` | Returns a single user |
| `DELETE /users/{userId}` | Deletes a user |
Here is an example definition for this API following backstage's `apiRef` style:
```ts
/* src/api.ts */
import { createApiRef } from '@backstage/core-plugin-api';
export interface User {
name: string;
email: string;
}
export interface MyAwesomeApi {
url: string;
listUsers: () => Promise<List<User>>;
getUser: (userId: string) => Promise<User>;
deleteUser: (userId: string) => Promise<boolean>;
}
export const myAwesomeApiRef = createApiRef<MyAwesomeApi>({
id: 'plugin.my-awesome-api.service',
description: 'Example API definition',
});
```
## Creating the API client
The `myAwesomeApiRef` is what you will use within backstage to reference the API
client in your plugin. The API ref itself is a global singleton object that
allows you to reference your instantiated API. The actual implementation would
look something like this:
```ts
/* src/api.ts */
/* ... */
import { DiscoveryApi } from '@backstage/core-plugin-api';
export class MyAwesomeApiClient implements MyAwesomeApi {
discoveryApi: DiscoveryApi;
constructor({discoveryApi}: {discoveryApi: DiscoveryApi}) {
this.discoveryApi = discoveryApi;
}
private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> {
// As configured previously for the backend proxy
const proxyUri = '${await this.discoveryApi.getBaseUrl('proxy')}/<your-proxy-uri>';
const resp = await fetch(`${proxyUri}${input}`, init);
if (!resp.ok) throw new Error(resp);
return await resp.json();
}
async listUsers(): Promise<List<User>> {
return await this.fetch<List<User>>('/users');
}
async getUser(userId: string): Promise<User> {
return await this.fetch<User>(`/users/${userId}`);
}
async deleteUser(userId: string): Promise<boolean> {
return await this.fetch<boolean>(
`/users/${userId}`,
{ method: 'DELETE' }
);
}
```
> For more information on the DiscoveryApi check out the
> [docs](../reference/core-plugin-api.discoveryapi.md)
## Bundling your ApiRef with your plugin
The final piece in the puzzle is bundling the `myAwesomeApiRef` with a factory
for `MyAwesomeApiClient` objects. This is usually done in the `plugin.ts` file
inside the plugin's `src` directory. This is an example of what it'd look like,
assuming you added the previous code in a file called `api.ts`:
```ts
/* src/plugin.ts */
import { myAwesomeApiRef, MyAwesomeApiClient } from './api';
import {
createPlugin,
createRouteRef,
createApiFactory,
createRoutableExtension,
createComponentExtension,
discoveryApiRef,
} from '@backstage/core-plugin-api';
//...
export const myCustomPlugin = createPlugin({
id: '<your-plugin-name>',
// Configure a factory for myAwesomeApiRef
apis: [
createApiFactory({
api: myAwesomeApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new MyAwesomeApiClient({ discoveryApi }),
}),
],
});
```
## Using the API in your components
Now you should be able to access your API using the backstage hook
[`useApi`](../reference/core-plugin-api.useapi.md) from within your plugin code.
```ts
/* plugins/my-awesome-plugin/src/components/AwesomeUsersTable.tsx */
import { useApi } from '@backstage/core-plugin-api';
import { myAwesomeApiRef } from '../../api';
export const AwesomeUsersTable = () => {
const apiClient = useApi(myAwesomeApiRef);
apiClient.listUsers()
.then(
...
)
}
```
@@ -0,0 +1,45 @@
---
title: The Big 5-0
author: Francesco Corti, Spotify
authorURL: https://github.com/fcorti/
authorImageURL: https://avatars.githubusercontent.com/u/6010860?v=4
---
Were excited to celebrate an important milestone for the Backstage community: 50+ [public adopters](https://github.com/backstage/backstage/blob/master/ADOPTERS.md)!
![Celebrating all of Backstages 50+ adopters.](assets/21-09-30/50-public-adopters.png)
Before digging into why we believe this is so important, we want to send a huge “thank you” to all the Backstage adopters and contributors who have helped grow the Backstage community. First, to the publicly listed adopters as your visible support for the project excites others to learn more about Backstage. Second, to those non-listed adopters, many of whom still engage in the community via issues, comments, or code contributions. Thank you!
<!--truncate-->
## Why we care about public adopters
![The pace of public adoption has accelerated over the last year](assets/21-09-30/public-backstage-adopters.png)
If you roll the clock back to early 2020 when Backstage was open sourced, we never couldve imagined surpassing this milestone. It feels particularly big given that Backstage tends to be adopted by complex organizations with hundreds (if not thousands of) developers and thousands (if not tens of thousands) of software components.
Weve also seen such diverse examples of Backstage in the wild. Adopters as varied as [American Airlines][am] and [Splunk][sp] have demoed their Backstage-built developer portals. Weve seen digital-first companies like [Zalando][za] and [DAZN][da] share their journey from proof-of-concept to in-production. And the developer experience team at [Expedia Group][ex] has been shared at multiple Community Sessions, detailing their adoption journey as well as contributing back with ideas for new features.
For the Backstage project, public adopters are important because they provide a wide variety of use cases, industries, and degrees of complexity (teams, number of services, etc.) that Backstage is able to support. In addition, a growing adopters list shows the projects maturity and helps other organizations understand the benefits and make the decision to join the project and the community.
In other words, the more the public adopters list grows and becomes more diverse, the more the community will grow and provide contributions that benefit all adopters.
## Fifty is just the beginning
![Backstage is growing: 52+ pull requests per week, excluding maintainers. Over 13,000 stars on GitHub. 518 total contributors, with about 7+ new contributors per week.](assets/21-09-30/backstage-stats.png)
While were thrilled to celebrate this milestone, the public adopters list is just one metric we monitor in the overall health of the Backstage project. It definitely doesnt tell the whole story! Were seeing continued growth within the contributor community, PRs, and GitHub overall ratings as well. As fate would have it, we also recently reached 50+ plugins in the [Backstage Plugin Marketplace][plugins]!
## Join us!
If you are a Backstage enthusiast, please [join me][news] and the entire Backstage Team in celebrating this milestone. And if you are a Backstage adopter not already listed in the [GitHub page][gh], consider adding your name to better inform the community and participate in the project.
[am]: https://backstage.spotify.com/blog/adopter-spotlight/american-airlines-runway/
[sp]: https://backstage.spotify.com/blog/community-session/splunk-pink-phonebook/
[za]: https://youtu.be/6sg5uMCLxTA
[da]: https://medium.com/dazn-tech/developer-experience-dx-at-dazn-e6de9a0208d2
[ex]: https://backstage.spotify.com/blog/community-session/firehydrant-expedia-loblaw/
[plugins]: https://backstage.io/plugins
[news]: https://mailchi.mp/spotify/backstage-community
[gh]: https://github.com/backstage/backstage/blob/master/ADOPTERS.md
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

+9
View File
@@ -0,0 +1,9 @@
---
title: Badges
author: Andreas Stenius
authorUrl: https://github.com/backstage/community
category: Discovery
description: The badges plugin offers a set of badges that can be used outside of Backstage, showing information related to data from the catalog.
documentation: https://github.com/backstage/backstage/blob/master/plugins/badges/README.md
iconUrl: img/badges.svg
npmPackageName: '@backstage/plugin-badges'
@@ -3,7 +3,7 @@ title: GitHub Release Manager
author: '@Spotify'
authorUrl: https://github.com/spotify
category: Release management
description: Manage releases without having to juggle git commands
description: Manage releases without having to juggle git commands.
documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager
iconUrl: img/git-release-manager-logo.svg
npmPackageName: '@backstage/plugin-git-release-manager'
+1 -1
View File
@@ -3,7 +3,7 @@ title: GKE Usage
author: BESTSELLER
authorUrl: bestsellerit.com
category: Discovery
description: This plugin will show you the cost and resource usage of your application within GKE.
description: This plugin will show you the cost and resource usage of your application within Google Kubernetes Engine (GKE).
documentation: https://github.com/BESTSELLER/backstage-plugin-gkeusage/blob/master/README.md
iconUrl: https://bestsellerit.com/img/google-container-engine_avatar.svg
npmPackageName: '@bestsellerit/backstage-plugin-gkeusage'
+1 -1
View File
@@ -3,7 +3,7 @@ title: Harbor
author: BESTSELLER
authorUrl: bestsellerit.com
category: Discovery
description: This plugin will show you information about docker images within harbor.
description: This plugin will show you information about Docker images within the Harbor cloud native registry.
documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master/README.md
iconUrl: https://bestsellerit.com/img/terraform-harbor/goharbor.jpeg
npmPackageName: '@bestsellerit/backstage-plugin-harbor'
+13
View File
@@ -0,0 +1,13 @@
---
title: Prometheus
author: Roadie
authorUrl: https://roadie.io
category: Monitoring
description: Prometheus plugin provides visualization of Prometheus metrics and alerts
documentation: https://roadie.io/backstage/plugins/prometheus/?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=prometheus
iconUrl: https://avatars.githubusercontent.com/u/3380462?s=200&v=4
npmPackageName: '@roadiehq/backstage-plugin-prometheus'
tags:
- monitoring
- graphs
- alerting
+9
View File
@@ -0,0 +1,9 @@
---
title: Shortcuts
author: Spotify
authorUrl: https://github.com/spotify
category: Utility
description: The shortcuts plugin allows a user to have easy access to pages within a Backstage app by storing them as "shortcuts" in the Sidebar.
documentation: https://github.com/backstage/backstage/blob/master/plugins/shortcuts/README.md
iconUrl: img/shortcuts.svg
npmPackageName: '@backstage/plugin-shortcuts'
+14 -9
View File
@@ -20,6 +20,20 @@ class Index extends React.Component {
return (
<main className="MainContent">
<Banner.Container>
<Banner.Dismissable storageKey="2021-10-07-software-templates-techdocs-beta">
🚀 Feature updates!{' '}
<a href="https://backstage.io/blog/2021/07/26/software-templates-are-now-in-beta">
Software Templates
</a>{' '}
and{' '}
<a href="https://backstage.io/blog/2021/09/16/the-techdocs-beta-has-landed">
TechDocs
</a>{' '}
are now in beta.
</Banner.Dismissable>
</Banner.Container>
<Block small className="bg-black-grey stripe-bottom">
<Block.Container>
<Block.TextBox>
@@ -54,15 +68,6 @@ class Index extends React.Component {
</Block.Container>
</Block>
<Banner.Container>
<Banner.Dismissable storageKey="k8s-launch">
🎉 New feature: Kubernetes for service owners.{' '}
<a href="https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners">
Learn more.
</a>
</Banner.Dismissable>
</Banner.Container>
<Block small className="stripe-top bg-black">
<Block.Container wrapped>
<Block.TextBox>
+2 -1
View File
@@ -255,7 +255,8 @@
"tutorials/quickstart-app-plugin",
"tutorials/migrating-away-from-core",
"tutorials/configuring-plugin-databases",
"tutorials/switching-sqlite-postgres"
"tutorials/switching-sqlite-postgres",
"tutorials/using-backstage-proxy-within-plugin"
],
"Architecture Decision Records (ADRs)": [
"architecture-decisions/adrs-overview",
+19 -2
View File
@@ -1094,10 +1094,27 @@ code {
position: relative;
overflow: visible;
z-index: 100;
margin: 25px auto 0 auto;
max-width: 1430px;
height: 0;
margin: -14px auto 14px auto;
}
@media only screen and (max-width: 1024px) {
.Banner__Container {
margin-top: 20px;
}
}
@media only screen and (max-width: 645px) {
.Banner__Container {
margin-top: 60px;
}
}
@media only screen and (max-width: 335px) {
.Banner__Container {
margin-top: 90px;
}
}
.Banner__DismissButton {
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3v-3h18v3z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,10H3v2h11V10z M14,6H3v2h11V6z M18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2H18z M3,16h7v-2H3V16z"/></g></svg>

After

Width:  |  Height:  |  Size: 298 B

+3 -1
View File
@@ -111,7 +111,7 @@ nav:
- Structure of a plugin: 'plugins/structure-of-a-plugin.md'
- Plugin Development: 'plugins/plugin-development.md'
- Integrate into the Software Catalog: 'plugins/integrating-plugin-into-software-catalog.md'
- Composability System Migration: 'plugins/composability.md'
- Composability System: 'plugins/composability.md'
- Backends and APIs:
- Proxying: 'plugins/proxying.md'
- Backend plugin: 'plugins/backend-plugin.md'
@@ -140,6 +140,7 @@ nav:
- Google: 'auth/google/provider.md'
- Okta: 'auth/okta/provider.md'
- OneLogin: 'auth/onelogin/provider.md'
- Bitbucket: 'auth/bitbucket/provider.md'
- Adding authentication providers: 'auth/add-auth-provider.md'
- Using authentication and identity: 'auth/using-auth.md'
- Sign in resolvers: 'auth/identity-resolver.md'
@@ -165,6 +166,7 @@ nav:
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
- Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
- Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
- Using the Backstage Proxy from Within a Plugin: 'tutorials/using-backstage-proxy-within-plugin.md'
- Architecture Decision Records (ADRs):
- Overview: 'architecture-decisions/index.md'
- ADR001 - Architecture Decision Record (ADR) log: 'architecture-decisions/adr001-add-adr-log.md'
+2 -2
View File
@@ -13,10 +13,10 @@
"build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts",
"build:api-docs": "yarn build:api-reports --docs",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"tsc:full": "backstage-cli clean && tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
"test": "lerna run test --since origin/master -- --coverage",
"test": "backstage-cli test",
"test:all": "lerna run test -- --coverage",
"lint": "lerna run lint --since origin/master --",
"lint:docs": "node ./scripts/check-docs-quality",
+84
View File
@@ -1,5 +1,89 @@
# example-app
## 0.2.49
### Patch Changes
- Updated dependencies
- @backstage/cli@0.7.15
- @backstage/core-components@0.6.1
- @backstage/core-plugin-api@0.1.10
- @backstage/core-app-api@0.1.16
- @backstage/plugin-org@0.3.26
- @backstage/plugin-catalog@0.7.0
- @backstage/plugin-catalog-react@0.5.2
- @backstage/catalog-model@0.9.4
- @backstage/plugin-cost-insights@0.11.9
- @backstage/plugin-user-settings@0.3.8
- @backstage/plugin-kubernetes@0.4.16
- @backstage/plugin-catalog-import@0.7.1
- @backstage/plugin-badges@0.2.12
- @backstage/plugin-home@0.4.3
- @backstage/plugin-search@0.4.14
- @backstage/plugin-shortcuts@0.1.11
- @backstage/plugin-api-docs@0.6.11
- @backstage/plugin-catalog-graph@0.1.3
- @backstage/plugin-circleci@0.2.26
- @backstage/plugin-cloudbuild@0.2.26
- @backstage/plugin-code-coverage@0.1.14
- @backstage/plugin-explore@0.3.19
- @backstage/plugin-gcp-projects@0.3.7
- @backstage/plugin-github-actions@0.4.21
- @backstage/plugin-graphiql@0.2.19
- @backstage/plugin-jenkins@0.5.9
- @backstage/plugin-kafka@0.2.18
- @backstage/plugin-lighthouse@0.2.28
- @backstage/plugin-newrelic@0.3.7
- @backstage/plugin-pagerduty@0.3.16
- @backstage/plugin-rollbar@0.3.17
- @backstage/plugin-scaffolder@0.11.7
- @backstage/plugin-sentry@0.3.24
- @backstage/plugin-tech-radar@0.4.10
- @backstage/plugin-techdocs@0.12.1
- @backstage/plugin-todo@0.1.13
## 0.2.48
### Patch Changes
- Updated dependencies
- @backstage/cli@0.7.14
- @backstage/plugin-techdocs@0.12.0
- @backstage/plugin-user-settings@0.3.7
- @backstage/core-app-api@0.1.15
- @backstage/plugin-catalog-import@0.7.0
- @backstage/plugin-badges@0.2.11
- @backstage/plugin-cost-insights@0.11.8
- @backstage/plugin-tech-radar@0.4.9
- @backstage/core-plugin-api@0.1.9
- @backstage/plugin-kubernetes@0.4.15
- @backstage/core-components@0.6.0
- @backstage/integration-react@0.1.11
- @backstage/plugin-catalog@0.6.17
- @backstage/plugin-api-docs@0.6.10
- @backstage/plugin-catalog-graph@0.1.2
- @backstage/plugin-catalog-react@0.5.1
- @backstage/plugin-circleci@0.2.25
- @backstage/plugin-cloudbuild@0.2.25
- @backstage/plugin-code-coverage@0.1.13
- @backstage/plugin-explore@0.3.18
- @backstage/plugin-gcp-projects@0.3.6
- @backstage/plugin-github-actions@0.4.20
- @backstage/plugin-graphiql@0.2.18
- @backstage/plugin-home@0.4.2
- @backstage/plugin-jenkins@0.5.8
- @backstage/plugin-kafka@0.2.17
- @backstage/plugin-lighthouse@0.2.27
- @backstage/plugin-newrelic@0.3.6
- @backstage/plugin-org@0.3.25
- @backstage/plugin-pagerduty@0.3.15
- @backstage/plugin-rollbar@0.3.16
- @backstage/plugin-scaffolder@0.11.6
- @backstage/plugin-search@0.4.13
- @backstage/plugin-sentry@0.3.23
- @backstage/plugin-shortcuts@0.1.10
- @backstage/plugin-todo@0.1.12
## 0.2.47
### Patch Changes
+39 -39
View File
@@ -1,46 +1,46 @@
{
"name": "example-app",
"version": "0.2.47",
"version": "0.2.49",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.9.3",
"@backstage/cli": "^0.7.13",
"@backstage/core-app-api": "^0.1.14",
"@backstage/core-components": "^0.5.0",
"@backstage/core-plugin-api": "^0.1.8",
"@backstage/integration-react": "^0.1.10",
"@backstage/plugin-api-docs": "^0.6.9",
"@backstage/plugin-badges": "^0.2.10",
"@backstage/plugin-catalog": "^0.6.16",
"@backstage/plugin-catalog-graph": "^0.1.1",
"@backstage/plugin-catalog-import": "^0.6.0",
"@backstage/plugin-catalog-react": "^0.5.0",
"@backstage/plugin-circleci": "^0.2.24",
"@backstage/plugin-cloudbuild": "^0.2.24",
"@backstage/plugin-code-coverage": "^0.1.12",
"@backstage/plugin-cost-insights": "^0.11.7",
"@backstage/plugin-explore": "^0.3.17",
"@backstage/plugin-gcp-projects": "^0.3.5",
"@backstage/plugin-github-actions": "^0.4.19",
"@backstage/plugin-graphiql": "^0.2.17",
"@backstage/plugin-home": "^0.4.1",
"@backstage/plugin-jenkins": "^0.5.7",
"@backstage/plugin-kafka": "^0.2.16",
"@backstage/plugin-kubernetes": "^0.4.14",
"@backstage/plugin-lighthouse": "^0.2.26",
"@backstage/plugin-newrelic": "^0.3.5",
"@backstage/plugin-org": "^0.3.24",
"@backstage/plugin-pagerduty": "0.3.14",
"@backstage/plugin-rollbar": "^0.3.15",
"@backstage/plugin-scaffolder": "^0.11.5",
"@backstage/plugin-search": "^0.4.12",
"@backstage/plugin-sentry": "^0.3.22",
"@backstage/plugin-shortcuts": "^0.1.9",
"@backstage/plugin-tech-radar": "^0.4.8",
"@backstage/plugin-techdocs": "^0.11.3",
"@backstage/plugin-todo": "^0.1.11",
"@backstage/plugin-user-settings": "^0.3.6",
"@backstage/catalog-model": "^0.9.4",
"@backstage/cli": "^0.7.15",
"@backstage/core-app-api": "^0.1.16",
"@backstage/core-components": "^0.6.1",
"@backstage/core-plugin-api": "^0.1.10",
"@backstage/integration-react": "^0.1.11",
"@backstage/plugin-api-docs": "^0.6.11",
"@backstage/plugin-badges": "^0.2.12",
"@backstage/plugin-catalog": "^0.7.0",
"@backstage/plugin-catalog-graph": "^0.1.3",
"@backstage/plugin-catalog-import": "^0.7.1",
"@backstage/plugin-catalog-react": "^0.5.2",
"@backstage/plugin-circleci": "^0.2.26",
"@backstage/plugin-cloudbuild": "^0.2.26",
"@backstage/plugin-code-coverage": "^0.1.14",
"@backstage/plugin-cost-insights": "^0.11.9",
"@backstage/plugin-explore": "^0.3.19",
"@backstage/plugin-gcp-projects": "^0.3.7",
"@backstage/plugin-github-actions": "^0.4.21",
"@backstage/plugin-graphiql": "^0.2.19",
"@backstage/plugin-home": "^0.4.3",
"@backstage/plugin-jenkins": "^0.5.9",
"@backstage/plugin-kafka": "^0.2.18",
"@backstage/plugin-kubernetes": "^0.4.16",
"@backstage/plugin-lighthouse": "^0.2.28",
"@backstage/plugin-newrelic": "^0.3.7",
"@backstage/plugin-org": "^0.3.26",
"@backstage/plugin-pagerduty": "0.3.16",
"@backstage/plugin-rollbar": "^0.3.17",
"@backstage/plugin-scaffolder": "^0.11.7",
"@backstage/plugin-search": "^0.4.14",
"@backstage/plugin-sentry": "^0.3.24",
"@backstage/plugin-shortcuts": "^0.1.11",
"@backstage/plugin-tech-radar": "^0.4.10",
"@backstage/plugin-techdocs": "^0.12.1",
"@backstage/plugin-todo": "^0.1.13",
"@backstage/plugin-user-settings": "^0.3.8",
"@backstage/search-common": "^0.2.0",
"@backstage/theme": "^0.2.10",
"@material-ui/core": "^4.12.2",
@@ -62,7 +62,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.17",
"@backstage/test-utils": "^0.1.18",
"@rjsf/core": "^3.0.0",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
+5 -1
View File
@@ -84,6 +84,8 @@ import { searchPage } from './components/search/SearchPage';
import { providers } from './identityProviders';
import * as plugins from './plugins';
import { techDocsPage } from './components/techdocs/TechDocsPage';
const app = createApp({
apis,
plugins: Object.values(plugins),
@@ -170,7 +172,9 @@ const routes = (
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
>
{techDocsPage}
</Route>
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
@@ -26,7 +26,7 @@ export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide(
name: 'LowerCaseValuePicker',
component: TextValuePicker,
validation: (value: string, validation: FieldValidation) => {
if (value.toLowerCase() !== value) {
if (value.toLocaleLowerCase('en-US') !== value) {
validation.addError('Only lowercase values are allowed.');
}
},
@@ -0,0 +1,44 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Content } from '@backstage/core-components';
import {
TechDocsPageHeader,
TechDocsPage,
Reader,
} from '@backstage/plugin-techdocs';
import React from 'react';
const DefaultTechDocsPage = () => {
return (
<TechDocsPage>
{({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => (
<>
<TechDocsPageHeader
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityRef={entityRef}
/>
<Content data-testid="techdocs-content">
<Reader onReady={onReady} entityRef={entityRef} />
</Content>
</>
)}
</TechDocsPage>
);
};
export const techDocsPage = <DefaultTechDocsPage />;
+7
View File
@@ -24,6 +24,7 @@ import {
oneloginAuthApiRef,
oauth2ApiRef,
oidcAuthApiRef,
bitbucketAuthApiRef,
} from '@backstage/core-plugin-api';
export const providers = [
@@ -81,4 +82,10 @@ export const providers = [
message: 'Sign In using OneLogin',
apiRef: oneloginAuthApiRef,
},
{
id: 'bitbucket-auth-provider',
title: 'Bitbucket',
message: 'Sign In using Bitbucket',
apiRef: bitbucketAuthApiRef,
},
];
+32
View File
@@ -1,5 +1,37 @@
# @backstage/backend-common
## 0.9.6
### Patch Changes
- 8f969d5a56: Correct error message typo
- a31afc5b62: Replace slash stripping regexp with trimEnd to remove CodeQL warning
- d7055285de: Add glob patterns support to config CORS options. It's possible to send patterns like:
```yaml
backend:
cors:
origin:
- https://*.my-domain.com
- http://localhost:700[0-9]
- https://sub-domain-+([0-9]).my-domain.com
```
- Updated dependencies
- @backstage/config-loader@0.6.10
- @backstage/integration@0.6.7
- @backstage/cli-common@0.1.4
## 0.9.5
### Patch Changes
- 8bb3c0a578: The `subscribe` method on the `Config` returned by `loadBackendConfig` is now forwarded through `getConfig` and `getOptionalConfig`.
- 0c8a59e293: Fix an issue where filtering in search doesn't work correctly for Bitbucket.
- Updated dependencies
- @backstage/integration@0.6.6
- @backstage/config-loader@0.6.9
## 0.9.4
### Patch Changes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.9.4",
"version": "0.9.6",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,11 +29,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.3",
"@backstage/cli-common": "^0.1.4",
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.6.8",
"@backstage/config-loader": "^0.6.10",
"@backstage/errors": "^0.1.2",
"@backstage/integration": "^0.6.5",
"@backstage/integration": "^0.6.7",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
@@ -77,8 +77,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.7.13",
"@backstage/test-utils": "^0.1.17",
"@backstage/cli": "^0.7.15",
"@backstage/test-utils": "^0.1.18",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -25,6 +25,7 @@ import {
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { trimEnd } from 'lodash';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import {
@@ -149,7 +150,7 @@ export class BitbucketUrlReader implements UrlReader {
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
const treeUrl = trimEnd(url.replace(filepath, ''), '/');
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
@@ -37,6 +37,7 @@ import {
ReadUrlResponse,
ReadUrlOptions,
} from './types';
import { trimEnd } from 'lodash';
/** @public */
export class GitlabUrlReader implements UrlReader {
@@ -186,7 +187,7 @@ export class GitlabUrlReader implements UrlReader {
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
const treeUrl = trimEnd(url.replace(filepath, ''), '/');
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { readCspOptions } from './config';
import { readCorsOptions, readCspOptions } from './config';
describe('config', () => {
describe('readCspOptions', () => {
@@ -42,4 +42,67 @@ describe('config', () => {
expect(() => readCspOptions(config)).toThrow(/wanted string-array/);
});
});
describe('readCorsOptions', () => {
it('reads single string', () => {
const mockCallback = jest.fn();
const config = new ConfigReader({ cors: { origin: 'https://*.value*' } });
const cors = readCorsOptions(config);
expect(cors).toEqual(
expect.objectContaining({
origin: expect.any(Function),
}),
);
const origin = cors?.origin as Function;
origin('https://a.value', mockCallback); // valid origin
origin('http://a.value', mockCallback); // invalid origin
origin(undefined, mockCallback); // when not origin needs to reject the call
expect(mockCallback.mock.calls[0][0]).toBe(null);
expect(mockCallback.mock.calls[1][0]).toBe(null);
expect(mockCallback.mock.calls[0][1]).toBe(true);
expect(mockCallback.mock.calls[1][1]).toBe(false);
expect(mockCallback.mock.calls[2][1]).toBe(false);
});
it('reads string array', () => {
const mockCallback = jest.fn();
const config = new ConfigReader({
cors: {
origin: ['http?(s)://*.value?(-+([0-9])).com', 'http://*.value'],
},
});
const cors = readCorsOptions(config);
expect(cors).toEqual(
expect.objectContaining({
origin: expect.any(Function),
}),
);
const origin = cors?.origin as Function;
origin('https://a.b.c.value-9.com', mockCallback);
origin('http://a.value-999.com', mockCallback);
origin('http://a.value', mockCallback);
origin('http://a.valuex', mockCallback);
expect(mockCallback.mock.calls[0][0]).toBe(null);
expect(mockCallback.mock.calls[1][0]).toBe(null);
expect(mockCallback.mock.calls[2][0]).toBe(null);
expect(mockCallback.mock.calls[3][0]).toBe(null);
expect(mockCallback.mock.calls[0][1]).toBe(true);
expect(mockCallback.mock.calls[1][1]).toBe(true);
expect(mockCallback.mock.calls[2][1]).toBe(true);
expect(mockCallback.mock.calls[3][1]).toBe(false);
});
it('reads undefined origin', () => {
const config = new ConfigReader({
cors: {},
});
const cors = readCorsOptions(config);
expect(cors).toEqual(expect.objectContaining({}));
expect(cors?.origin).toBeUndefined();
});
});
});
@@ -16,6 +16,7 @@
import { Config } from '@backstage/config';
import { CorsOptions } from 'cors';
import { Minimatch } from 'minimatch';
export type BaseOptions = {
listenPort?: string | number;
@@ -46,6 +47,13 @@ export type CertificateAttributes = {
*/
export type CspOptions = Record<string, string[]>;
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
type CustomOrigin = (
requestOrigin: string | undefined,
callback: (err: Error | null, origin?: StaticOrigin) => void,
) => void;
/**
* Reads some base options out of a config object.
*
@@ -78,7 +86,7 @@ export function readBaseOptions(config: Config): BaseOptions {
typeof port !== 'string'
) {
throw new Error(
`Invalid type in config for key 'backend.listen.post', got ${typeof port}, wanted string or number`,
`Invalid type in config for key 'backend.listen.port', got ${typeof port}, wanted string or number`,
);
}
@@ -112,7 +120,7 @@ export function readCorsOptions(config: Config): CorsOptions | undefined {
}
return removeUnknown({
origin: getOptionalStringOrStrings(cc, 'origin'),
origin: createCorsOriginMatcher(getOptionalStringOrStrings(cc, 'origin')),
methods: getOptionalStringOrStrings(cc, 'methods'),
allowedHeaders: getOptionalStringOrStrings(cc, 'allowedHeaders'),
exposedHeaders: getOptionalStringOrStrings(cc, 'exposedHeaders'),
@@ -207,16 +215,45 @@ function getOptionalStringOrStrings(
key: string,
): string | string[] | undefined {
const value = config.getOptional(key);
if (
value === undefined ||
typeof value === 'string' ||
isStringArray(value)
) {
if (value === undefined || isStringOrStrings(value)) {
return value;
}
throw new Error(`Expected string or array of strings, got ${typeof value}`);
}
function createCorsOriginMatcher(
originValue: string | string[] | undefined,
): CustomOrigin | undefined {
if (originValue === undefined) {
return originValue;
}
if (!isStringOrStrings(originValue)) {
throw new Error(
`Expected string or array of strings, got ${typeof originValue}`,
);
}
const allowedOrigin =
typeof originValue === 'string' ? [originValue] : originValue;
const allowedOriginPatterns =
allowedOrigin?.map(
pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),
) ?? [];
return (origin, callback) => {
return callback(
null,
allowedOriginPatterns.some(pattern => pattern.match(origin ?? '')),
);
};
}
function isStringOrStrings(value: any): value is string | string[] {
return typeof value === 'string' || isStringArray(value);
}
function isStringArray(value: any): value is string[] {
if (!Array.isArray(value)) {
return false;
+32
View File
@@ -1,5 +1,37 @@
# example-backend
## 0.2.49
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog-backend@0.16.0
- @backstage/catalog-model@0.9.4
- @backstage/plugin-proxy-backend@0.2.13
- @backstage/plugin-auth-backend@0.4.3
- @backstage/backend-common@0.9.6
- @backstage/catalog-client@0.5.0
- @backstage/integration@0.6.7
- @backstage/plugin-scaffolder-backend@0.15.7
- example-app@0.2.49
- @backstage/plugin-badges-backend@0.1.11
- @backstage/plugin-code-coverage-backend@0.1.12
- @backstage/plugin-jenkins-backend@0.1.6
- @backstage/plugin-techdocs-backend@0.10.4
- @backstage/plugin-todo-backend@0.1.13
## 0.2.48
### Patch Changes
- Updated dependencies
- @backstage/backend-common@0.9.5
- @backstage/plugin-catalog-backend@0.15.0
- @backstage/plugin-azure-devops-backend@0.1.1
- @backstage/integration@0.6.6
- @backstage/plugin-auth-backend@0.4.2
- example-app@0.2.48
## 0.2.47
### Patch Changes
+17 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.47",
"version": "0.2.49",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,35 +24,36 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.9.4",
"@backstage/catalog-client": "^0.4.0",
"@backstage/catalog-model": "^0.9.3",
"@backstage/backend-common": "^0.9.6",
"@backstage/catalog-client": "^0.5.0",
"@backstage/catalog-model": "^0.9.4",
"@backstage/config": "^0.1.10",
"@backstage/integration": "^0.6.5",
"@backstage/integration": "^0.6.7",
"@backstage/plugin-app-backend": "^0.3.16",
"@backstage/plugin-auth-backend": "^0.4.1",
"@backstage/plugin-badges-backend": "^0.1.10",
"@backstage/plugin-catalog-backend": "^0.14.0",
"@backstage/plugin-code-coverage-backend": "^0.1.11",
"@backstage/plugin-auth-backend": "^0.4.3",
"@backstage/plugin-azure-devops-backend": "^0.1.1",
"@backstage/plugin-badges-backend": "^0.1.11",
"@backstage/plugin-catalog-backend": "^0.16.0",
"@backstage/plugin-code-coverage-backend": "^0.1.12",
"@backstage/plugin-graphql-backend": "^0.1.9",
"@backstage/plugin-jenkins-backend": "^0.1.5",
"@backstage/plugin-jenkins-backend": "^0.1.6",
"@backstage/plugin-kubernetes-backend": "^0.3.16",
"@backstage/plugin-kafka-backend": "^0.2.10",
"@backstage/plugin-proxy-backend": "^0.2.12",
"@backstage/plugin-proxy-backend": "^0.2.13",
"@backstage/plugin-rollbar-backend": "^0.1.15",
"@backstage/plugin-scaffolder-backend": "^0.15.6",
"@backstage/plugin-scaffolder-backend": "^0.15.7",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5",
"@backstage/plugin-search-backend": "^0.2.6",
"@backstage/plugin-search-backend-node": "^0.4.2",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4",
"@backstage/plugin-search-backend-module-pg": "^0.2.1",
"@backstage/plugin-techdocs-backend": "^0.10.3",
"@backstage/plugin-todo-backend": "^0.1.12",
"@backstage/plugin-techdocs-backend": "^0.10.4",
"@backstage/plugin-todo-backend": "^0.1.13",
"@gitbeaker/node": "^30.2.0",
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
"dockerode": "^3.2.1",
"example-app": "^0.2.47",
"example-app": "^0.2.49",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-prom-bundle": "^6.3.6",
@@ -64,7 +65,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.7.13",
"@backstage/cli": "^0.7.15",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
+3
View File
@@ -38,6 +38,7 @@ import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsInit, metricsHandler } from './metrics';
import auth from './plugins/auth';
import azureDevOps from './plugins/azuredevops';
import catalog from './plugins/catalog';
import codeCoverage from './plugins/codecoverage';
import kubernetes from './plugins/kubernetes';
@@ -94,6 +95,7 @@ async function main() {
);
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
@@ -112,6 +114,7 @@ async function main() {
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv));
apiRouter.use('/search', await search(searchEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/todo', await todo(todoEnv));
@@ -0,0 +1,26 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { createRouter } from '@backstage/plugin-azure-devops-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return await createRouter({ logger, config });
}
+17
View File
@@ -1,5 +1,22 @@
# @backstage/catalog-client
## 0.5.0
### Minor Changes
- bb0f6b8a0f: Updates the `<EntitySwitch if={asyncMethod}/>` to accept asynchronous `if` functions.
Adds the new `getEntityAncestors` method to `CatalogClient`.
Updates the `<EntityProcessingErrorsPanel />` to make use of the ancestry endpoint to display errors for entities further up the ancestry tree. This makes it easier to discover issues where for example the origin location has been removed or malformed.
`hasCatalogProcessingErrors()` is now changed to be asynchronous so any calls outside the already established entitySwitch need to be awaited.
### Patch Changes
- Updated dependencies
- @backstage/catalog-model@0.9.4
## 0.4.0
### Minor Changes
+24
View File
@@ -38,6 +38,11 @@ export interface CatalogApi {
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>>;
// (undocumented)
getEntityAncestors(
request: CatalogEntityAncestorsRequest,
options?: CatalogRequestOptions,
): Promise<CatalogEntityAncestorsResponse>;
// (undocumented)
getEntityByName(
name: EntityName,
options?: CatalogRequestOptions,
@@ -88,6 +93,11 @@ export class CatalogClient implements CatalogApi {
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>>;
// (undocumented)
getEntityAncestors(
request: CatalogEntityAncestorsRequest,
options?: CatalogRequestOptions,
): Promise<CatalogEntityAncestorsResponse>;
// (undocumented)
getEntityByName(
compoundName: EntityName,
options?: CatalogRequestOptions,
@@ -133,6 +143,20 @@ export type CatalogEntitiesRequest = {
fields?: string[] | undefined;
};
// @public (undocumented)
export type CatalogEntityAncestorsRequest = {
entityRef: string;
};
// @public (undocumented)
export type CatalogEntityAncestorsResponse = {
root: EntityName;
items: {
entity: Entity;
parents: EntityName[];
}[];
};
// @public (undocumented)
export type CatalogListResponse<T> = {
items: T[];
+3 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-client",
"description": "An isomorphic client for the catalog backend",
"version": "0.4.0",
"version": "0.5.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,13 +30,12 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.9.3",
"@backstage/config": "^0.1.10",
"@backstage/catalog-model": "^0.9.4",
"@backstage/errors": "^0.1.2",
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/cli": "^0.7.13",
"@backstage/cli": "^0.7.15",
"@types/jest": "^26.0.7",
"msw": "^0.29.0"
},
@@ -20,6 +20,7 @@ import {
Location,
LOCATION_ANNOTATION,
ORIGIN_LOCATION_ANNOTATION,
parseEntityRef,
stringifyEntityRef,
stringifyLocationReference,
} from '@backstage/catalog-model';
@@ -33,6 +34,8 @@ import {
CatalogEntitiesRequest,
CatalogListResponse,
CatalogRequestOptions,
CatalogEntityAncestorsRequest,
CatalogEntityAncestorsResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
@@ -44,6 +47,20 @@ export class CatalogClient implements CatalogApi {
this.discoveryApi = options.discoveryApi;
}
async getEntityAncestors(
request: CatalogEntityAncestorsRequest,
options?: CatalogRequestOptions,
): Promise<CatalogEntityAncestorsResponse> {
const { kind, namespace, name } = parseEntityRef(request.entityRef);
return await this.requestRequired(
'GET',
`/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent(
namespace,
)}/${encodeURIComponent(name)}/ancestry`,
options,
);
}
async getLocationById(
id: string,
options?: CatalogRequestOptions,
+15
View File
@@ -28,6 +28,17 @@ export type CatalogEntitiesRequest = {
fields?: string[] | undefined;
};
/** @public */
export type CatalogEntityAncestorsRequest = {
entityRef: string;
};
/** @public */
export type CatalogEntityAncestorsResponse = {
root: EntityName;
items: { entity: Entity; parents: EntityName[] }[];
};
/** @public */
export type CatalogListResponse<T> = {
items: T[];
@@ -45,6 +56,10 @@ export interface CatalogApi {
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>>;
getEntityAncestors(
request: CatalogEntityAncestorsRequest,
options?: CatalogRequestOptions,
): Promise<CatalogEntityAncestorsResponse>;
getEntityByName(
name: EntityName,
options?: CatalogRequestOptions,
@@ -21,6 +21,8 @@ export type {
CatalogEntitiesRequest,
CatalogListResponse,
CatalogRequestOptions,
CatalogEntityAncestorsRequest,
CatalogEntityAncestorsResponse,
} from './api';
export type { DiscoveryApi } from './discovery';
export { CATALOG_FILTER_EXISTS } from './api';
+7
View File
@@ -1,5 +1,12 @@
# @backstage/catalog-model
## 0.9.4
### Patch Changes
- 957e4b3351: Updated dependencies
- ca0559444c: Avoid usage of `.to*Case()`, preferring `.toLocale*Case('en-US')` instead.
## 0.9.3
### Patch Changes
+5 -8
View File
@@ -10,12 +10,9 @@ import { SerializedError } from '@backstage/errors';
import * as yup from 'yup';
// @public @deprecated (undocumented)
export const analyzeLocationSchema: yup.ObjectSchema<
{
location: LocationSpec;
},
object
>;
export const analyzeLocationSchema: yup.SchemaOf<{
location: LocationSpec;
}>;
// @public (undocumented)
interface ApiEntityV1alpha1 extends Entity {
@@ -337,7 +334,7 @@ export { LocationEntityV1alpha1 };
export const locationEntityV1alpha1Validator: KindValidator;
// @public @deprecated (undocumented)
export const locationSchema: yup.ObjectSchema<Location_2, object>;
export const locationSchema: yup.SchemaOf<Location_2>;
// @public (undocumented)
export type LocationSpec = {
@@ -347,7 +344,7 @@ export type LocationSpec = {
};
// @public @deprecated (undocumented)
export const locationSpecSchema: yup.ObjectSchema<LocationSpec, object>;
export const locationSpecSchema: yup.SchemaOf<LocationSpec>;
// @public (undocumented)
export function makeValidator(overrides?: Partial<Validators>): Validators;
+4 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-model",
"description": "Types and validators that help describe the model of a Backstage Catalog",
"version": "0.9.3",
"version": "0.9.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,16 +33,15 @@
"@backstage/config": "^0.1.10",
"@backstage/errors": "^0.1.2",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.8",
"@types/yup": "^0.29.13",
"ajv": "^7.0.3",
"json-schema": "^0.3.0",
"lodash": "^4.17.21",
"uuid": "^8.0.0",
"yup": "^0.29.3"
"yup": "^0.32.9"
},
"devDependencies": {
"@backstage/cli": "^0.7.13",
"@types/express": "^4.17.6",
"@backstage/cli": "^0.7.15",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
+8 -4
View File
@@ -248,7 +248,9 @@ export function stringifyEntityRef(
name = ref.name;
}
return `${kind.toLowerCase()}:${namespace.toLowerCase()}/${name.toLowerCase()}`;
return `${kind.toLocaleLowerCase('en-US')}:${namespace.toLocaleLowerCase(
'en-US',
)}/${name.toLocaleLowerCase('en-US')}`;
}
/**
@@ -296,8 +298,10 @@ export function compareEntityToRef(
}
return (
entityKind.toLowerCase() === refKind.toLowerCase() &&
entityNamespace.toLowerCase() === refNamespace.toLowerCase() &&
entityName.toLowerCase() === refName.toLowerCase()
entityKind.toLocaleLowerCase('en-US') ===
refKind.toLocaleLowerCase('en-US') &&
entityNamespace.toLocaleLowerCase('en-US') ===
refNamespace.toLocaleLowerCase('en-US') &&
entityName.toLocaleLowerCase('en-US') === refName.toLocaleLowerCase('en-US')
);
}
@@ -21,11 +21,11 @@ import { LocationSpec, Location } from './types';
* @public
* @deprecated Use JSONSchema or validators instead.
*/
export const locationSpecSchema = yup
.object<LocationSpec>({
export const locationSpecSchema: yup.SchemaOf<LocationSpec> = yup
.object({
type: yup.string().required(),
target: yup.string().required(),
presence: yup.string(),
presence: yup.mixed().oneOf(['required', 'optional']),
})
.noUnknown()
.required();
@@ -34,11 +34,12 @@ export const locationSpecSchema = yup
* @public
* @deprecated Use JSONSchema or validators instead.
*/
export const locationSchema = yup
.object<Location>({
export const locationSchema: yup.SchemaOf<Location> = yup
.object({
id: yup.string().required(),
type: yup.string().required(),
target: yup.string().required(),
presence: yup.mixed().oneOf(['required', 'optional']),
})
.noUnknown()
.required();
@@ -47,9 +48,10 @@ export const locationSchema = yup
* @public
* @deprecated Use JSONSchema or validators instead.
*/
export const analyzeLocationSchema = yup
.object<{ location: LocationSpec }>({
location: locationSpecSchema,
})
.noUnknown()
.required();
export const analyzeLocationSchema: yup.SchemaOf<{ location: LocationSpec }> =
yup
.object({
location: locationSpecSchema,
})
.noUnknown()
.required();
+6
View File
@@ -1,5 +1,11 @@
# @backstage/cli-common
## 0.1.4
### Patch Changes
- ca0559444c: Avoid usage of `.to*Case()`, preferring `.toLocale*Case('en-US')` instead.
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.3",
"version": "0.1.4",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+1 -1
View File
@@ -115,7 +115,7 @@ export function findPaths(searchDir: string): Paths {
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
const targetDir = fs
.realpathSync(process.cwd())
.replace(/^[a-z]:/, str => str.toUpperCase());
.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
+25
View File
@@ -1,5 +1,30 @@
# @backstage/cli
## 0.7.15
### Patch Changes
- ae4680b88d: The `create-plugin` command now passes the extension name via the `name` key
in `createRoutableExtension()` calls in newly created plugins.
- df1242ffe4: Adding `--inspect-brk` as an option when debugging backend for development
- c7f2a2307d: When creating a backend plugin with `--backend` flag, don't add `-backend` if it's already suffixed
- 185fec5c0c: The default jest configuration used by the `test` command now supports yarn workspaces. By running `backstage-cli test` in the root of a monorepo, all packages will now automatically be included in the test suite and it will run just like it does within a package. Each package in the monorepo will still use its own local jest configuration, and only packages that have `backstage-cli test` in the `test` script within `package.json` will be included.
- Updated dependencies
- @backstage/config-loader@0.6.10
- @backstage/cli-common@0.1.4
## 0.7.14
### Patch Changes
- 3a8704f16b: Only serve static assets if there is a public folder during `app:serve` and `plugin:serve`. This fixes a common bug that would break `plugin:serve` with an `EBUSY` error.
- 40199b61d6: Configuration schema is now also collected from the root `package.json` if it exists.
- 2a6c393c06: The `create-plugin` command now prefers dependency versions ranges that are already in the lockfile.
- 58f91943ab: Improved ´plugin:diff´ check for the `package.json` `"files"` field.
- 12e074a6e4: Fix duplication checks to stop looking for the old core packages, and to allow some explicitly
- Updated dependencies
- @backstage/config-loader@0.6.9
## 0.7.13
### Patch Changes
+63 -10
View File
@@ -16,20 +16,23 @@
const fs = require('fs-extra');
const path = require('path');
const glob = require('util').promisify(require('glob'));
async function getConfig() {
async function getProjectConfig(targetPath) {
const configJsPath = path.resolve(targetPath, 'jest.config.js');
const configTsPath = path.resolve(targetPath, 'jest.config.ts');
// If the package has it's own jest config, we use that instead.
if (await fs.pathExists('jest.config.js')) {
return require(path.resolve('jest.config.js'));
} else if (await fs.pathExists('jest.config.ts')) {
return require(path.resolve('jest.config.ts'));
if (await fs.pathExists(configJsPath)) {
return require(configJsPath);
} else if (await fs.pathExists(configTsPath)) {
return require(configTsPath);
}
// We read all "jest" config fields in package.json files all the way to the filesystem root.
// All configs are merged together to create the final config, with longer paths taking precedence.
// The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
const pkgJsonConfigs = [];
let currentPath = process.cwd();
let currentPath = targetPath;
// Some sanity check to avoid infinite loop
for (let i = 0; i < 100; i++) {
@@ -70,8 +73,8 @@ async function getConfig() {
const transformModulePattern = transformModules && `(?!${transformModules})`;
const options = {
rootDir: path.resolve('src'),
coverageDirectory: path.resolve('coverage'),
rootDir: path.resolve(targetPath, 'src'),
coverageDirectory: path.resolve(targetPath, 'coverage'),
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
@@ -96,11 +99,61 @@ async function getConfig() {
};
// Use src/setupTests.ts as the default location for configuring test env
if (fs.existsSync('src/setupTests.ts')) {
if (fs.existsSync(path.resolve(targetPath, 'src/setupTests.ts'))) {
options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
}
return Object.assign(options, ...pkgJsonConfigs);
}
module.exports = getConfig();
// This loads the root jest config, which in turn will either refer to a single
// configuration for the current package, or a collection of configurations for
// the target workspace packages
async function getRootConfig() {
const targetPath = process.cwd();
const targetPackagePath = path.resolve(targetPath, 'package.json');
const exists = await fs.pathExists(targetPackagePath);
if (!exists) {
return getProjectConfig(targetPath);
}
// Check whether the current package is a workspace root or not
const data = await fs.readJson(targetPackagePath);
const workspacePatterns = data.workspaces && data.workspaces.packages;
if (!workspacePatterns) {
return getProjectConfig(targetPath);
}
// If the target package is a workspace root, we find all packages in the
// workspace and load those in as separate jest projects instead.
const projectPaths = await Promise.all(
workspacePatterns.map(pattern => glob(path.join(targetPath, pattern))),
).then(_ => _.flat());
const configs = await Promise.all(
projectPaths.flat().map(async projectPath => {
const packagePath = path.resolve(projectPath, 'package.json');
if (!(await fs.pathExists(packagePath))) {
return undefined;
}
// We check for the presence of "backstage-cli test" in the package test
// script to determine whether a given package should be tested
const packageData = await fs.readJson(packagePath);
const testScript = packageData.scripts && packageData.scripts.test;
if (testScript && testScript.includes('backstage-cli test')) {
return await getProjectConfig(projectPath);
}
return undefined;
}),
).then(cs => cs.filter(Boolean));
return {
rootDir: targetPath,
projects: configs,
};
}
module.exports = getRootConfig();
+10 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.7.13",
"version": "0.7.15",
"private": false,
"publishConfig": {
"access": "public"
@@ -30,9 +30,9 @@
"dependencies": {
"@babel/core": "^7.4.4",
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.3",
"@backstage/cli-common": "^0.1.4",
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.6.8",
"@backstage/config-loader": "^0.6.10",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -76,6 +76,7 @@
"express": "^4.17.1",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "9.1.0",
"glob": "^7.1.7",
"handlebars": "^4.7.3",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^7.0.4",
@@ -117,13 +118,13 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.9.4",
"@backstage/backend-common": "^0.9.6",
"@backstage/config": "^0.1.10",
"@backstage/core-components": "^0.5.0",
"@backstage/core-plugin-api": "^0.1.8",
"@backstage/core-app-api": "^0.1.14",
"@backstage/dev-utils": "^0.2.10",
"@backstage/test-utils": "^0.1.17",
"@backstage/core-components": "^0.6.1",
"@backstage/core-plugin-api": "^0.1.10",
"@backstage/core-app-api": "^0.1.16",
"@backstage/dev-utils": "^0.2.11",
"@backstage/test-utils": "^0.1.18",
"@backstage/theme": "^0.2.10",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",

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