Merge remote-tracking branch 'origin/master' into feat/resolve-refs-in-yaml
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Use Json types from @backstage/types
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Wait for indexer initialization before finalizing indexing.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings': minor
|
||||
---
|
||||
|
||||
Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-explore': patch
|
||||
---
|
||||
|
||||
Adds styling to graph forcing it to always fill out the available space.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Added `lifecycleFactory` implementation.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-codescene': patch
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Updated dependency `rc-progress` to `3.4.1`.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Internal refactor to avoid usage of deprecated symbols.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Registered shutdown hook in experimental catalog plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
---
|
||||
|
||||
Fix incorrectly exported GithubOrgEntityProvider as a type
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Implement Custom Field Explorer to view and play around with available installed custom field extensions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
fixed `headerOptions` not passed to `TemplatePage` component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fixed Entity kind pluralisation in the `CatalogKindHeader` component.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Added `lifecycleFactory` to default service factories.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added initial support for registering shutdown hooks via `lifecycleServiceRef`.
|
||||
+6
-1
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
},
|
||||
```
|
||||
|
||||
@@ -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):
|
||||
|
||||

|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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). He’s been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 ?? []),
|
||||
],
|
||||
|
||||
+3
-2
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+6
-6
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+8
@@ -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[];
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user