Merge remote-tracking branch 'origin/master' into feat/resolve-refs-in-yaml

This commit is contained in:
Thorsten Hake
2022-11-22 09:10:59 +01:00
277 changed files with 5740 additions and 3153 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-catalog-graph': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-splunk-on-call': patch
'@backstage/plugin-techdocs-react': patch
---
Internal refactor to improve tests
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': minor
---
**BREAKING PRODUCERS**: Added a new `getEntitiesByRefs` endpoint to `CatalogApi`, for efficient batch fetching of entities by ref.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
changed tests created by create-plugin to follow eslint-rules best practices particularly testing-library/prefer-screen-queries and testing-library/render-result-naming-convention
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog-backend': minor
---
Added new `POST /entities/by-refs` endpoint, which allows you to efficiently
batch-fetch entities by their entity ref. This can be useful e.g. in graphql
resolvers or similar contexts where you need to fetch many entities at the same
time.
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/plugin-events-backend': minor
---
**BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig`
and replace it with `bind(router: Router)`.
Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`.
```diff
// at packages/backend/src/plugins/events.ts
const eventsRouter = Router();
- const httpRouter = Router();
- eventsRouter.use('/http', httpRouter);
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
logger: env.logger,
- router: httpRouter,
});
+ http.bind(eventsRouter);
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Use Json types from @backstage/types
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Wait for indexer initialization before finalizing indexing.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Create a variable for minimum height and add a prop named 'fit' for determining if the graph height should grow or be contained.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-user-settings': minor
---
Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-explore': patch
---
Adds styling to graph forcing it to always fill out the available space.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-events-backend': patch
'@backstage/plugin-events-node': minor
---
Introduce a new interface `RequestDetails` to abstract `Request`
providing access to request body and headers.
**BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Added `lifecycleFactory` implementation.
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/core-app-api': patch
'@backstage/core-components': patch
'@backstage/core-plugin-api': patch
'@backstage/dev-utils': patch
'@backstage/test-utils': patch
'@backstage/types': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-shortcuts': patch
'@backstage/plugin-user-settings': patch
---
Updated dependency `zen-observable` to `^0.9.0`.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-components': patch
'@backstage/plugin-codescene': patch
'@backstage/plugin-sonarqube': patch
---
Updated dependency `rc-progress` to `3.4.1`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Updated indexer and decorator base classes to take advantage of features introduced in Node.js v16; be sure you are running a [supported version of Node.js](https://backstage.io/docs/releases/v1.8.0#node-16-and-18).
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-common': patch
'@backstage/core-components': patch
---
Internal refactor to avoid usage of deprecated symbols.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Registered shutdown hook in experimental catalog plugin.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-github': patch
---
Fix incorrectly exported GithubOrgEntityProvider as a type
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Implement Custom Field Explorer to view and play around with available installed custom field extensions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
fixed `headerOptions` not passed to `TemplatePage` component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Fixed Entity kind pluralisation in the `CatalogKindHeader` component.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-vault-backend': patch
---
Use `express-promise-router` to catch errors properly.
Add `403` error as a known one. It will now return a `NotAllowed` error.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Added `lifecycleFactory` to default service factories.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Added initial support for registering shutdown hooks via `lifecycleServiceRef`.
+6 -1
View File
@@ -18,7 +18,7 @@ var path = require('path');
module.exports = {
root: true,
plugins: ['notice'],
plugins: ['notice', 'testing-library'],
rules: {
'notice/notice': [
'error',
@@ -45,5 +45,10 @@ module.exports = {
"CallExpression[arguments.length=0] > MemberExpression[property.name='toUpperCase']",
},
],
'testing-library/await-async-query': 'error',
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/prefer-wait-for': 'error',
'testing-library/no-dom-import': 'error',
},
};
+3
View File
@@ -241,6 +241,7 @@ pageview
parallelization
parseable
Patrik
pattison
Peloton
performant
Performant
@@ -316,6 +317,7 @@ Spotifiers
spotify
Spotify
sqlite
sqs
squidfunk
src
statefulsets
@@ -406,3 +408,4 @@ zod
Zolotusky
zoomable
zsh
Lainfiesta
@@ -0,0 +1,63 @@
name: Automate merge message
on:
pull_request:
branches: ['master']
types: ['closed']
permissions:
pull-requests: write
actions: none
checks: none
contents: none
deployments: none
issues: none
packages: none
pages: none
repository-projects: none
security-events: none
statuses: none
jobs:
message:
# prevent running towards forks, and only run on merged PRs
if: github.repository == 'backstage/backstage' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: '${{ github.event.pull_request.merge_commit_sha }}'
- name: fetch base
run: git fetch --depth 1 origin ${{ github.event.pull_request.base.sha }}
# We avoid using the in-source script since this workflow has elevated permissions that we don't want to expose
- name: Generate Message
id: generate-message
run: |
rm -f generate.js
wget -O generate.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/generate-merge-message.js 1>&2
node generate.js FETCH_HEAD > message.txt
- name: Post Message
uses: actions/github-script@v6
env:
ISSUE_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const owner = "backstage";
const repo = "backstage";
const body = require('fs').readFileSync('message.txt', 'utf8').trim();
const issue_number = Number(process.env.ISSUE_NUMBER);
if (!body) {
console.log(`skipping comment for #${issue_number}`);
return;
}
console.log(`creating comment for #${issue_number}`);
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
- name: verify yarn dependency duplicates
run: yarn dedupe --check
run: node scripts/verify-lockfile-duplicates.js
- name: verify changesets
run: node scripts/verify-changesets.js
+3
View File
@@ -32,6 +32,9 @@ jobs:
run: yarn install --immutable
working-directory: microsite
- name: verify yarn dependency duplicates
run: node scripts/verify-lockfile-duplicates.js
- name: prettier
run: yarn prettier:check
working-directory: microsite
+31
View File
@@ -171,6 +171,37 @@ Exported types can be marked with either `@public`, `@alpha` or `@beta` release
If a package does not have this configuration, then all exported types are considered stable, even if they are marked as `@alpha` or `@beta`.
#### Changes that are Not Considered Breaking
There are a few exceptions that are not considered breaking in the [versioning policy](https://backstage.io/docs/overview/versioning-policy#changes-that-are-not-considered-breaking),
primarily regarding Utility APIs and Backend Service interfaces (referred to "contracts" below) that are supplied by the
Backstage core packages.
Example of a non-breaking change to a contract which has a default
implementation, since consumers typically only interact with that contract as
callers of existing methods:
```diff
export interface MyService {
oldMethod(): void;
+ newMethod(): void;
}
```
Changes such as these must still be marked with `**BREAKING PRODUCERS**:` in the
changelog, to highlight them for those who might be implementing custom
implementations of those contracts or putting mocks of the contract in tests.
Example of a breaking change to a contract, which affects existing consumers and
therefore makes it NOT fall under these exceptions:
```diff
export interface MyService {
- oldMethod(): void;
+ oldMethod(): Promise<void>;
}
```
#### Type Contract Direction
An important distinction to make when looking at changes to an API Report is the direction of the contract of a changed type, that is, whether it's used as input or output from the user's point of view. In the next two sections we'll dive into the different directions of a type contract, and how it affects whether a change is breaking or not.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+62 -2
View File
@@ -258,9 +258,58 @@ Passport-supported authentication method.
## Custom ScmAuthApi Implementation
If you are using any custom authentication providers, like for example one for GitHub Enterprise, then you are likely to need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed, and is used for example by the Scaffolder (Software Templates) and Catalog Import plugins.
The default `ScmAuthAPi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`:
To set up a custom `ScmAuthApi` implementation, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The following example shows an implementation that supports both public GitHub via `githubAuthApi` as well as a GitHub Enterprise installation hosted at `ghe.example.com` via `gheAuthApi`:
```ts
ScmAuth.createDefaultApiFactory();
```
If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins.
The first step is to remove the code that creates the default providers.
```diff
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
+ ScmAuth,
} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
...
+ ScmAuth.createDefaultApiFactory(),
...
];
```
Then replace it with something like this, which will create an `ApiFactory` with only a github provider.
```ts
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmAuthApiRef,
deps: {
githubAuthApi: githubAuthApiRef,
},
factory: ({ githubAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi),
),
});
```
If you use any custom authentication integrations, a new provider can be added to the `ApiFactory`.
The first step is to create a new authentication ref, which follows the naming convention of `xxxAuthApiRef`. The example below is for a new GitHub enterprise integration which can be defined either inside the app itself if it's only used for this purpose or inside a common internal package for APIs, such as `@internal/apis`:
```ts
const gheAuthApiRef: ApiRef<OAuthApi & ProfileInfoApi & SessionApi> =
createApiRef({
id: 'internal.auth.ghe',
});
```
The new ref is then used to add a new provider to the ApiFactory:
```ts
createApiFactory({
@@ -278,3 +327,14 @@ createApiFactory({
),
});
```
Finally, you also need to add and configure another provider to the `auth-backend` using the provider ID, which in this example is `ghe`:
```ts
import { providers } from '@backstage/plugin-auth-backend';
// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts
providerFactories: {
ghe: providers.github.create(),
},
```
+3 -3
View File
@@ -68,7 +68,7 @@ export const searchPage = (
switch (result.type) {
case 'software-catalog':
return (
<CatalogResultListItem
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
@@ -253,7 +253,7 @@ const MyCustomFilter = () => {
It's good practice for search results to highlight information that was used to
return it in the first place! The code below highlights how you might specify a
custom result item component, using the `<CatalogResultListItem />` component as
custom result item component, using the `<CatalogSearchResultListItem />` component as
an example:
```tsx {7-13}
@@ -265,7 +265,7 @@ an example:
switch (result.type) {
case 'software-catalog':
return (
<CatalogResultListItem
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
@@ -159,7 +159,12 @@ Check out the numbered markings - let's go through them one by one.
the outcome of that. This example issues a `fetch` to the right service and
issues a full refresh of its entity bucket based on that.
5. The method translates the foreign data model to the native `Entity` form, as
expected by the catalog.
expected by the catalog. The `Entity` must include the
`backstage.io/managed-by-location` and
`backstage.io/managed-by-origin-location annotations`; otherwise, it will not
appear in the Catalog and will generate warning logs. The
[Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location)
documentation has guidance on what values to use for these.
6. Finally, we issue a "mutation" to the catalog. This persists the entities in
our own bucket, along with an optional `locationKey` that's used for conflict
checks. But this is a bigger topic - mutations warrant their own explanatory
@@ -181,3 +181,102 @@ const CustomFieldExtension = scaffolderPlugin.provide(
})
);
```
## Previewing Custom Field Extensions
You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer
(accessible via the `/create/edit` route by default):
![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png)
In order to make your new custom field extension available in the explorer you will have to define a
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
properties: {
focused: {
description: 'Whether to focus this field',
type: 'boolean',
},
},
},
returnValue: { type: 'string' },
};
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
import { z } from 'zod';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: MyCustomExtensionWithOptionsProps) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
+92
View File
@@ -393,6 +393,98 @@ You can add more icons, if the [default icons](https://github.com/backstage/back
Note: If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon`
## Custom Sidebar
As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar.
### Sidebar Sub-menu
For this example we'll show you how you can expand the sidebar with a sub-menu:
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
2. Then we want to add the following imports for the icons:
```ts
import ApiIcon from '@material-ui/icons/Extension';
import ComponentIcon from '@material-ui/icons/Memory';
import DomainIcon from '@material-ui/icons/Apartment';
import ResourceIcon from '@material-ui/icons/Work';
import SystemIcon from '@material-ui/icons/Category';
import UserIcon from '@material-ui/icons/Person';
```
3. Then update the `@backstage/core-components` import like this:
```diff
import {
Sidebar,
sidebarConfig,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebarOpenState,
Link,
+ GroupIcon,
+ SidebarSubmenu,
+ SidebarSubmenuItem,
} from '@backstage/core-components';
```
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
```ts
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
<SidebarSubmenu title="Catalog">
<SidebarSubmenuItem
title="Domains"
to="catalog?filters[kind]=domain"
icon={DomainIcon}
/>
<SidebarSubmenuItem
title="Systems"
to="catalog?filters[kind]=system"
icon={SystemIcon}
/>
<SidebarSubmenuItem
title="Components"
to="catalog?filters[kind]=component"
icon={ComponentIcon}
/>
<SidebarSubmenuItem
title="APIs"
to="catalog?filters[kind]=api"
icon={ApiIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Resources"
to="catalog?filters[kind]=resource"
icon={ResourceIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Groups"
to="catalog?filters[kind]=group"
icon={GroupIcon}
/>
<SidebarSubmenuItem
title="Users"
to="catalog?filters[kind]=user"
icon={UserIcon}
/>
</SidebarSubmenu>
</SidebarItem>
```
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
![Sidebar sub-menu example](./../assets/getting-started/sidebar-submenu-example.png)
You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar)
## Custom Homepage
In addition to a custom theme, a custom logo, you can also customize the
+1
View File
@@ -34,6 +34,7 @@ guide to do a repository-based installation.
- [Package manager](https://nodejs.org/en/download/package-manager/)
- [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md)
- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install)
- You will need to use Yarn classic to create a new project, but it can then be [migrated to Yarn 3](../tutorials/yarn-migration.md)
- `docker` [installation](https://docs.docker.com/engine/install/)
- `git` [installation](https://github.com/git-guides/install-git)
- If the system is not directly accessible over your network the following ports
+20 -1
View File
@@ -99,7 +99,7 @@ This versioning is completely decoupled from the Backstage release versioning,
meaning you might for example have `@backstage/core-plugin-api` version `3.1.4`
be part of the `1.12` Backstage release.
Following versioning policy applies to all packages:
The following versioning policy applies to all packages:
- Breaking changes are noted in the changelog, and documentation is updated.
- Breaking changes are prefixed with `**BREAKING**: ` in the changelog.
@@ -120,6 +120,25 @@ For packages at version `1.0.0` or above, the following policy also applies:
changes without a deprecation period, but the changes must still adhere to
semver.
### Changes that are Not Considered Breaking
There are a few changes that would typically be considered breaking changes, but
that we make exceptions for. This is both to be able to evolve the project more
rapidly, also because the alternative ends up having a bigger impact on users.
For all Utility APIs and Backend Services that _have_ a built-in implementation,
we only consider the API stability for consumers of those interfaces. This means
that it is not considered a breaking change to break the contract for producers
of the interface.
Changes that fall under the above rule, must be marked with
`**BREAKING PRODUCERS**:` in the changelog.
For any case of dependency injection, it is not considered a breaking change to
add a dependency on a Utility API or Backend Service that is provided by the
framework. This includes any dependency that is provided by the
`@backstage/app-defaults` and `@backstage/backend-defaults` packages.
### Release Stages
The release stages(`@alpha`, `@beta` `@public`) refers to the
@@ -0,0 +1,16 @@
---
title: The Linux Foundation launches its Introduction to Backstage course
description: The new Linux Foundation course is available for free on edX and is designed to help individuals understand how to map Backstage to their organization needs
author: Jorge Lainfiesta, Roadie
authorURL: https://www.linkedin.com/in/jrlainfiesta/
---
Backstage continues to grow in [popularity and maturity](https://roadie.io/blog/backstage-consolidating-its-role-in-the-cloud-native-ecosystem/), with industry leaders not only adopting the framework but actively participating in the community through contributions and commercial offerings. One of the most recent contributions comes from the Linux Foundation, [launching an introductory course](https://training.linuxfoundation.org/blog/23107/) aimed at DevOps engineers or professionals working on Developer Productivity or Developer Experience. The course is available for free on edX, with a paid option for the certified track.
[![Introduction to Backstage: Developer Portals Made Easy (LFS142x): Enroll!](assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy)
<!--truncate-->
The course, [Introduction to Backstage: Developer Portals Made Easy (LFS142x)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy), starts discussing the benefits of adopting a Developer Portal. Then, it dives into what is Backstage, its main features (Catalog, Scaffolder, TechDocs), and how to map its capabilities to your organization. At last, it provides tips on how to connect with the community to ease up your adoption journey.
The course author, [Jorge Lainfiesta](https://www.linkedin.com/in/jrlainfiesta/), is a Technical Marketing Manager at [Roadie](https://roadie.io). Jorge has a background in software engineering (PayPal) and digital communication (UCLA). Hes been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+13
View File
@@ -0,0 +1,13 @@
---
title: Cloudsmith
author: Roadie
authorUrl: https://roadie.io
category: CI/CD
description: Show Cloudsmith Repository stats on your backstage homepage.
documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/frontend/backstage-plugin-cloudsmith
iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg
npmPackageName: '@roadiehq/backstage-plugin-cloudsmith'
tags:
- dashboards
- monitoring
addedDate: '2022-11-18'
+1 -1
View File
@@ -6,7 +6,7 @@ category: Deployment # A single category e.g. CI, Machine Learning, Services, Mo
description: |
Show workloads, environments and resources deployed by Humanitec Platform Orchestrator.
Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions.
documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec
documentation: https://github.com/thefrontside/playhouse/tree/main/plugins/humanitec
iconUrl: img/humanitec-logo.png
npmPackageName: '@frontside/backstage-plugin-humanitec'
addedDate: '2022-06-22'
+2 -2
View File
@@ -34,7 +34,7 @@ const Background = props => {
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.MediaFrame>
<iframe
src="https://www.youtube.com/embed/HJeIK0Ga_mE"
src="https://www.youtube.com/embed/q9EQLR1l2SM"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -45,7 +45,7 @@ const Background = props => {
<iframe
width="300"
height="500"
src="https://www.youtube.com/live_chat?v=HJeIK0Ga_mE&embed_domain=backstage.io&dark_theme=1"
src="https://www.youtube.com/live_chat?v=q9EQLR1l2SM&embed_domain=backstage.io&dark_theme=1"
></iframe>
</Block.MediaFrame>
</Block.Container>
+12 -87
View File
@@ -281,14 +281,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.12.11":
version: 7.12.11
resolution: "@babel/helper-validator-identifier@npm:7.12.11"
checksum: e604c6bf890704fc46c1ae13bf23afb242b810224ec3403bba67cdbf0d8dabfec4b82123d6dfb18135a0ee3f7f79218583c819363ebb5e04a0a49d8418db7fce
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.18.6":
"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/helper-validator-identifier@npm:7.18.6"
checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648
@@ -1241,18 +1234,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4":
version: 7.12.12
resolution: "@babel/types@npm:7.12.12"
dependencies:
"@babel/helper-validator-identifier": ^7.12.11
lodash: ^4.17.19
to-fast-properties: ^2.0.0
checksum: 1eefe9468573a32491030f2986f820eb4289ff0b93e437b97f665e73cc8dae0ec0bdd95f37995bb3b70b5ce2a9a45a302affbbad497150021cf874c5ef10e361
languageName: node
linkType: hard
"@babel/types@npm:^7.8.3":
"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.18.10
resolution: "@babel/types@npm:7.18.10"
dependencies:
@@ -2917,7 +2899,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.3.3":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -2929,7 +2911,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4.3.1, debug@npm:^4.1.0":
"debug@npm:4.3.1":
version: 4.3.1
resolution: "debug@npm:4.3.1"
dependencies:
@@ -4275,21 +4257,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.6":
version: 7.2.0
resolution: "glob@npm:7.2.0"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^3.0.4
once: ^1.3.0
path-is-absolute: ^1.0.0
checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134
languageName: node
linkType: hard
"glob@npm:^7.1.4":
"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6":
version: 7.2.3
resolution: "glob@npm:7.2.3"
dependencies:
@@ -4430,14 +4398,7 @@ __metadata:
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0":
version: 4.2.4
resolution: "graceful-fs@npm:4.2.4"
checksum: 9d58c444eb4f391ce30b451aae8a8af2bd675d9f6f624719e97306f571ab89b2bd2b5f9025199bc63a2edfe2e53e7701554012f32a708148d53aa689163728cc
languageName: node
linkType: hard
"graceful-fs@npm:^4.2.6":
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.10
resolution: "graceful-fs@npm:4.2.10"
checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da
@@ -5056,15 +5017,6 @@ __metadata:
languageName: node
linkType: hard
"is-core-module@npm:^2.1.0":
version: 2.2.0
resolution: "is-core-module@npm:2.2.0"
dependencies:
has: ^1.0.3
checksum: 61e2aff4a7db4f8f7d5a97b484808af17290f4197b34a797cd3d3d27b6b448951064f8d3d6ceae4394fa9b7e6cf08aacd2ba7a17ef6352e922fe803580fbde56
languageName: node
linkType: hard
"is-core-module@npm:^2.8.0":
version: 2.8.1
resolution: "is-core-module@npm:2.8.1"
@@ -6196,7 +6148,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:3.0.4, minimatch@npm:^3.0.4, minimatch@npm:~3.0.2":
"minimatch@npm:3.0.4, minimatch@npm:~3.0.2":
version: 3.0.4
resolution: "minimatch@npm:3.0.4"
dependencies:
@@ -6205,7 +6157,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^3.1.1":
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
dependencies:
@@ -6941,7 +6893,7 @@ __metadata:
languageName: node
linkType: hard
"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7":
"path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a
@@ -7967,7 +7919,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.1.6":
"resolve@npm:^1.1.6, resolve@npm:^1.10.0":
version: 1.21.0
resolution: "resolve@npm:1.21.0"
dependencies:
@@ -7980,17 +7932,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.10.0":
version: 1.19.0
resolution: "resolve@npm:1.19.0"
dependencies:
is-core-module: ^2.1.0
path-parse: ^1.0.6
checksum: a05b356e47b85ad3613d9e2a39a824f3c27f4fcad9c9ff6c7cc71a2e314c5904a90ab37481ad0069d03cab9eaaac6eb68aca1bc3355fdb05f1045cd50e2aacea
languageName: node
linkType: hard
"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>":
"resolve@patch:resolve@^1.1.6#~builtin<compat/resolve>, resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>":
version: 1.21.0
resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin<compat/resolve>::version=1.21.0&hash=07638b"
dependencies:
@@ -8003,16 +7945,6 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@^1.10.0#~builtin<compat/resolve>":
version: 1.19.0
resolution: "resolve@patch:resolve@npm%3A1.19.0#~builtin<compat/resolve>::version=1.19.0&hash=07638b"
dependencies:
is-core-module: ^2.1.0
path-parse: ^1.0.6
checksum: 2443b94d347e6946c87c85faf13071f605e609e0b54784829b0ed2b917d050bfc1cbaf4ecc6453f224cfa7d0c5dcd97cbb273454cd210bee68e4af15c1a5abc9
languageName: node
linkType: hard
"responselike@npm:1.0.2":
version: 1.0.2
resolution: "responselike@npm:1.0.2"
@@ -8313,14 +8245,7 @@ __metadata:
languageName: node
linkType: hard
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2":
version: 3.0.3
resolution: "signal-exit@npm:3.0.3"
checksum: f0169d3f1263d06df32ca072b0bf33b34c6f8f0341a7a1621558a2444dfbe8f5fec76b35537fcc6f0bc4944bdb5336fe0bdcf41a5422c4e45a1dba3f45475e6c
languageName: node
linkType: hard
"signal-exit@npm:^3.0.7":
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7":
version: 3.0.7
resolution: "signal-exit@npm:3.0.7"
checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318
+9 -1
View File
@@ -33,7 +33,8 @@
"build-storybook": "yarn ./storybook run build-storybook",
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"prepare": "husky install"
"prepare": "husky install",
"postinstall": "husky install"
},
"workspaces": {
"packages": [
@@ -70,6 +71,7 @@
"e2e-test": "workspace:*",
"eslint": "^8.6.0",
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-testing-library": "^5.9.1",
"fs-extra": "10.1.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.0",
@@ -92,6 +94,12 @@
],
"*.md": [
"node ./scripts/check-docs-quality"
],
"./yarn.lock": [
"node ./scripts/verify-lockfile-duplicates --fix"
],
"*/yarn.lock": [
"node ./scripts/verify-lockfile-duplicates --fix"
]
},
"packageManager": "yarn@3.2.3"
+4
View File
@@ -25,14 +25,17 @@ describe('App', () => {
it('should display support info when clicking the button', () => {
cy.visit('/');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('support-button').click({ force: true });
cy.contains('#backstage');
});
it('should display error message when triggering it', () => {
cy.visit('/');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('error-button').click({ force: true });
cy.contains('Error: Oh no!');
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('error-button-close').click({ force: true });
});
@@ -55,6 +58,7 @@ describe('App', () => {
cy.contains('Token')
.get('input[name=github-auth-tf]')
.type('password', { force: true });
// eslint-disable-next-line testing-library/await-async-query
cy.findByTestId('github-auth-button').click({ force: true });
cy.contains(`Welcome, ${name}!`);
cy.contains('Logout').click({ force: true });
+1 -1
View File
@@ -80,7 +80,7 @@
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"devDependencies": {
"@backstage/test-utils": "workspace:^",
+3 -3
View File
@@ -80,8 +80,8 @@ import {
TextSize,
} from '@backstage/plugin-techdocs-module-addons-contrib';
import {
SettingsLayout,
UserSettingsPage,
UserSettingsTab,
} from '@backstage/plugin-user-settings';
import { AdvancedSettings } from './components/advancedSettings';
import AlarmIcon from '@material-ui/icons/Alarm';
@@ -267,9 +267,9 @@ const routes = (
element={<CostInsightsLabelDataflowInstructionsPage />}
/>
<Route path="/settings" element={<UserSettingsPage />}>
<UserSettingsTab path="/advanced" title="Advanced">
<SettingsLayout.Route path="/advanced" title="Advanced">
<AdvancedSettings />
</UserSettingsTab>
</SettingsLayout.Route>
</Route>
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
<Route path="/apache-airflow" element={<ApacheAirflowPage />} />
+50 -1
View File
@@ -47,12 +47,21 @@ import {
SidebarSpace,
Link,
useSidebarOpenState,
SidebarSubmenu,
SidebarSubmenuItem,
} from '@backstage/core-components';
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import GroupIcon from '@material-ui/icons/People';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import ApiIcon from '@material-ui/icons/Extension';
import ComponentIcon from '@material-ui/icons/Memory';
import DomainIcon from '@material-ui/icons/Apartment';
import ResourceIcon from '@material-ui/icons/Work';
import SystemIcon from '@material-ui/icons/Category';
import UserIcon from '@material-ui/icons/Person';
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
@@ -93,7 +102,47 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
<SidebarSubmenu title="Catalog">
<SidebarSubmenuItem
title="Domains"
to="catalog?filters[kind]=domain"
icon={DomainIcon}
/>
<SidebarSubmenuItem
title="Systems"
to="catalog?filters[kind]=system"
icon={SystemIcon}
/>
<SidebarSubmenuItem
title="Components"
to="catalog?filters[kind]=component"
icon={ComponentIcon}
/>
<SidebarSubmenuItem
title="APIs"
to="catalog?filters[kind]=api"
icon={ApiIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Resources"
to="catalog?filters[kind]=resource"
icon={ResourceIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Groups"
to="catalog?filters[kind]=group"
icon={GroupIcon}
/>
<SidebarSubmenuItem
title="Users"
to="catalog?filters[kind]=user"
icon={UserIcon}
/>
</SidebarSubmenu>
</SidebarItem>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
+6
View File
@@ -4,6 +4,7 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackendLifecycle } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
@@ -66,6 +67,11 @@ export type HttpRouterFactoryOptions = {
indexPlugin?: string;
};
// @public
export const lifecycleFactory: (
options?: undefined,
) => ServiceFactory<BackendLifecycle>;
// @public (undocumented)
export const loggerFactory: (options?: undefined) => ServiceFactory<Logger>;
@@ -25,4 +25,5 @@ export { schedulerFactory } from './schedulerService';
export { tokenManagerFactory } from './tokenManagerService';
export { urlReaderFactory } from './urlReaderService';
export { httpRouterFactory } from './httpRouterService';
export { lifecycleFactory } from './lifecycleService';
export type { HttpRouterFactoryOptions } from './httpRouterService';
@@ -0,0 +1,47 @@
/*
* Copyright 2022 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 { getVoidLogger } from '@backstage/backend-common';
import { BackendLifecycleImpl } from './lifecycleService';
describe('lifecycleService', () => {
it('should execute registered shutdown hook', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
const hook = jest.fn();
service.addShutdownHook({
pluginId: 'test',
fn: async () => {
hook();
},
});
// should not execute the hook more than once.
await service.shutdown();
await service.shutdown();
await service.shutdown();
expect(hook).toHaveBeenCalledTimes(1);
});
it('should not throw errors', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
service.addShutdownHook({
pluginId: 'test',
fn: async () => {
throw new Error('oh no');
},
});
await expect(service.shutdown()).resolves.toBeUndefined();
});
});
@@ -0,0 +1,96 @@
/*
* Copyright 2022 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 {
BackendLifecycle,
createServiceFactory,
lifecycleServiceRef,
loggerToWinstonLogger,
pluginMetadataServiceRef,
rootLoggerServiceRef,
BackendLifecycleShutdownHook,
} from '@backstage/backend-plugin-api';
import { Logger } from 'winston';
const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit'];
export class BackendLifecycleImpl {
constructor(private readonly logger: Logger) {
CALLBACKS.map(signal => process.on(signal, () => this.shutdown()));
}
#isCalled = false;
#shutdownTasks: Array<BackendLifecycleShutdownHook & { pluginId: string }> =
[];
addShutdownHook(
options: BackendLifecycleShutdownHook & { pluginId: string },
): void {
this.#shutdownTasks.push(options);
}
async shutdown(): Promise<void> {
if (this.#isCalled) {
return;
}
this.#isCalled = true;
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
await Promise.all(
this.#shutdownTasks.map(hook =>
Promise.resolve()
.then(() => hook.fn())
.catch(e => {
this.logger.error(
`Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`,
);
})
.then(() =>
this.logger.info(
`Successfully ran shutdown hook registered by plugin ${hook.pluginId}`,
),
),
),
);
}
}
class PluginScopedLifecycleImpl implements BackendLifecycle {
constructor(
private readonly lifecycle: BackendLifecycleImpl,
private readonly pluginId: string,
) {}
addShutdownHook(options: BackendLifecycleShutdownHook): void {
this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId });
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
* @public */
export const lifecycleFactory = createServiceFactory({
service: lifecycleServiceRef,
deps: {
logger: rootLoggerServiceRef,
plugin: pluginMetadataServiceRef,
},
async factory({ logger }) {
const rootLifecycle = new BackendLifecycleImpl(
loggerToWinstonLogger(logger),
);
return async ({ plugin }) => {
return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId());
};
},
});
@@ -115,7 +115,7 @@ export function parseMysqlConnectionString(
password,
host: hostname,
port: Number(port || 3306),
database: decodeURIComponent(pathname.substr(1)),
database: decodeURIComponent(pathname.substring(1)),
};
const ssl = searchParams.get('ssl');
@@ -31,7 +31,6 @@ import AWSMock from 'aws-sdk-mock';
import aws from 'aws-sdk';
import path from 'path';
import { NotModifiedError } from '@backstage/errors';
import getRawBody from 'raw-body';
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
@@ -247,15 +246,16 @@ describe('AwsS3UrlReader', () => {
});
it('returns contents of an object in a bucket', async () => {
const response = await reader.read(
const { buffer } = await reader.readUrl(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
);
const response = await buffer();
expect(response.toString().trim()).toBe('site_name: Test');
});
it('rejects unknown targets', async () => {
await expect(
reader.read(
reader.readUrl(
'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml',
),
).rejects.toThrow(
@@ -294,21 +294,21 @@ describe('AwsS3UrlReader', () => {
});
it('returns contents of an object in a bucket via buffer', async () => {
const response = await reader.readUrl!(
const { buffer, etag } = await reader.readUrl(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
);
expect(response.etag).toBe('123abc');
const buffer = await response.buffer();
expect(buffer.toString().trim()).toBe('site_name: Test');
expect(etag).toBe('123abc');
const response = await buffer();
expect(response.toString().trim()).toBe('site_name: Test');
});
it('returns contents of an object in a bucket via stream', async () => {
const response = await reader.readUrl!(
const { buffer, etag } = await reader.readUrl(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
);
expect(response.etag).toBe('123abc');
const fromStream = await getRawBody(response.stream!());
expect(fromStream.toString().trim()).toBe('site_name: Test');
expect(etag).toBe('123abc');
const response = await buffer();
expect(response.toString().trim()).toBe('site_name: Test');
});
it('rejects unknown targets', async () => {
@@ -354,12 +354,12 @@ describe('AwsS3UrlReader', () => {
});
it('returns contents of an object in a bucket via buffer', async () => {
const response = await reader.readUrl!(
const { buffer, etag } = await reader.readUrl(
'http://localhost:4566/test-bucket/awsS3-mock-object.yaml',
);
expect(response.etag).toBe('123abc');
const buffer = await response.buffer();
expect(buffer.toString().trim()).toBe('site_name: Test');
expect(etag).toBe('123abc');
const response = await buffer();
expect(response.toString().trim()).toBe('site_name: Test');
});
});
@@ -116,8 +116,9 @@ describe('AzureUrlReader', () => {
treeResponseFactory,
});
const data = await reader.read(url);
const res = await JSON.parse(data.toString('utf-8'));
const { buffer } = await reader.readUrl(url);
const fromStream = await buffer();
const res = await JSON.parse(fromStream.toString());
expect(res).toEqual(response);
});
@@ -145,7 +146,7 @@ describe('AzureUrlReader', () => {
logger,
treeResponseFactory,
});
await reader.read(url);
await reader.readUrl(url);
}).rejects.toThrow(error);
});
});
@@ -161,7 +161,7 @@ describe('BitbucketCloudUrlReader', () => {
describe('read', () => {
it('rejects unknown targets', async () => {
await expect(
reader.read('https://not.bitbucket.com/apa'),
reader.readUrl('https://not.bitbucket.com/apa'),
).rejects.toThrow(
'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket Cloud URL or file path',
);
@@ -171,21 +171,22 @@ describe('FetchUrlReader', () => {
describe('read', () => {
it('should return etag from the response', async () => {
const buffer = await fetchUrlReader.read(
const { buffer } = await fetchUrlReader.readUrl(
'https://backstage.io/some-resource',
);
expect(buffer.toString()).toBe('content foo');
const response = await buffer();
expect(response.toString()).toBe('content foo');
});
it('should throw NotFound if server responds with 404', async () => {
await expect(
fetchUrlReader.read('https://backstage.io/not-exists'),
fetchUrlReader.readUrl('https://backstage.io/not-exists'),
).rejects.toThrow(NotFoundError);
});
it('should throw Error if server responds with 500', async () => {
await expect(
fetchUrlReader.read('https://backstage.io/error'),
fetchUrlReader.readUrl('https://backstage.io/error'),
).rejects.toThrow(Error);
});
});
@@ -92,7 +92,7 @@ describe('GithubUrlReader', () => {
describe('implementation', () => {
it('rejects unknown targets', async () => {
await expect(
githubProcessor.read('https://not.github.com/apa'),
githubProcessor.readUrl('https://not.github.com/apa'),
).rejects.toThrow(
'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path',
);
@@ -135,7 +135,7 @@ describe('GithubUrlReader', () => {
),
);
await gheProcessor.read(
await gheProcessor.readUrl(
'https://github.com/backstage/mock/tree/blob/main',
);
});
@@ -150,8 +150,9 @@ describe('GitlabUrlReader', () => {
treeResponseFactory,
});
const data = await reader.read(url);
const res = await JSON.parse(data.toString('utf-8'));
const { buffer } = await reader.readUrl(url);
const fromStream = await buffer();
const res = await JSON.parse(fromStream.toString());
expect(res).toEqual(response);
});
@@ -169,7 +170,7 @@ describe('GitlabUrlReader', () => {
logger,
treeResponseFactory,
});
await reader.read(url);
await reader.readUrl(url);
}).rejects.toThrow(error);
});
});
@@ -41,8 +41,8 @@ describe('UrlReaderPredicateMux', () => {
reader: barReader,
});
await mux.read('http://foo/1');
expect(fooReader.read).toHaveBeenCalledWith('http://foo/1');
await mux.readUrl('http://foo/1');
expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/1', undefined);
await mux.readUrl('http://foo/2');
expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/2', undefined);
await mux.readTree('http://foo/3');
@@ -50,8 +50,8 @@ describe('UrlReaderPredicateMux', () => {
await mux.search('http://foo/4');
expect(fooReader.search).toHaveBeenCalledWith('http://foo/4', undefined);
await mux.read('http://bar/1');
expect(barReader.read).toHaveBeenCalledWith('http://bar/1');
await mux.readUrl('http://bar/1');
expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/1', undefined);
await mux.readUrl('http://bar/2');
expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/2', undefined);
await mux.readTree('http://bar/3');
@@ -1,172 +0,0 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import { isError } from '@backstage/errors';
import { getVoidLogger } from '../logging';
import { UrlReaders } from './UrlReaders';
const reader = UrlReaders.default({
logger: getVoidLogger(),
config: new ConfigReader({
// The tokens in this config provide read only access to the backstage-verification repos
integrations: {
github: [
{
host: 'github.com',
token:
process.env.INTEGRATION_TEST_GITHUB_TOKEN ||
`${86}af${617}d9c3c8bf958b37a${630691452765}bb0b0a`,
},
],
gitlab: [
{
host: 'gitlab.com',
token:
process.env.INTEGRATION_TEST_GITLAB_TOKEN || 'tveGtSHDBJM9ZRHZNRfm',
},
],
bitbucket: [
{
host: 'bitbucket.org',
username: 'backstage-verification',
appPassword:
process.env.INTEGRATION_TEST_BITBUCKET_TOKEN ||
'H79MAAhtbZwCafkVTrrQ',
},
],
azure: [
{
host: 'dev.azure.com',
// lasts until 2022-01-28
token:
process.env.INTEGRATION_TEST_AZURE_TOKEN ||
`myvyavvfojh6wvw4ose4bfywqttqx${5}z${5}zs${5}bdxauqaek3yinkazq`,
},
],
},
}),
});
function withRetries(count: number, fn: () => Promise<void>) {
return async () => {
let error;
for (let i = 0; i < count; i++) {
try {
await fn();
return;
} catch (err) {
error = err;
}
}
if (
isError(error) &&
!error.message.match(/rate limit|Too Many Requests/)
) {
throw error;
} else {
console.warn('Request was rate limited', error);
}
};
}
/* eslint-disable jest/no-disabled-tests */
describe.skip('UrlReaders', () => {
jest.setTimeout(30_000);
it(
'should read data from azure',
withRetries(3, async () => {
const data = await reader.read(
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml',
);
expect(data.toString()).toContain('test-template-azure');
const res = await reader.readTree(
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2F{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from gitlab',
withRetries(3, async () => {
const data = await reader.read(
'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml',
);
expect(data.toString()).toContain('test-template-gitlab');
const res = await reader.readTree(
'https://gitlab.com/backstage-verification/test-templates/-/tree/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from bitbucket',
withRetries(3, async () => {
const data = await reader.read(
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
);
expect(data.toString()).toContain('test-template-bitbucket');
const res = await reader.readTree(
'https://bitbucket.org/backstage-verification/test-template/src/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
it(
'should read data from github',
withRetries(3, async () => {
const data = await reader.read(
'https://github.com/backstage-verification/test-templates/blob/master/template.yaml',
);
expect(data.toString()).toContain('test-template-github');
const res = await reader.readTree(
'https://github.com/backstage-verification/test-templates/tree/master/{{cookiecutter.name}}',
);
const files = await res.files();
expect(files).toEqual([
{
path: 'catalog-info.yaml',
content: expect.any(Function),
},
]);
}),
);
});
@@ -22,6 +22,7 @@ import {
databaseFactory,
discoveryFactory,
httpRouterFactory,
lifecycleFactory,
loggerFactory,
permissionsFactory,
rootLoggerFactory,
@@ -43,6 +44,7 @@ export const defaultServiceFactories = [
tokenManagerFactory,
urlReaderFactory,
httpRouterFactory,
lifecycleFactory,
];
/**
+13
View File
@@ -24,6 +24,16 @@ export interface BackendFeature {
register(reg: BackendRegistrationPoints): void;
}
// @public (undocumented)
export interface BackendLifecycle {
addShutdownHook(options: BackendLifecycleShutdownHook): void;
}
// @public (undocumented)
export type BackendLifecycleShutdownHook = {
fn: () => void | Promise<void>;
};
// @public (undocumented)
export interface BackendModuleConfig<TOptions> {
// (undocumented)
@@ -158,6 +168,9 @@ export interface HttpRouterService {
// @public (undocumented)
export const httpRouterServiceRef: ServiceRef<HttpRouterService, 'plugin'>;
// @public (undocumented)
export const lifecycleServiceRef: ServiceRef<BackendLifecycle, 'plugin'>;
// @public (undocumented)
export interface Logger {
// (undocumented)
@@ -28,4 +28,9 @@ export { permissionsServiceRef } from './permissionsServiceRef';
export { schedulerServiceRef } from './schedulerServiceRef';
export { rootLoggerServiceRef } from './rootLoggerServiceRef';
export { pluginMetadataServiceRef } from './pluginMetadataServiceRef';
export { lifecycleServiceRef } from './lifecycleServiceRef';
export type {
BackendLifecycle,
BackendLifecycleShutdownHook,
} from './lifecycleServiceRef';
export type { PluginMetadata } from './pluginMetadataServiceRef';
@@ -0,0 +1,42 @@
/*
* Copyright 2022 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 { createServiceRef } from '../system/types';
/**
* @public
**/
export type BackendLifecycleShutdownHook = {
fn: () => void | Promise<void>;
};
/**
* @public
**/
export interface BackendLifecycle {
/**
* Register a function to be called when the backend is shutting down.
*/
addShutdownHook(options: BackendLifecycleShutdownHook): void;
}
/**
* @public
*/
export const lifecycleServiceRef = createServiceRef<BackendLifecycle>({
id: 'core.lifecycle',
scope: 'plugin',
});
+1 -3
View File
@@ -27,14 +27,12 @@ export default async function createPlugin(
subscribers: EventSubscriber[],
): Promise<Router> {
const eventsRouter = Router();
const httpRouter = Router();
eventsRouter.use('/http', httpRouter);
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
logger: env.logger,
router: httpRouter,
});
http.bind(eventsRouter);
await new EventsBackend(env.logger)
.addPublishers(http)
+29 -5
View File
@@ -34,6 +34,10 @@ export interface CatalogApi {
request?: GetEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogRequestOptions,
@@ -91,6 +95,10 @@ export class CatalogClient implements CatalogApi {
request?: GetEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
getEntityAncestors(
request: GetEntityAncestorsRequest,
options?: CatalogRequestOptions,
@@ -145,14 +153,30 @@ export interface CatalogRequestOptions {
export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
'backstage.io/catalog-processing';
// @public
export type EntityFieldsQuery = string[];
// @public
export type EntityFilterQuery =
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>;
// @public
export interface GetEntitiesByRefsRequest {
entityRefs: string[];
fields?: EntityFieldsQuery | undefined;
}
// @public
export interface GetEntitiesByRefsResponse {
items: Array<Entity | undefined>;
}
// @public
export interface GetEntitiesRequest {
after?: string;
fields?: string[] | undefined;
filter?:
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
fields?: EntityFieldsQuery;
filter?: EntityFilterQuery;
limit?: number;
offset?: number;
}
@@ -195,6 +195,35 @@ describe('CatalogClient', () => {
});
});
describe('getEntitiesByRefs', () => {
it('encodes and decodes the query correctly', async () => {
const entity = {
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'Test2',
namespace: 'test1',
},
};
server.use(
rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => {
expect(req.url.searchParams.get('fields')).toBe('a,b');
await expect(req.json()).resolves.toEqual({
entityRefs: ['k:n/a', 'k:n/b'],
});
return res(ctx.json({ items: [entity, null] }));
}),
);
const response = await client.getEntitiesByRefs(
{ entityRefs: ['k:n/a', 'k:n/b'], fields: ['a', 'b'] },
{ token },
);
expect(response).toEqual({ items: [entity, null] });
});
});
describe('getEntityByRef', () => {
const existingEntity: Entity = {
apiVersion: 'v1',
@@ -37,6 +37,8 @@ import {
GetEntityFacetsRequest,
GetEntityFacetsResponse,
ValidateEntityResponse,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { FetchApi } from './types/fetch';
@@ -169,6 +171,40 @@ export class CatalogClient implements CatalogApi {
return { items: entities.sort(refCompare) };
}
/**
* {@inheritdoc CatalogApi.getEntitiesByRefs}
*/
async getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse> {
const params: string[] = [];
if (request.fields?.length) {
params.push(`fields=${request.fields.map(encodeURIComponent).join(',')}`);
}
const baseUrl = await this.discoveryApi.getBaseUrl('catalog');
const query = params.length ? `?${params.join('&')}` : '';
const url = `${baseUrl}/entities/by-refs${query}`;
const response = await this.fetchApi.fetch(url, {
headers: {
'Content-Type': 'application/json',
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
},
method: 'POST',
body: JSON.stringify({ entityRefs: request.entityRefs }),
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const { items } = await response.json();
return { items };
}
/**
* {@inheritdoc CatalogApi.getEntityByRef}
*/
+130 -58
View File
@@ -29,6 +29,75 @@ export const CATALOG_FILTER_EXISTS = Symbol.for(
'CATALOG_FILTER_EXISTS_0e15b590c0b343a2bae3e787e84c2111',
);
/**
* A key-value based filter expression for entities.
*
* @remarks
*
* Each key of a record is a dot-separated path into the entity structure, e.g.
* `metadata.name`.
*
* The values are literal values to match against. As a value you can also pass
* in the symbol `CATALOG_FILTER_EXISTS` (exported from this package), which
* means that you assert on the existence of that key, no matter what its value
* is.
*
* All matching of keys and values is case insensitive.
*
* If multiple filter sets are given as an array, then there is effectively an
* OR between each filter set.
*
* Within one filter set, there is effectively an AND between the various keys.
*
* Within one key, if there are more than one value, then there is effectively
* an OR between them.
*
* Example: For an input of
*
* ```
* [
* { kind: ['API', 'Component'] },
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
* ]
* ```
*
* This effectively means
*
* ```
* (kind = EITHER 'API' OR 'Component')
* OR
* (metadata.name = 'a' AND metadata.namespace = 'b' )
* ```
*
* @public
*/
export type EntityFilterQuery =
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>;
/**
* A set of dot-separated paths into an entity's keys, showing what parts of an
* entity to include in a response, and excluding all others.
*
* @remarks
*
* Example: For an input of `['kind', 'metadata.annotations']`, then response
* objects will be shaped like
*
* ```
* {
* "kind": "Component",
* "metadata": {
* "annotations": {
* "foo": "bar"
* }
* }
* }
* ```
* @public
*/
export type EntityFieldsQuery = string[];
/**
* The request type for {@link CatalogClient.getEntities}.
*
@@ -36,67 +105,14 @@ export const CATALOG_FILTER_EXISTS = Symbol.for(
*/
export interface GetEntitiesRequest {
/**
* If given, return only entities that match the given patterns.
*
* @remarks
*
* If multiple filter sets are given as an array, then there is effectively an
* OR between each filter set.
*
* Within one filter set, there is effectively an AND between the various
* keys.
*
* Within one key, if there are more than one value, then there is effectively
* an OR between them.
*
* Example: For an input of
*
* ```
* [
* { kind: ['API', 'Component'] },
* { 'metadata.name': 'a', 'metadata.namespace': 'b' }
* ]
* ```
*
* This effectively means
*
* ```
* (kind = EITHER 'API' OR 'Component')
* OR
* (metadata.name = 'a' AND metadata.namespace = 'b' )
* ```
*
* Each key is a dot separated path in each object.
*
* As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS`
* (exported from this package), which means that you assert on the existence
* of that key, no matter what its value is.
* If given, return only entities that match the given filter.
*/
filter?:
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>
| undefined;
filter?: EntityFilterQuery;
/**
* If given, return only the parts of each entity that match those dot
* separated paths in each object.
*
* @remarks
*
* Example: For an input of `['kind', 'metadata.annotations']`, then response
* objects will be shaped like
*
* ```
* {
* "kind": "Component",
* "metadata": {
* "annotations": {
* "foo": "bar"
* }
* }
* }
* ```
* If given, return only the parts of each entity that match the field
* declarations.
*/
fields?: string[] | undefined;
fields?: EntityFieldsQuery;
/**
* If given, skips over the first N items in the result set.
*/
@@ -121,6 +137,45 @@ export interface GetEntitiesResponse {
items: Entity[];
}
/**
* The request type for {@link CatalogClient.getEntitiesByRefs}.
*
* @public
*/
export interface GetEntitiesByRefsRequest {
/**
* The list of entity refs to fetch.
*
* @remarks
*
* The returned list of entities will be in the same order as the refs, and
* null will be returned in those positions that were not found.
*/
entityRefs: string[];
/**
* If given, return only the parts of each entity that match the field
* declarations.
*/
fields?: EntityFieldsQuery | undefined;
}
/**
* The response type for {@link CatalogClient.getEntitiesByRefs}.
*
* @public
*/
export interface GetEntitiesByRefsResponse {
/**
* The returned list of entities.
*
* @remarks
*
* The list will be in the same order as the refs given in the request, and
* null will be returned in those positions that were not found.
*/
items: Array<Entity | undefined>;
}
/**
* The request type for {@link CatalogClient.getEntityAncestors}.
*
@@ -296,6 +351,23 @@ export interface CatalogApi {
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse>;
/**
* Gets a batch of entities, by their entity refs.
*
* @remarks
*
* The output list of entities is of the same size and in the same order as
* the requested list of entity refs. Entries that are not found are returned
* as null.
*
* @param request - Request parameters
* @param options - Additional options
*/
getEntitiesByRefs(
request: GetEntitiesByRefsRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesByRefsResponse>;
/**
* Gets entity ancestor information, i.e. the hierarchy of parent entities
* whose processing resulted in a given entity appearing in the catalog.
+5 -1
View File
@@ -20,13 +20,17 @@ export type {
AddLocationResponse,
CatalogApi,
CatalogRequestOptions,
EntityFieldsQuery,
EntityFilterQuery,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
GetEntitiesRequest,
GetEntitiesResponse,
GetEntityAncestorsRequest,
GetEntityAncestorsResponse,
Location,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
Location,
ValidateEntityResponse,
} from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
@@ -224,7 +224,7 @@ describe('CommonValidatorFunctions', () => {
['abc xyz', true],
['abc xyz abc.', true],
])(`isValidString %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidString(value)).toBe(result);
expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result);
});
it.each([
-11
View File
@@ -239,17 +239,6 @@ function createConfigForRole(dir, role, extraConfig = {}) {
name: '@material-ui/icons/', // because this is possible too ._.
message: "Please import '@material-ui/icons/<Icon>' instead.",
},
{
name: '@material-ui/core',
importNames: ['Link'],
message:
'Prefer using `Link` from `@backstage/core-components` rather than material-UI',
},
{
name: '@material-ui/core/Link',
message:
'Prefer using `Link` from `@backstage/core-components` rather than material-UI',
},
...require('module').builtinModules,
...(extraConfig.restrictedImports ?? []),
],
@@ -4,6 +4,7 @@ import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { screen } from '@testing-library/react';
import {
setupRequestMockHandlers,
renderInTestApp,
@@ -22,11 +23,11 @@ describe('ExampleComponent', () => {
});
it('should render', async () => {
const rendered = await renderInTestApp(
await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<ExampleComponent />
</ThemeProvider>,
);
expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
expect(screen.getByText('Welcome to {{ id }}!')).toBeInTheDocument();
});
});
@@ -1,5 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { ExampleFetchComponent } from './ExampleFetchComponent';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -19,7 +19,7 @@ describe('ExampleFetchComponent', () => {
);
});
it('should render', async () => {
const rendered = render(<ExampleFetchComponent />);
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
await render(<ExampleFetchComponent />);
expect(await screen.findByTestId('progress')).toBeInTheDocument();
});
});
+1 -1
View File
@@ -39,7 +39,7 @@
"@types/prop-types": "^15.7.3",
"prop-types": "^15.7.2",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15",
"zen-observable": "^0.9.0",
"zod": "^3.11.6"
},
"peerDependencies": {
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { FeatureFlagged } from './FeatureFlagged';
import { render } from '@testing-library/react';
@@ -44,7 +45,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).toBeInTheDocument();
});
it('should not render contents when the feature flag is disabled', async () => {
jest
@@ -61,7 +62,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).not.toBeInTheDocument();
});
});
describe('without', () => {
@@ -80,7 +81,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).not.toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).not.toBeInTheDocument();
});
it('should render contents when the feature flag is disabled', async () => {
jest
@@ -97,7 +98,7 @@ describe('FeatureFlagged', () => {
</Wrapper>,
);
expect(await queryByText('BACKSTAGE!')).toBeInTheDocument();
expect(queryByText('BACKSTAGE!')).toBeInTheDocument();
});
});
});
+1
View File
@@ -250,6 +250,7 @@ export interface DependencyGraphProps<NodeData, EdgeData>
edgeRanks?: number;
edges: DependencyEdge<EdgeData>[];
edgeWeight?: number;
fit?: 'grow' | 'contain';
labelOffset?: number;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
labelPosition?: LabelPosition;
+2 -2
View File
@@ -56,7 +56,7 @@
"pluralize": "^8.0.0",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
"rc-progress": "3.4.0",
"rc-progress": "3.4.1",
"react-helmet": "6.1.0",
"react-hook-form": "^7.12.2",
"react-markdown": "^8.0.0",
@@ -67,7 +67,7 @@
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6",
"remark-gfm": "^3.0.1",
"zen-observable": "^0.8.15",
"zen-observable": "^0.9.0",
"zod": "^3.11.6"
},
"peerDependencies": {
@@ -170,6 +170,14 @@ export interface DependencyGraphProps<NodeData, EdgeData>
* Default: 'curveMonotoneX'
*/
curve?: 'curveStepBefore' | 'curveMonotoneX';
/**
* Controls if the graph should be contained or grow
*
* @remarks
*
* Default: 'grow'
*/
fit?: 'grow' | 'contain';
}
const WORKSPACE_ID = 'workspace';
@@ -203,6 +211,7 @@ export function DependencyGraph<NodeData, EdgeData>(
defs,
zoom = 'enabled',
curve = 'curveMonotoneX',
fit = 'grow',
...svgProps
} = props;
const theme: BackstageTheme = useTheme();
@@ -223,6 +232,9 @@ export function DependencyGraph<NodeData, EdgeData>(
const maxWidth = Math.max(graphWidth, containerWidth);
const maxHeight = Math.max(graphHeight, containerHeight);
const minHeight = Math.min(graphHeight, containerHeight);
const scalableHeight = fit === 'grow' ? maxHeight : minHeight;
const containerRef = React.useMemo(
() =>
@@ -394,7 +406,7 @@ export function DependencyGraph<NodeData, EdgeData>(
ref={containerRef}
{...svgProps}
width="100%"
height={maxHeight}
height={scalableHeight}
viewBox={`0 0 ${maxWidth} ${maxHeight}`}
>
<defs>
@@ -70,8 +70,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
expect(dot).toBeInTheDocument();
expect(text).toBeInTheDocument();
@@ -96,8 +96,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
act(() => {
Element.prototype.getBoundingClientRect = jest.fn(
@@ -128,8 +128,8 @@ describe('<FeatureCalloutCircular />', () => {
description="description"
/>,
);
const dot = await getByTestId('dot');
const text = await getByTestId('text');
const dot = getByTestId('dot');
const text = getByTestId('text');
act(() => {
Element.prototype.getBoundingClientRect = jest.fn(
@@ -41,7 +41,7 @@ describe('<Select />', () => {
const { getByText, getByTestId } = render(<Select {...minProps} />);
expect(getByText('Default')).toBeInTheDocument();
const input = await getByTestId('select');
const input = getByTestId('select');
expect(input.textContent).toBe('All results');
// Simulate click on input
@@ -68,7 +68,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => {
</ListItemIcon>
<ListItemText
primary={item.title}
secondary={item.links?.reduce<React.ReactNodeArray>(
secondary={item.links?.reduce<React.ReactNode[]>(
(prev, link, idx) => [
...prev,
idx > 0 && <br key={idx} />,
@@ -18,7 +18,6 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import Typography from '@material-ui/core/Typography';
import { WarningPanel, WarningProps } from './WarningPanel';
const propsTitle: WarningProps = { title: 'Mock title' };
@@ -41,7 +40,7 @@ describe('<WarningPanel />', () => {
it('renders title', async () => {
await renderInTestApp(<WarningPanel {...propsTitleMessage} />);
const expandIcon = await screen.getByText('Warning: Mock title');
const expandIcon = screen.getByText('Warning: Mock title');
fireEvent.click(expandIcon);
expect(screen.getByText('Warning: Mock title')).toBeInTheDocument();
expect(screen.getByText('Some more info')).toBeInTheDocument();
+1 -1
View File
@@ -38,7 +38,7 @@
"@backstage/version-bridge": "workspace:^",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -47,7 +47,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"react-use": "^17.2.4",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+8
View File
@@ -189,6 +189,14 @@ export interface Config {
* @visibility secret
*/
clientSecret: string;
/**
* List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations.
* However you can list the installations with the GitHub API. You can find the list of installations here:
* https://api.github.com/app/installations
* The relevant documentation for this is here.
* https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples
*/
allowedInstallationOwners?: string[];
}>;
}>;
+1 -1
View File
@@ -45,7 +45,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"cross-fetch": "^3.1.5",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
+1 -1
View File
@@ -35,7 +35,7 @@
"@backstage/cli": "workspace:^",
"@types/zen-observable": "^0.8.0",
"luxon": "^3.0.0",
"zen-observable": "^0.8.15"
"zen-observable": "^0.9.0"
},
"files": [
"dist"
@@ -46,7 +46,7 @@ describe('BitriseArtifactsComponent', () => {
const rendered = renderComponent();
const btn = await rendered.findByTestId('btn');
expect(await rendered.queryByText('VISIBLE')).not.toBeInTheDocument();
expect(rendered.queryByText('VISIBLE')).not.toBeInTheDocument();
btn.click();
@@ -26,10 +26,8 @@ export { GithubDiscoveryProcessor } from './processors/GithubDiscoveryProcessor'
export { GithubMultiOrgReaderProcessor } from './processors/GithubMultiOrgReaderProcessor';
export { GithubOrgReaderProcessor } from './processors/GithubOrgReaderProcessor';
export { GithubEntityProvider } from './providers/GithubEntityProvider';
export type {
GithubOrgEntityProvider,
GithubOrgEntityProviderOptions,
} from './providers/GithubOrgEntityProvider';
export { GithubOrgEntityProvider } from './providers/GithubOrgEntityProvider';
export type { GithubOrgEntityProviderOptions } from './providers/GithubOrgEntityProvider';
export { githubEntityProviderCatalogModule } from './service/GithubEntityProviderCatalogModule';
export {
type GithubMultiOrgConfig,
+14 -13
View File
@@ -37,7 +37,7 @@ import { EntityRelationSpec } from '@backstage/plugin-catalog-node';
import { GetEntitiesRequest } from '@backstage/catalog-client';
import { JsonValue } from '@backstage/types';
import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import { Permission } from '@backstage/plugin-permission-common';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
@@ -86,9 +86,9 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor {
// (undocumented)
preProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
_: CatalogProcessorEmit,
originLocation: LocationSpec,
originLocation: LocationSpec_2,
): Promise<Entity>;
}
@@ -100,7 +100,7 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor {
// (undocumented)
getProcessorName(): string;
// (undocumented)
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
}
// @public (undocumented)
@@ -110,7 +110,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
// (undocumented)
postProcessEntity(
entity: Entity,
_location: LocationSpec,
_location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
// (undocumented)
@@ -284,7 +284,7 @@ export class CodeOwnersProcessor implements CatalogProcessor {
// (undocumented)
getProcessorName(): string;
// (undocumented)
preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
}
// @alpha
@@ -409,7 +409,7 @@ export class FileReaderProcessor implements CatalogProcessor {
getProcessorName(): string;
// (undocumented)
readLocation(
location: LocationSpec,
location: LocationSpec_2,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
@@ -431,7 +431,7 @@ export class LocationEntityProcessor implements CatalogProcessor {
// (undocumented)
postProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
}
@@ -441,18 +441,19 @@ export type LocationEntityProcessorOptions = {
integrations: ScmIntegrationRegistry;
};
export { LocationSpec };
// @public @deprecated
export type LocationSpec = LocationSpec_2;
// @public (undocumented)
export function locationSpecToLocationEntity(opts: {
location: LocationSpec;
location: LocationSpec_2;
parentEntity?: Entity;
}): LocationEntityV1alpha1;
// @public (undocumented)
export function parseEntityYaml(
data: Buffer,
location: LocationSpec,
location: LocationSpec_2,
): Iterable<CatalogProcessorResult>;
// @alpha
@@ -518,7 +519,7 @@ export class PlaceholderProcessor implements CatalogProcessor {
// (undocumented)
preProcessEntity(
entity: Entity,
location: LocationSpec,
location: LocationSpec_2,
emit: CatalogProcessorEmit,
): Promise<Entity>;
}
@@ -574,7 +575,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
getProcessorName(): string;
// (undocumented)
readLocation(
location: LocationSpec,
location: LocationSpec_2,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
@@ -80,6 +80,36 @@ export type EntitiesResponse = {
pageInfo: PageInfo;
};
/**
* A request for a batch of entities.
*/
export interface EntitiesBatchRequest {
/**
* The refs for which to fetch entities.
*/
entityRefs: string[];
/**
* Any additional filters to apply in the selection of the entities.
*/
filter?: EntityFilter;
/**
* Strips out only the parts of the entity bodies to include in the response.
*/
fields?: (entity: Entity) => Entity;
/**
* The optional token that authorizes the action.
*/
authorizationToken?: string;
}
export interface EntitiesBatchResponse {
/**
* The list of entities, in the same order as the refs in the request. Entries
* that are null signify that no entity existed with that ref.
*/
items: Array<Entity | null>;
}
export type EntityAncestryResponse = {
rootEntityRef: string;
items: Array<{
@@ -130,6 +160,11 @@ export interface EntitiesCatalog {
*/
entities(request?: EntitiesRequest): Promise<EntitiesResponse>;
/**
* Fetches a batch of entities.
*/
entitiesBatch(request: EntitiesBatchRequest): Promise<EntitiesBatchResponse>;
/**
* Removes a single entity.
*
+16 -1
View File
@@ -22,7 +22,6 @@
export type {
DeferredEntity,
LocationSpec,
EntityRelationSpec,
CatalogProcessor,
CatalogProcessorParser,
@@ -48,3 +47,19 @@ export * from './processing';
export * from './search';
export * from './service';
export * from './util';
import { LocationSpec as NonDeprecatedLocationSpec } from '@backstage/plugin-catalog-common';
/**
* Holds the entity location information.
*
* @remarks
*
* `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
* This flag is then set to indicate that the file can be not present.
* default value: 'required'.
*
* @public
* @deprecated use the same type from `@backstage/plugin-catalog-common` instead
*/
export type LocationSpec = NonDeprecatedLocationSpec;
@@ -17,7 +17,7 @@
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import path from 'path';
import { LocationSpec } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
/**
* Rules to apply to catalog entities.

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