This commit is contained in:
Tim Hansen
2020-09-18 10:02:11 -06:00
113 changed files with 2398 additions and 1031 deletions
+2
View File
@@ -3,6 +3,8 @@ name: E2E Test Linux
on:
pull_request:
paths-ignore:
- 'contrib/**'
- 'docs/**'
- 'microsite/**'
jobs:
+8 -2
View File
@@ -6,13 +6,15 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
> Collect changes for the next release below
## v0.1.1-alpha.22
### @backstage/core
- Introduced initial version of an inverted app/plugin relationship, where plugins export components for apps to use, instead registering themselves directly into the app. This enables more fine-grained control of plugin features, and also composition of plugins such as catalog pages with additional cards and tabs. This breaks the use of `RouteRef`s, and there will be more changes related to this in the future, but this change lays the initial foundation. See `packages/app` and followup PRs for how to update plugins for this change. [#2076](https://github.com/spotify/backstage/pull/2076)
- Switch to an automatic dependency injection mechanism for all Utility APIs, allowing plugins to ship default implementations of their APIs. See [https://backstage.io/docs/api/utility-apis](https://backstage.io/docs/api/utility-apis). [#2285](https://github.com/spotify/backstage/pull/2285)
> Collect changes for the next release below
### @backstage/cli
- Change `backstage-cli backend:build-image` to forward all args to `docker image build`, instead of just tag. Also add `--build` flag for building all dependent packages before packaging the workspace for the docker build. [#2299](https://github.com/spotify/backstage/pull/2299)
@@ -21,6 +23,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
- Change root `tsc` output dir to `dist-types`, in order to allow for standalone plugin repos. [#2278](https://github.com/spotify/backstage/pull/2278)
### @backstage/catalog-backend
- We have simplified the way that GitHub ingestion works. The `catalog.processors.githubApi` key is deprecated, in favor of `catalog.processors.github`. At the same time, the location type `github/api` is likewise deprecated, in favor of `github`. This location type now serves both raw HTTP reads and APIv3 reads, depending on how you configure it. It also supports having several providers at once - for example, both public GitHub and an internal GitHub Enterprise, with different keys. If you still use the `catalog.processors.githubApi` config key, things will work but you will get a deprecation warning at startup. In a later release, support for the old key will go away entirely. See the [configuration section in the docs](https://backstage.io/docs/features/software-catalog/configuration) for more details.
## v0.1.1-alpha.21
- Added many more frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084)
+9 -5
View File
@@ -35,6 +35,8 @@ organization:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
generators:
techdocs: 'docker'
sentry:
organization: spotify
@@ -58,21 +60,23 @@ catalog:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
providers:
- target: https://github.com
token:
$secret:
env: GITHUB_PRIVATE_TOKEN
# Example for how to add your GitHub Enterprise instance:
#### Example for how to add your GitHub Enterprise instance using the API:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $secret:
# env: GHE_PRIVATE_TOKEN
#### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
# - target: https://ghe.example.net
# rawBaseUrl: https://ghe.example.net/raw
# token:
# $secret:
# env: GHE_PRIVATE_TOKEN
bitbucketApi:
username:
$secret:
+9
View File
@@ -0,0 +1,9 @@
# Backstage Contrib
This directory contains various community contributions related to Backstage.
Unless otherwise specified, all content in this hierarchy fall under the same
[licensing terms](../LICENSE) as in the rest of the repository, and come with
no guarantees of functionality or fitness of purpose. That being said, we
really appreciate contributions in here and encourage them being kept up to
date.
@@ -0,0 +1 @@
# Basic Kubernetes example with Helm
@@ -4,6 +4,67 @@ title: Catalog Configuration
description: Documentation on Software Catalog Configuration
---
## Processors
The catalog makes use of so called processors to perform all kinds of ingestion
tasks, such as reading raw entity data from a remote source, parsing it,
transforming it, and validating it. These processors are configured under the
`catalog.processors` key.
### Processor: github
The `github` processor is responsible for fetching entity data from files on
GitHub or GitHub Enterprise. The configuration for this processor lives under
`catalog.processors.github`. Example:
```yaml
catalog:
processors:
github:
providers:
- target: https://github.com
token:
$secret:
env: GITHUB_PRIVATE_TOKEN
- target: https://ghe.example.net
apiBaseUrl: https://ghe.example.net/api/v3
rawBaseUrl: https://ghe.example.net/raw
token:
$secret:
env: GHE_PRIVATE_TOKEN
```
The main subkey is `providers`, where you can list the various GitHub compatible
providers you want to be able to fetch data from. Each entry is a structure with
up to four elements:
- `target` (required): The string prefix of the location target that you want to
match on, with no trailing slash. For GitHub, it should be exactly
`https://github.com`.
- `token` (optional): An authentication token as expected by GitHub. If
supplied, it will be passed along with all calls to this provider, both API
and raw. If it is not supplied, anonymous access will be used.
- `apiBaseUrl` (optional): If you want to communicate using the APIv3 method
with this provider, specify the base URL for its endpoint here, with no
trailing slash. Specifically when the target is github, you can leave it out
to be inferred automatically. For a GitHub Enterprise installation, it is
commonly at `https://api.<host>` or `https://<host>/api/v3`.
- `rawBaseUrl` (optional): If you want to communicate using the raw HTTP method
with this provider, specify the base URL for its endpoint here, with no
trailing slash. Specifically when the target is public GitHub, you can leave
it out to be inferred automatically. For a GitHub Enterprise installation, it
is commonly at `https://api.<host>` or `https://<host>/api/v3`.
You need to supply either `apiBaseUrl` or `rawBaseUrl` or both (except for
public GitHub, for which we can infer them). The `apiBaseUrl` will always be
preferred over the other if a `token` is given, otherwise `rawBaseUrl` will be
preferred.
If you do not supply a public GitHub provider, one will be added automatically,
silently at startup for convenience. So you only have to list it if you want to
supply a token for it - and if you do, you can also leave out the `apiBaseUrl`
and `rawBaseUrl` fields.
## Static Location Configuration
To enable declarative catalog setups, it is possible to add locations to the
+24
View File
@@ -71,6 +71,30 @@ building and publishing of your documentation, you want to change the
`requestUrl` to point to your storage. In this case `storageUrl` is not
required.
### Disable Docker in Docker situation (Optional)
The TechDocs backend plugin runs a docker container with mkdocs to generate the
frontend of the docs from source files (Markdown). If you are deploying
Backstage using Docker, this will mean that your Backstage Docker container will
try to run another Docker container for TechDocs backend.
To avoid this problem, we have a configuration available. You can set a value in
your `app-config.yaml` that tells the techdocs generator if it should run the
`local` mkdocs or run it from `docker`. This defaults to running as `docker` if
no config is provided.
```yaml
techdocs:
generators:
techdocs: local
```
Setting `generators.techdocs` to `local` means you will have to make sure your
environment is compatible with techdocs. You will have to install the
`mkdocs-techdocs-container` and 'mkdocs' package from pip, as well as graphviz
and plantuml from your package manager. This has only been tested with python
3.7 and python 3.8.
## Run Backstage locally
Change folder to `<backstage-project-root>/packages/backend` and run the
+199
View File
@@ -0,0 +1,199 @@
---
id: quickstart-app-auth
title: Monorepo App Setup With Authentication
---
###### September 15th 2020 - @backstage/create-app - v0.1.1-alpha.21
<br />
> This document takes you through setting up a backstage app that runs in your
> own environment. It starts with a skeleton install and verifying of the
> monorepo's functionality. Next, GitHub authentication is added and tested.
>
> This document assumes you have NodeJS 12 active along with Yarn. Please note,
> that at the time of this writing, the current version is 0.1.1-alpha.21. This
> guide can still be used with future versions, just, verify as you go. If you
> run into issues, you can compare your setup with mine here >
> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app).
# The Skeleton Application
From the terminal:
1. Create a (monorepo) application: `npx @backstage/create-app`
1. Enter an `id` for your new app like `mybiz-backstage` I went with
`simple-backstage-app`
1. Choose `SQLite` as your database. This is the quickest way to get started as
PostgreSQL requires additional setup not covered here.
1. Start your backend: `yarn --cwd packages/backend start`
```zsh
# You should see positive verbiage in your terminal output
2020-09-11T22:20:26.712Z backstage info Listening on :7000
```
5. Finally, start the frontend. Open a new terminal window and from the root of
your project, run: `yarn start`
```zsh
# You should see positive verbiage in your terminal output
「wds」: Project is running at http://localhost:3000/
```
Once the app compiles, a browser window should have popped with your stand alone
application loaded at `localhost:3000`. This could take a couple minutes.
```zsh
# You should see positive verbiage in your terminal output
ℹℹ 「wdm」: Compiled successfully.
```
Since there is no auth currently configured, you are automatically entered as a
guest. Let's fix that now and add auth.
# The Auth Configuration
1. Open `app-config.yaml` and change it as follows
_from:_
```yaml
auth:
providers: {}
```
_to:_
```yaml
auth:
providers:
github:
development:
clientId:
$secret:
env: AUTH_GITHUB_CLIENT_ID
clientSecret:
$secret:
env: AUTH_GITHUB_CLIENT_SECRET
## uncomment the following three lines if using enterprise
# enterpriseInstanceUrl:
# $secret:
# env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
```
2. Set environment variables in whatever fashion is easiest for you. I chose to
add mine to my `.zshrc` profile.
```zsh
# For macOS Catalina & Z Shell
# ------ simple-backstage-app GitHub
export AUTH_GITHUB_CLIENT_ID=xxx
export AUTH_GITHUB_CLIENT_SECRET=xxx
# export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://github.{MY_BIZ}.com
```
3. And of course I need to source that file.
```zsh
# Loading the new variables
% source ~/.zshrc
# Any other currently opened terminals need to be restarted to pick up the new values
# verify your setup by running env
% env
# should output something like
> ...
> AUTH_GITHUB_CLIENT_ID=xxx
> AUTH_GITHUB_CLIENT_SECRET=xxx
> ...
```
4. The values to replace `xxx` above come from your oauth app setup.
```
> Log into http://github.com
> Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new]
> Set Homepage URL = http://localhost:3000
> Set Callback URL = http://localhost:7000/auth/github
> Click [Register application]
> On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET`
> Don't forget to `source` that profile file again if necessary.
```
5. Open and change _root > packages > app > src >_`App.tsx` as follows
```tsx
// Add the following imports to the existing list from core
import { githubAuthApiRef, SignInPage } from '@backstage/core';
```
6. In the same file, change the createApp function as follows
```tsx
const app = createApp({
apis,
plugins: Object.values(plugins),
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={[
{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Simple Backstage Application Login',
apiRef: githubAuthApiRef,
},
]}
align="center"
/>
);
},
},
});
```
6. Open and change _root > packages > app > src >_ `apis.ts` as follows
```ts
// Add the following imports to the existing list from core
import { githubAuthApiRef, GithubAuth } from '@backstage/core';
```
7. In the same file, change the builder block for oauthRequestApiRef as follows
_from:_
```ts
builder.add(oauthRequestApiRef, new OAuthRequestManager());
```
_to:_
```ts
const oauthRequestApi = builder.add(
oauthRequestApiRef,
new OAuthRequestManager(),
);
builder.add(
githubAuthApiRef,
GithubAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
```
> Start the backend and frontend as before. When the browser loads, you should
> be presented with a login page for GitHub. Login as usual with your GitHub
> account. If this is your first time, you will be asked to authorize and then
> are redirected to the catalog page if all is well.
# Where to go from here
> You're probably eager to write your first custom plugin. Follow this next
> tutorial for an in-depth look at a custom GitHub repository browser plugin.
> [Adding Custom Plugin to Existing Monorepo App](quickstart-app-plugin.md).
+487
View File
@@ -0,0 +1,487 @@
---
id: quickstart-app-plugin
title: Adding Custom Plugin to Existing Monorepo App
---
###### September 15th 2020 - v0.1.1-alpha.21
<br />
> This document takes you through setting up a new plugin for your existing
> monorepo with a _GitHub provider already setup_. If you don't have either of
> those, you can clone
> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app)
> which this document builds on.
>
> This document does not cover authoring a plugin for sharing with the Backstage
> community. That will have to be a later discussion.
>
> We start with a skeleton plugin install. And after verifying its
> functionality, extend the Sidebar to make our life easy. Finally, we add
> custom code to display GitHub repository information.
>
> This document assumes you have NodeJS 12 active along with Yarn. Please note,
> that at the time of this writing, the current version is 0.1.1-alpha.21. This
> guide can still be used with future versions, just, verify as you go. If you
> run into issues, you can compare your setup with mine here >
> [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin).
# The Skeleton Plugin
1. Start by using the built in creator. From the terminal and root of your
project run: `yarn create-plugin`
1. Enter a plugin ID. I used `github-playground`
1. When the process finishes, let's start the backend:
`yarn --cwd packages/backend start`
1. If you see errors starting, refer to
[Auth Configuration](https://github.com/johnson-jesse/simple-backstage-app/blob/master/README.md#the-auth-configuration)
for more information on environment variables.
1. And now the frontend, from a new terminal window and the root of your
project: `yarn start`
1. As usual, a browser window should popup loading the App.
1. Now manually navigate to our plugin page from your browser:
`http://localhost:3000/github-playground`
1. You should see successful verbiage for this endpoint,
`Welcome to github-playground!`
# The Shortcut
Let's add a shortcut.
1. Open and modify `root: packages > app > src > sidebar.tsx` with the
following:
```tsx
import GitHubIcon from '@material-ui/icons/GitHub';
...
<SidebarItem icon={GitHubIcon} to="github-playground" text="GitHub Repository" />
```
Simple! The App will reload with your changes automatically. You should now see
a github icon displayed in the sidebar. Clicking that will link to our new
plugin. And now, the API fun begins.
# The Identity
Our first modification will be to extract information from the Identity API.
1. Start by opening
`root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx`
1. Add two new imports
```tsx
// Add identityApiRef to the list of imported from core
import { identityApiRef } from '@backstage/core';
import { useApi } from '@backstage/core-api';
```
3. Adjust the ExampleComponent from inline to block
_from inline:_
```tsx
const ExampleComponent: FC<{}> = () => ( ... )
```
_to block:_
```tsx
const ExampleComponent: FC<{}> = () => {
return (
...
)
}
```
4. Now add our hook and const data before the return statement
```tsx
// our API hook
const identityApi = useApi(identityApiRef);
// data to use
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
```
5. Finally, update the InfoCard's jsx to use our new data
```tsx
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>
```
If everything is saved, you should see your name, id, and email on the
github-playground page. Our data accessed is synchronous. So we just grab and
go.
6. Here is the entire file for reference
<details><summary>Complete ExampleComponent.tsx</summary>
<p>
```tsx
import React, { FC } from 'react';
import { Typography, Grid } from '@material-ui/core';
import {
InfoCard,
Header,
Page,
pageTheme,
Content,
ContentHeader,
HeaderLabel,
SupportButton,
identityApiRef,
} from '@backstage/core';
import { useApi } from '@backstage/core-api';
import ExampleFetchComponent from '../ExampleFetchComponent';
const ExampleComponent: FC<{}> = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
return (
<Page theme={pageTheme.tool}>
<Header
title="Welcome to github-playground!"
subtitle="Optional subtitle"
>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Plugin title">
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title={userId}>
<Typography variant="body1">
{`${profile.displayName} | ${profile.email}`}
</Typography>
</InfoCard>
</Grid>
<Grid item>
<ExampleFetchComponent />
</Grid>
</Grid>
</Content>
</Page>
);
};
export default ExampleComponent;
```
</p>
</details>
# The Wipe
The last file we will touch is ExampleFetchComponent. Because of the number of
changes, let's start by wiping this component clean.
1. Start by opening
`root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx`
1. Replace everyting in the file with the following:
```tsx
import React, { FC } from 'react';
import { useAsync } from 'react-use';
import Alert from '@material-ui/lab/Alert';
import {
Table,
TableColumn,
Progress,
githubAuthApiRef,
} from '@backstage/core';
import { useApi } from '@backstage/core-api';
import { graphql } from '@octokit/graphql';
const ExampleFetchComponent: FC<{}> = () => {
return <div>Nothing to see yet</div>;
};
export default ExampleFetchComponent;
```
3. Save that and ensure you see no errors. Comment out the unused imports if
your linter gets in the way.
###### We will add a lot to this file for the sake of ease. Please don't do this in productional code!
# The Graph Model
GitHub has a graphql API available for interacting. Let's start by adding our
basic repository query
1. Add the query const statement outside ExampleFetchComponent
```tsx
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
```
2. Using this structure as a guide, we will break our query into type parts
3. Add the following outside of ExampleFetchComponent
```tsx
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};
type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};
```
# The Tabel Model
Using Backstage's own component library, let's define a custom table. This
component will get used if we have data to display.
1. Add the following outside of ExampleFetchComponent
```tsx
type DenseTableProps = {
viewer: Viewer;
};
export const DenseTable: FC<DenseTableProps> = ({ viewer }) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Created', field: 'createdAt' },
{ title: 'Description', field: 'description' },
{ title: 'Disk Usage', field: 'diskUsage' },
{ title: 'Fork', field: 'isFork' },
];
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={columns}
data={viewer.repositories.nodes}
/>
);
};
```
# The Fetch
We're ready to flush out our fetch component
1. Add our api hook inside ExampleFetchComponent
```tsx
const auth = useApi(githubAuthApiRef);
```
2. The access token we need to make our GitHub request and the request itself is
obtained in an asynchronous manner.
3. Add the useAsync block inside the ExampleFetchComponent
```tsx
const { value, loading, error } = useAsync(async (): Promise<any> => {
const token = await auth.getAccessToken();
const gqlEndpoint = graphql.defaults({
// Uncomment baseUrl if using enterprise
// baseUrl: 'https://github.MY-BIZ.com/api',
headers: {
authorization: `token ${token}`,
},
});
const { viewer } = await gqlEndpoint(query);
return viewer;
}, []);
```
4. The resolved data is conventiently destructured with value containing our
Viewer type. loading as a boolean, self explainatory. And error which is
present only if necessary. So let's use those as the first 3 of 4 multi
return statements.
5. Add the _if return_ blocks below our async block
```tsx
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
```
6. The third line here utilizes our custom table accepting our Viewer type.
7. Finally, we add our _else return_ block to catch any other scenarios.
```tsx
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
```
8. After saving that, and given we don't have any errors, you should see a table
with basic information on your repositories.
9. Here is the entire file for reference
<details><summary>Complete ExampleFetchComponent.tsx</summary>
<p>
```tsx
import React, { FC } from 'react';
import { useAsync } from 'react-use';
import Alert from '@material-ui/lab/Alert';
import {
Table,
TableColumn,
Progress,
githubAuthApiRef,
} from '@backstage/core';
import { useApi } from '@backstage/core-api';
import { graphql } from '@octokit/graphql';
const query = `{
viewer {
repositories(first: 100) {
totalCount
nodes {
name
createdAt
description
diskUsage
isFork
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`;
type Node = {
name: string;
createdAt: string;
description: string;
diskUsage: number;
isFork: boolean;
};
type Viewer = {
repositories: {
totalCount: number;
nodes: Node[];
pageInfo: {
endCursor: string;
hasNextPage: boolean;
};
};
};
type DenseTableProps = {
viewer: Viewer;
};
export const DenseTable: FC<DenseTableProps> = ({ viewer }) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Created', field: 'createdAt' },
{ title: 'Description', field: 'description' },
{ title: 'Disk Usage', field: 'diskUsage' },
{ title: 'Fork', field: 'isFork' },
];
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={columns}
data={viewer.repositories.nodes}
/>
);
};
const ExampleFetchComponent: FC<{}> = () => {
const auth = useApi(githubAuthApiRef);
const { value, loading, error } = useAsync(async (): Promise<any> => {
const token = await auth.getAccessToken();
const gqlEndpoint = graphql.defaults({
// Uncomment baseUrl if using enterprise
// baseUrl: 'https://github.MY-BIZ.com/api',
headers: {
authorization: `token ${token}`,
},
});
const { viewer } = await gqlEndpoint(query);
return viewer;
}, []);
if (loading) return <Progress />;
if (error) return <Alert severity="error">{error.message}</Alert>;
if (value && value.repositories) return <DenseTable viewer={value} />;
return (
<Table
title="List Of User's Repositories"
options={{ search: false, paging: false }}
columns={[]}
data={[]}
/>
);
};
export default ExampleFetchComponent;
```
</p>
</details>
10. We finished! If there are no errors, you should see your own GitHub
repoistory information displayed in a basic table. If you run into issues,
you can compare the repo that backs this documdnt,
[simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin)
# Where to go from here
> Break apart ExampleFetchComponent into smaller logical parts contained in
> their own files. Rename your components to something other than ExampleXxx.
>
> You might be real proud of a plugin you develop. Follow this next tutorial for
> an in-depth look at publishing and including that for the entire Backstage
> community. [TODO](#).
+1 -1
View File
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.1.1-alpha.20"
"version": "0.1.1-alpha.22"
}
+13
View File
@@ -462,6 +462,19 @@ class Index extends React.Component {
Contribute
</ActionBlock.Link>
</ActionBlock>
<Block small className="bg-black-grey cncf-block">
<Block.Container center>
<Block.SmallTitle small>
Backstage is a{' '}
<a href="https://www.cncf.io">
Cloud Native Computing Foundation
</a>{' '}
sandbox project
</Block.SmallTitle>
<div className="cncf-logo" />
</Block.Container>
</Block>
</main>
);
}
+6 -1
View File
@@ -40,6 +40,7 @@
"ids": [
"features/software-catalog/software-catalog-overview",
"features/software-catalog/installation",
"features/software-catalog/configuration",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/well-known-annotations",
@@ -143,7 +144,11 @@
"ids": ["api/backend"]
}
],
"Tutorials": ["tutorials/journey"],
"Tutorials": [
"tutorials/journey",
"tutorials/quickstart-app-auth",
"tutorials/quickstart-app-plugin"
],
"Architecture Decision Records (ADRs)": [
"architecture-decisions/adrs-overview",
"architecture-decisions/adrs-adr001",
+11
View File
@@ -1070,3 +1070,14 @@ code {
margin: auto;
}
}
.cncf-block {
text-align: center;
}
.cncf-logo {
background: center no-repeat url(../img/cncf-white.svg);
width: 100%;
height: 100px;
margin-bottom: 40px;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="-2.82 -4.07 419.64 80.14"><style>svg {enable-background:new 0 0 414 73}</style><path d="M101.2 32c3.3 0 5.9-1.3 8-3.9l4.2 4.4c-3.4 3.8-7.3 5.7-11.9 5.7-4.6 0-8.3-1.4-11.3-4.3s-4.4-6.5-4.4-10.9 1.5-8.1 4.5-11c3-2.9 6.7-4.4 11.1-4.4 4.9 0 9 1.9 12.2 5.6l-4.1 4.7c-2.1-2.6-4.7-3.9-7.8-3.9-2.5 0-4.6.8-6.4 2.4-1.8 1.6-2.7 3.8-2.7 6.6s.8 5 2.5 6.7c1.7 1.4 3.7 2.3 6.1 2.3zm17.5 5.8V8.1h6.6v23.8H138v5.9zm48.3-4.1c-3 2.9-6.7 4.4-11.1 4.4-4.4 0-8.1-1.5-11.1-4.4-3-2.9-4.5-6.6-4.5-10.9s1.5-8 4.5-10.9c3-2.9 6.7-4.4 11.1-4.4 4.4 0 8.1 1.5 11.1 4.4 3 2.9 4.5 6.6 4.5 10.9s-1.5 8-4.5 10.9zm-2.3-10.9c0-2.6-.9-4.9-2.5-6.8-1.7-1.9-3.8-2.8-6.3-2.8s-4.6.9-6.3 2.8c-1.7 1.9-2.6 4.1-2.6 6.8s.9 4.9 2.6 6.8c1.7 1.9 3.8 2.8 6.3 2.8s4.6-.9 6.3-2.8 2.5-4.1 2.5-6.8zm20.3 7.4c1.1 1.4 2.6 2.1 4.5 2.1s3.4-.7 4.4-2.1c1.1-1.4 1.6-3.3 1.6-5.6V8.1h6.6v16.6c0 4.3-1.2 7.6-3.6 9.9-2.4 2.3-5.4 3.5-9.1 3.5-3.7 0-6.8-1.2-9.2-3.5-2.4-2.3-3.6-5.6-3.6-9.9V8.1h6.6v16.4c.1 2.4.7 4.3 1.8 5.7zM231.7 12c2.8 2.6 4.3 6.2 4.3 10.8s-1.4 8.3-4.1 11c-2.8 2.7-7 4-12.6 4H209V8.1h10.5c5.3 0 9.4 1.3 12.2 3.9zm-4.8 17.7c1.6-1.5 2.4-3.8 2.4-6.7 0-2.9-.8-5.2-2.4-6.7-1.6-1.6-4.1-2.4-7.5-2.4h-3.7V32h4.2c3 0 5.3-.8 7-2.3zm46.8-21.6h6.7v29.7h-6.7l-14.1-18.6v18.6h-6.7V8.1h6.3l14.5 19.2zm35.1 29.7l-2.8-6.4h-12.5l-2.8 6.4h-7.1l12.8-29.7h6.4l12.8 29.7h-6.8zM299.9 17l-3.7 8.6h7.4l-3.7-8.6zm29.9-3.1v23.9h-6.7V13.9h-8.4V8.1h23.5v5.8zM343 8.1h6.6v29.7H343zM367.9 27l7.6-18.9h7.2l-12 29.7h-5.6L353.2 8.1h7.2zm39.7-18.9V14h-14.8v6.2h13.3v5.6h-13.3V32h15.3v5.8h-21.9V8.1zm-314 55c1.8 0 3.3-.7 4.5-2.2l2.4 2.5c-1.9 2.1-4.1 3.2-6.7 3.2s-4.7-.8-6.3-2.4c-1.7-1.6-2.5-3.7-2.5-6.1 0-2.5.8-4.5 2.5-6.2s3.8-2.5 6.2-2.5c2.7 0 5 1 6.9 3.1l-2.3 2.6c-1.2-1.5-2.6-2.2-4.4-2.2-1.4 0-2.6.5-3.6 1.4-1 .9-1.5 2.1-1.5 3.7s.5 2.8 1.4 3.7c.9 1 2.1 1.4 3.4 1.4zm23.4 1c-1.7 1.6-3.8 2.5-6.2 2.5s-4.5-.8-6.2-2.5c-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5s4.5.8 6.2 2.5c1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.2-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c.9-1 1.4-2.3 1.4-3.8zm21.8-2.1l-4.5 9.2h-2.2l-4.5-9.2v10.5h-3.7V49.8h5l4.3 9.1 4.3-9.1h5v16.6h-3.7zm19.3-4.6c1.2 1 1.8 2.5 1.8 4.6s-.6 3.6-1.8 4.5c-1.2 1-3 1.4-5.5 1.4h-2.2v4.6h-3.7V49.8h5.9c2.5 0 4.3.5 5.5 1.5zm-2.7 6.5c.4-.5.7-1.2.7-2.2s-.3-1.6-.9-2c-.6-.4-1.5-.6-2.7-.6h-2.1v5.6h2.5c1.2 0 2-.3 2.5-.8zm11.8 4.3c.6.8 1.5 1.2 2.5 1.2s1.9-.4 2.5-1.2c.6-.8.9-1.8.9-3.2v-9.2h3.7V59c0 2.4-.7 4.3-2 5.6-1.3 1.3-3 1.9-5.1 1.9s-3.8-.7-5.1-2-2-3.2-2-5.6v-9.3h3.7V59c0 1.3.3 2.4.9 3.1zm20.5-9.1v13.4h-3.7V53h-4.7v-3.2h13.1V53zm7.5-3.2h3.7v16.7H194zm19.4 0h3.7v16.6h-3.7L205.5 56v10.4h-3.7V49.8h3.4l8.2 10.7zm18.7 8.2h3.7v5.9c-1.7 1.8-3.9 2.8-6.9 2.8-2.4 0-4.5-.8-6.2-2.4-1.7-1.6-2.5-3.7-2.5-6.1 0-2.5.8-4.5 2.5-6.2s3.7-2.5 6.1-2.5c2.4 0 4.5.8 6.2 2.4l-1.9 2.8c-.7-.7-1.4-1.1-2.1-1.3-.6-.2-1.3-.4-2-.4-1.4 0-2.6.5-3.6 1.4-1 1-1.5 2.2-1.5 3.8 0 1.6.5 2.8 1.4 3.8.9.9 2 1.4 3.3 1.4 1.3 0 2.4-.2 3.2-.7l.3-4.7zm24.7-8.2V53h-7.7v3.6h7.4v3.3h-7.4v6.5h-3.7V49.8zM274 64.1c-1.7 1.6-3.8 2.5-6.2 2.5-2.5 0-4.5-.8-6.2-2.5-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5 2.5 0 4.5.8 6.2 2.5 1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.3-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c1-1 1.4-2.3 1.4-3.8zm11.4 4.1c.6.8 1.5 1.2 2.5 1.2s1.9-.4 2.5-1.2c.6-.8.9-1.8.9-3.2v-9.2h3.7V59c0 2.4-.7 4.3-2 5.6-1.3 1.3-3 1.9-5.1 1.9s-3.8-.7-5.1-2c-1.3-1.3-2-3.2-2-5.6v-9.3h3.7V59c-.1 1.3.3 2.4.9 3.1zm25.1-12.3h3.7v16.6h-3.7L301.3 56v10.4h-3.7V49.8h3.4l8.2 10.7zm20.5 2.2c1.6 1.5 2.4 3.5 2.4 6.1 0 2.6-.8 4.6-2.3 6.1-1.5 1.5-3.9 2.3-7.1 2.3H317V49.8h5.9c3 0 5.3.7 6.8 2.2zm-2.7 9.9c.9-.9 1.4-2.1 1.4-3.7s-.5-2.9-1.4-3.8c-.9-.9-2.3-1.3-4.2-1.3h-2.1v10.1h2.4c1.7-.1 3-.5 3.9-1.3zm19.7 4.5l-1.5-3.6h-7l-1.5 3.6h-4l7.2-16.7h3.6l7.2 16.7h-4zm-5-11.7l-2.1 4.8h4.2l-2.1-4.8zm16.8-1.7v13.4h-3.8V53H350v-3.2h13.2V53zm7.4-3.2h3.7v16.7h-3.7zm21.8 14.3c-1.7 1.6-3.8 2.5-6.2 2.5-2.5 0-4.5-.8-6.2-2.5-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5 2.5 0 4.5.8 6.2 2.5 1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.2-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c.9-1 1.4-2.3 1.4-3.8zm18.5-8.2h3.8v16.6H405L397.1 56v10.4h-3.7V49.8h3.5l8.1 10.7z"/><path fill="#446ca9" d="M14.5 46.7H5.4v21.4h21.3v-9.2H14.5zm45.8.1v12.1H48.1v-.1 9.3h21.3V46.7h-9.2zM5.4 25.4h9.2l-.1-.1V13.2h12.2V4H5.4zM48.1 4v9.2h12.2v12.2h9.1V4z"/><path fill="#76c4d5" d="M46.9 25.4L34.7 13.2h13.4V4H26.7v9.2l12.2 12.2zm-11 21.3h-8L38 56.8l2 2.1H26.7v9.2h21.4v-9.3l-6.1-6zm24.4-21.3v13.3l-2.1-2.1-10.1-10.1v8.1l6 6 6.1 6.1h9.2V25.4zM26.7 37.5L14.6 25.4H5.4v21.3h9.1V33.4l12.2 12.2z"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

+2 -1
View File
@@ -28,10 +28,11 @@ nav:
- Features:
- Software Catalog:
- Overview: 'features/software-catalog/index.md'
- Installation: 'features/software-catalog/installation.md'
- Configuration: 'features/software-catalog/configuration.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.md'
- Configuration: 'features/software-catalog/configuration.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- API: 'features/software-catalog/api.md'
+24 -24
View File
@@ -1,32 +1,32 @@
{
"name": "example-app",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-api-docs": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/plugin-circleci": "^0.1.1-alpha.21",
"@backstage/plugin-explore": "^0.1.1-alpha.21",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.21",
"@backstage/plugin-github-actions": "^0.1.1-alpha.21",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.21",
"@backstage/plugin-graphiql": "^0.1.1-alpha.21",
"@backstage/plugin-jenkins": "^0.1.1-alpha.21",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.21",
"@backstage/plugin-newrelic": "^0.1.1-alpha.21",
"@backstage/plugin-register-component": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
"@backstage/plugin-sentry": "^0.1.1-alpha.21",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
"@backstage/plugin-welcome": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-api-docs": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/plugin-circleci": "^0.1.1-alpha.22",
"@backstage/plugin-explore": "^0.1.1-alpha.22",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.22",
"@backstage/plugin-github-actions": "^0.1.1-alpha.22",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.22",
"@backstage/plugin-graphiql": "^0.1.1-alpha.22",
"@backstage/plugin-jenkins": "^0.1.1-alpha.22",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.22",
"@backstage/plugin-newrelic": "^0.1.1-alpha.22",
"@backstage/plugin-register-component": "^0.1.1-alpha.22",
"@backstage/plugin-rollbar": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.22",
"@backstage/plugin-sentry": "^0.1.1-alpha.22",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs": "^0.1.1-alpha.22",
"@backstage/plugin-welcome": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
@@ -35,8 +35,6 @@ import {
SidebarSearchField,
SidebarSpace,
SidebarUserSettings,
SidebarThemeToggle,
SidebarPinButton,
DefaultProviderSettings,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
@@ -103,9 +101,7 @@ const Root: FC<{}> = ({ children }) => (
/>
<SidebarSpace />
<SidebarDivider />
<SidebarThemeToggle />
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
<SidebarPinButton />
</Sidebar>
{children}
</SidebarPage>
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -42,12 +42,12 @@
"helmet": "^4.0.0",
"knex": "^0.21.1",
"lodash": "^4.17.15",
"logform": "^2.1.1",
"morgan": "^1.10.0",
"prom-client": "^12.0.0",
"selfsigned": "^1.10.7",
"stoppable": "^1.1.0",
"winston": "^3.2.1",
"logform": "^2.1.1"
"winston": "^3.2.1"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -58,7 +58,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,23 +18,23 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/plugin-app-backend": "^0.1.1-alpha.21",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.21",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.21",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.21",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.21",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.21",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.21",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/plugin-app-backend": "^0.1.1-alpha.22",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.22",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.22",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.22",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.22",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.22",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.22",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.22",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.21",
"example-app": "^0.1.1-alpha.22",
"express": "^4.17.1",
"knex": "^0.21.1",
"pg": "^8.3.0",
@@ -43,7 +43,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+50 -21
View File
@@ -55,31 +55,60 @@ export default async function createPlugin({
const publishers = new Publishers();
const githubToken = config.getString('scaffolder.github.token');
const repoVisibility = config.getString(
'scaffolder.github.visibility',
) as RepoVisilityOptions;
const githubConfig = config.getOptionalConfig('scaffolder.github');
const githubClient = new Octokit({ auth: githubToken });
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
if (githubConfig) {
try {
const repoVisibility = githubConfig.getString(
'visibility',
) as RepoVisilityOptions;
const githubToken = githubConfig.getString('token');
const githubClient = new Octokit({ auth: githubToken });
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
} catch (e) {
const providerName = 'github';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
if (gitLabConfig) {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
try {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
} catch (e) {
const providerName = 'gitlab';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const dockerClient = new Docker();
+1 -1
View File
@@ -31,7 +31,7 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger);
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
const preparers = new Preparers();
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,9 +28,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -41,8 +41,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/test-utils-core": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -80,6 +80,15 @@ export class UserFlags extends Map<FeatureFlagName, FeatureFlagState> {
return output;
}
toggle(name: FeatureFlagName): FeatureFlagState {
if (super.get(name) === FeatureFlagState.On) {
super.set(name, FeatureFlagState.Off);
} else {
super.set(name, FeatureFlagState.On);
}
return super.get(name) || FeatureFlagState.Off;
}
delete(name: FeatureFlagName): boolean {
const output = super.delete(name);
this.save();
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -54,8 +54,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -1,75 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, useContext } from 'react';
import { makeStyles } from '@material-ui/core';
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
import { SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext } from './Page';
const ARROW_BUTTON_SIZE = 20;
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(theme => {
return {
root: {
position: 'relative',
alignSelf: 'stretch',
},
arrowButtonWrapper: {
position: 'absolute',
right: 0,
width: ARROW_BUTTON_SIZE,
height: ARROW_BUTTON_SIZE,
top: -(theme.spacing(6) + ARROW_BUTTON_SIZE) / 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '2px 0px 0px 2px',
background: theme.palette.pinSidebarButton.background,
color: theme.palette.pinSidebarButton.icon,
border: 'none',
outline: 'none',
cursor: 'pointer',
},
arrowButtonIcon: {
transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'),
},
};
});
export const SidebarPinButton: FC<{}> = () => {
const { isOpen } = useContext(SidebarContext);
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const classes = useStyles({ isPinned });
return (
<div className={classes.root}>
{isOpen && (
<button
className={classes.arrowButtonWrapper}
onClick={toggleSidebarPinState}
>
<DoubleArrowIcon
className={classes.arrowButtonIcon}
style={{ fontSize: 14 }}
/>
</button>
)}
</div>
);
};
@@ -0,0 +1,26 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { List, ListSubheader } from '@material-ui/core';
import { SidebarThemeToggle } from './ThemeToggle';
import { SidebarPinButton } from './PinButton';
export const AppSettingsList = () => (
<List dense subheader={<ListSubheader>App Settings</ListSubheader>}>
<SidebarThemeToggle />
<SidebarPinButton />
</List>
);
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader';
type Props = {
providerSettings: React.ReactNode;
};
export const AuthProvidersList = ({ providerSettings }: Props) => (
<List subheader={<ListSubheader>Available Auth Providers</ListSubheader>}>
{providerSettings}
</List>
);
@@ -0,0 +1,73 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
FeatureFlagName,
useApi,
featureFlagsApiRef,
} from '@backstage/core-api';
import {
ListItem,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import CheckIcon from '@material-ui/icons/CheckCircle';
import { ToggleButton } from '@material-ui/lab';
export type Item = {
name: FeatureFlagName;
pluginId: string;
};
type Props = {
featureFlag: Item;
};
export const FlagItem = ({ featureFlag }: Props) => {
const api = useApi(featureFlagsApiRef);
const [enabled, setEnabled] = React.useState(
Boolean(api.getFlags().get(featureFlag.name)),
);
const toggleFlag = () => {
const newState = api.getFlags().toggle(featureFlag.name);
setEnabled(Boolean(newState));
};
return (
<ListItem>
<ListItemText
primary={featureFlag.name}
secondary={`Registered in ${featureFlag.pluginId} plugin`}
/>
<ListItemSecondaryAction>
<ToggleButton
size="small"
value="flag"
selected={enabled}
onChange={toggleFlag}
>
<Tooltip placement="top" arrow title={enabled ? 'Disable' : 'Enable'}>
<CheckIcon />
</Tooltip>
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
};
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import List from '@material-ui/core/List';
import ListSubheader from '@material-ui/core/ListSubheader';
import { FlagItem, Item } from './FeatureFlagsItem';
type Props = {
featureFlags: Item[];
};
export const FeatureFlagsList = ({ featureFlags }: Props) => (
<List dense subheader={<ListSubheader>Feature Flags</ListSubheader>}>
{featureFlags.map(featureFlag => (
<FlagItem key={featureFlag.name} featureFlag={featureFlag} />
))}
</List>
);
@@ -0,0 +1,64 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react';
import {
ListItem,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import { ToggleButton } from '@material-ui/lab';
import { SidebarPinStateContext } from '../Page';
export const SidebarPinButton = () => {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const PinIcon = () => (
<Tooltip
placement="top"
arrow
title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`}
>
{isPinned ? <LockIcon /> : <LockOpenIcon />}
</Tooltip>
);
return (
<ListItem>
<ListItemText
primary="Pin Sidebar"
secondary="Prevent the sidebar from collapsing"
/>
<ListItemSecondaryAction>
<ToggleButton
size="small"
value="pin"
selected={isPinned}
onChange={() => {
toggleSidebarPinState();
}}
>
<PinIcon />
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
};
@@ -14,31 +14,53 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api';
import { SidebarItem } from '../Items';
import { IconButton, Tooltip } from '@material-ui/core';
import StarBorder from '@material-ui/icons/StarBorder';
import React from 'react';
import { IconComponent, OAuthApi, OpenIdConnectApi } from '@backstage/core-api';
import {
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
Tooltip,
} from '@material-ui/core';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
import { ToggleButton } from '@material-ui/lab';
export const ProviderSettingsItem: FC<{
type Props = {
title: string;
icon: IconComponent;
signedIn: boolean;
api: OAuthApi | OpenIdConnectApi;
signInHandler: Function;
}> = ({ title, icon, signedIn, api, signInHandler }) => {
return (
<SidebarItem key={title} text={title} icon={icon ?? StarBorder}>
<IconButton onClick={() => (signedIn ? api.logout() : signInHandler())}>
};
export const ProviderSettingsItem = ({
title,
icon: Icon,
signedIn,
api,
signInHandler,
}: Props) => (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={title} />
<ListItemSecondaryAction>
<ToggleButton
size="small"
value={title}
selected={signedIn}
onChange={() => (signedIn ? api.logout() : signInHandler())}
>
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
>
<PowerButton color={signedIn ? 'secondary' : 'primary'} />
<PowerButton />
</Tooltip>
</IconButton>
</SidebarItem>
);
};
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
@@ -0,0 +1,74 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
Card,
CardContent,
CardHeader,
makeStyles,
Divider,
} from '@material-ui/core';
import { AppSettingsList } from './AppSettingsList';
import { AuthProvidersList } from './AuthProviderList';
import { FeatureFlagsList } from './FeatureFlagsList';
import { SignInAvatar } from './SignInAvatar';
import { UserSettingsMenu } from './UserSettingsMenu';
import { useUserProfile } from './useUserProfileInfo';
import { useApi, featureFlagsApiRef } from '@backstage/core-api';
const useStyles = makeStyles({
root: {
minWidth: 400,
},
});
type Props = {
providerSettings?: React.ReactNode;
};
export const SettingsDialog = ({ providerSettings }: Props) => {
const classes = useStyles();
const { profile, displayName } = useUserProfile();
const featureFlagsApi = useApi(featureFlagsApiRef);
const featureFlags = featureFlagsApi.getRegisteredFlags();
return (
<Card className={classes.root}>
<CardHeader
avatar={<SignInAvatar size={48} />}
action={<UserSettingsMenu />}
title={displayName}
subheader={profile.email}
/>
<CardContent>
<AppSettingsList />
{providerSettings && (
<>
<Divider />
<AuthProvidersList providerSettings={providerSettings} />
</>
)}
{featureFlags.length > 0 && (
<>
<Divider />
<FeatureFlagsList featureFlags={featureFlags} />
</>
)}
</CardContent>
</Card>
);
};
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, Avatar } from '@material-ui/core';
import { useUserProfile } from './useUserProfileInfo';
import { sidebarConfig } from '../config';
const useStyles = makeStyles<BackstageTheme, { size: number }>({
avatar: {
width: ({ size }) => size,
height: ({ size }) => size,
},
});
type Props = { size?: number };
export const SignInAvatar = ({ size }: Props) => {
const { iconSize } = sidebarConfig;
const classes = useStyles(size ? { size } : { size: iconSize });
const { profile, displayName } = useUserProfile();
return (
<Avatar src={profile.picture} className={classes.avatar}>
{displayName[0]}
</Avatar>
);
};
@@ -0,0 +1,87 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useObservable } from 'react-use';
import LightIcon from '@material-ui/icons/WbSunny';
import DarkIcon from '@material-ui/icons/Brightness2';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
import { appThemeApiRef, useApi } from '@backstage/core-api';
import ToggleButton from '@material-ui/lab/ToggleButton';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import {
ListItem,
ListItemText,
ListItemSecondaryAction,
Tooltip,
} from '@material-ui/core';
export const SidebarThemeToggle = () => {
const appThemeApi = useApi(appThemeApiRef);
const themeId = useObservable(
appThemeApi.activeThemeId$(),
appThemeApi.getActiveThemeId(),
);
const themeIds = appThemeApi.getInstalledThemes();
// TODO(marcuseide): can these be put on the theme itself?
const themeIcons = {
dark: <DarkIcon />,
light: <LightIcon />,
};
const handleSetTheme = (
_event: React.MouseEvent<HTMLElement>,
newThemeId: string | undefined,
) => {
if (themeIds.some(t => t.id === newThemeId)) {
appThemeApi.setActiveThemeId(newThemeId);
} else {
appThemeApi.setActiveThemeId(undefined);
}
};
return (
<ListItem>
<ListItemText primary="Theme" secondary="Change the theme mode" />
<ListItemSecondaryAction>
<ToggleButtonGroup
exclusive
size="small"
value={themeId ?? 'auto'}
onChange={handleSetTheme}
>
{themeIds.map(theme => (
<ToggleButton key={theme.id} value={theme.variant}>
<Tooltip
placement="top"
arrow
title={`Select ${theme.variant} theme`}
>
{themeIcons[theme.variant]}
</Tooltip>
</ToggleButton>
))}
<ToggleButton value="auto">
<Tooltip placement="top" arrow title="Select auto theme">
<AutoIcon />
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
</ListItemSecondaryAction>
</ListItem>
);
};
@@ -1,61 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, useRef } from 'react';
import { makeStyles, Avatar, Divider } from '@material-ui/core';
import { useApi, identityApiRef } from '@backstage/core-api';
import { SidebarItem } from '../Items';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
const useStyles = makeStyles({
avatar: {
width: 24,
height: 24,
},
});
export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
open,
setOpen,
}) => {
const ref = useRef<Element>(); // for scrolling down when collapse item opens
const classes = useStyles();
const identityApi = useApi(identityApiRef);
const handleClick = () => {
setOpen(!open);
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
};
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
const displayName = profile.displayName ?? userId;
const SignInAvatar = () => (
<Avatar src={profile.picture} className={classes.avatar}>
{displayName[0]}
</Avatar>
);
return (
<>
<Divider innerRef={ref} />
<SidebarItem text={displayName} onClick={handleClick} icon={SignInAvatar}>
{open ? <ExpandMore /> : <ExpandLess />}
</SidebarItem>
</>
);
};
@@ -0,0 +1,77 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useContext } from 'react';
import { Popover } from '@material-ui/core';
import { SignInAvatar } from './SignInAvatar';
import { SettingsDialog } from './SettingsDialog';
import { SidebarItem } from '../Items';
import { useUserProfile } from './useUserProfileInfo';
import { SidebarContext } from '../config';
type Props = {
providerSettings?: React.ReactNode;
};
export const SidebarUserSettings = ({ providerSettings }: Props) => {
const { isOpen: sidebarOpen } = useContext(SidebarContext);
const { displayName } = useUserProfile();
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | undefined>(
undefined,
);
const handleOpen = (event?: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event?.currentTarget ?? undefined);
setOpen(true);
};
const handleClose = () => {
setAnchorEl(undefined);
setOpen(false);
};
useEffect(() => {
if (!sidebarOpen && open) setOpen(false);
}, [open, sidebarOpen]);
const SidebarAvatar = () => <SignInAvatar />;
return (
<>
<SidebarItem
text={displayName}
onClick={handleOpen}
icon={SidebarAvatar}
/>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<SettingsDialog providerSettings={providerSettings} />
</Popover>
</>
);
};
@@ -0,0 +1,55 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { identityApiRef, useApi } from '@backstage/core-api';
import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
import MoreVertIcon from '@material-ui/icons/MoreVert';
export const UserSettingsMenu = () => {
const identityApi = useApi(identityApiRef);
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState<undefined | HTMLElement>(
undefined,
);
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};
const handleClose = () => {
setAnchorEl(undefined);
setOpen(false);
};
return (
<>
<IconButton onClick={handleOpen}>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => identityApi.logout()}>
<ListItemIcon>
<SignOutIcon />
</ListItemIcon>
Sign Out
</MenuItem>
</Menu>
</>
);
};
@@ -17,4 +17,4 @@
export { ProviderSettingsItem } from './ProviderSettingsItem';
export { OAuthProviderSettings } from './OAuthProviderSettings';
export { OIDCProviderSettings } from './OIDCProviderSettings';
export { UserProfile } from './UserProfile';
export { SidebarUserSettings } from './UserSettings';
@@ -0,0 +1,26 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useApi, identityApiRef } from '@backstage/core-api';
export const useUserProfile = () => {
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const profile = identityApi.getProfile();
const displayName = profile.displayName ?? userId;
return { profile, displayName };
};
@@ -1,58 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { useObservable } from 'react-use';
import LightIcon from '@material-ui/icons/WbSunny';
import DarkIcon from '@material-ui/icons/Brightness2';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
import { appThemeApiRef, useApi } from '@backstage/core-api';
import { SidebarItem } from './Items';
export const SidebarThemeToggle: FC<{}> = () => {
const appThemeApi = useApi(appThemeApiRef);
const themeId = useObservable(
appThemeApi.activeThemeId$(),
appThemeApi.getActiveThemeId(),
);
let text = 'Auto';
let icon = AutoIcon;
switch (themeId) {
case 'dark':
text = 'Dark mode';
icon = DarkIcon;
break;
case 'light':
text = 'Light mode';
icon = LightIcon;
break;
default:
break;
}
const handleToggle = () => {
if (!themeId) {
appThemeApi.setActiveThemeId('light');
} else if (themeId === 'light') {
appThemeApi.setActiveThemeId('dark');
} else {
appThemeApi.setActiveThemeId(undefined);
}
};
return <SidebarItem text={text} onClick={handleToggle} icon={icon} />;
};
@@ -1,53 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { identityApiRef, useApi } from '@backstage/core-api';
import Collapse from '@material-ui/core/Collapse';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
import React, { useContext, useEffect } from 'react';
import { SidebarContext } from './config';
import { SidebarItem } from './Items';
import { UserProfile as SidebarUserProfile } from './Settings';
type SidebarUserSettingsProps = { providerSettings?: React.ReactNode };
export function SidebarUserSettings({
providerSettings,
}: SidebarUserSettingsProps) {
const { isOpen: sidebarOpen } = useContext(SidebarContext);
const [open, setOpen] = React.useState(false);
const identityApi = useApi(identityApiRef);
// Close the provider list when sidebar collapse
useEffect(() => {
if (!sidebarOpen && open) setOpen(false);
}, [open, sidebarOpen]);
return (
<>
<SidebarUserProfile open={open} setOpen={setOpen} />
<Collapse in={open} timeout="auto">
{providerSettings}
<SidebarItem
icon={SignOutIcon}
text="Sign Out"
onClick={() => identityApi.logout()}
/>
</Collapse>
</>
);
}
@@ -25,14 +25,11 @@ export {
SidebarSpacer,
} from './Items';
export { IntroCard, SidebarIntro } from './Intro';
export { SidebarPinButton } from './PinButton';
export {
SIDEBAR_INTRO_LOCAL_STORAGE,
SidebarContext,
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
export { SidebarThemeToggle } from './SidebarThemeToggle';
export { SidebarUserSettings } from './UserSettings';
export { DefaultProviderSettings } from './DefaultProviderSettings';
export * from './Settings';
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"fs-extra": "^9.0.0",
@@ -46,6 +46,8 @@ proxy:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
generators:
techdocs: 'docker'
lighthouse:
baseUrl: http://localhost:3003
@@ -65,10 +67,6 @@ catalog:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
providers:
- target: https://github.com
token:
@@ -18,8 +18,6 @@ import {
SidebarContext,
SidebarSpace,
SidebarUserSettings,
SidebarThemeToggle,
SidebarPinButton,
DefaultProviderSettings,
} from '@backstage/core';
@@ -39,9 +37,7 @@ export const AppSidebar = () => (
<SidebarDivider />
<SidebarSpace />
<SidebarDivider />
<SidebarThemeToggle />
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
<SidebarPinButton />
</Sidebar>
);
@@ -15,7 +15,7 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger);
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"homepage": "https://backstage.io",
"repository": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": true,
"homepage": "https://backstage.io",
"repository": {
@@ -21,7 +21,7 @@
"test:e2e": "yarn start"
},
"devDependencies": {
"@backstage/cli-common": "^0.1.1-alpha.21",
"@backstage/cli-common": "^0.1.1-alpha.22",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"fs-extra": "^9.0.0",
+11 -1
View File
@@ -403,5 +403,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) {
}
}
process.on('unhandledRejection', handleError);
process.on('unhandledRejection', (error: Error) => {
// Try to avoid exiting if the unhandled error is coming from jsdom, i.e. zombie.
// Those are typically errors on the page that should be benign, at least in the
// context of this test. We have other ways of asserting that the page is being
// rendered correctly.
if (error?.stack?.includes('node_modules/jsdom/lib')) {
console.log(`Ignored error inside jsdom, ${error}`);
} else {
handleError(error);
}
});
main().catch(handleError);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
"@backstage/theme": "^0.1.1-alpha.21"
"@backstage/theme": "^0.1.1-alpha.22"
},
"devDependencies": {
"@storybook/addon-actions": "^6.0.21",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public"
@@ -44,7 +44,7 @@
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"commander": "^6.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/test-utils-core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21"
"@backstage/cli": "^0.1.1-alpha.22"
},
"files": [
"dist"
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@kyma-project/asyncapi-react": "^0.11.0",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
@@ -39,9 +39,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -20,6 +20,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { apiDocsConfigRef } from '../../config';
import { ApiExplorerPage } from './ApiExplorerPage';
describe('ApiCatalogPage', () => {
@@ -32,6 +33,7 @@ describe('ApiCatalogPage', () => {
metadata: {
name: 'Entity1',
},
spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
@@ -39,12 +41,17 @@ describe('ApiCatalogPage', () => {
metadata: {
name: 'Entity2',
},
spec: { type: 'openapi' },
},
] as Entity[]),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
const apiDocsConfig = {
getApiDefinitionWidget: () => undefined,
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
@@ -52,6 +59,7 @@ describe('ApiCatalogPage', () => {
apis={ApiRegistry.from([
[catalogApiRef, catalogApi],
[storageApiRef, MockStorageApi.create()],
[apiDocsConfigRef, apiDocsConfig],
])}
>
{children}
@@ -15,9 +15,11 @@
*/
import { Entity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import * as React from 'react';
import { apiDocsConfigRef } from '../../config';
import { ApiExplorerTable } from './ApiExplorerTable';
const entites: Entity[] = [
@@ -25,29 +27,38 @@ const entites: Entity[] = [
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api1' },
spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api2' },
spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api3' },
spec: { type: 'grpc' },
},
];
const apiRegistry = ApiRegistry.with(apiDocsConfigRef, {
getApiDefinitionWidget: () => undefined,
});
describe('ApiCatalogTable component', () => {
it('should render error message when error is passed in props', async () => {
const rendered = render(
wrapInTestApp(
<ApiExplorerTable
titlePreamble="APIs"
entities={[]}
loading={false}
error={{ code: 'error' }}
/>,
<ApiProvider apis={apiRegistry}>
<ApiExplorerTable
titlePreamble="APIs"
entities={[]}
loading={false}
error={{ code: 'error' }}
/>
</ApiProvider>,
),
);
const errorMessage = await rendered.findByText(
@@ -59,11 +70,13 @@ describe('ApiCatalogTable component', () => {
it('should display entity names when loading has finished and no error occurred', async () => {
const rendered = render(
wrapInTestApp(
<ApiExplorerTable
titlePreamble="APIs"
entities={entites}
loading={false}
/>,
<ApiProvider apis={apiRegistry}>
<ApiExplorerTable
titlePreamble="APIs"
entities={entites}
loading={false}
/>
</ApiProvider>,
),
);
expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument();
@@ -14,14 +14,23 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import { Link, Chip } from '@material-ui/core';
import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
import { Table, TableColumn, useApi } from '@backstage/core';
import { Chip, Link } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { apiDocsConfigRef } from '../../config';
import { entityRoute } from '../../routes';
const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => {
const config = useApi(apiDocsConfigRef);
const definition = config.getApiDefinitionWidget(apiEntity);
const type = definition ? definition.title : apiEntity.spec.type;
return <span>{type}</span>;
};
const columns: TableColumn<Entity>[] = [
{
title: 'Name',
@@ -54,8 +63,11 @@ const columns: TableColumn<Entity>[] = [
field: 'spec.lifecycle',
},
{
title: 'Type', // TODO: Resolve the type display name using the API from https://github.com/spotify/backstage/pull/2451
title: 'Type',
field: 'spec.type',
render: (entity: Entity) => (
<ApiTypeTitle apiEntity={entity as ApiEntityV1alpha1} />
),
},
{
title: 'Description',
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config-loader": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config-loader": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -30,7 +30,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/supertest": "^2.0.8",
"msw": "^0.19.5",
"supertest": "^4.0.2"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
@@ -49,7 +49,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,13 +20,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"git-url-parse": "^11.2.0",
"knex": "^0.21.1",
"lodash": "^4.17.15",
"morgan": "^1.10.0",
@@ -39,7 +40,8 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/git-url-parse": "^9.0.0",
"@types/lodash": "^4.14.151",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
@@ -343,7 +343,14 @@ export class CommonDatabase implements Database {
entityName?: string,
message?: string,
): Promise<void> {
return this.database<DatabaseLocationUpdateLogEvent>(
// Remove log entries older than a day
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 1);
await this.database<DatabaseLocationUpdateLogEvent>('location_update_log')
.where('created_at', '<', cutoff.toISOString())
.del();
await this.database<DatabaseLocationUpdateLogEvent>(
'location_update_log',
).insert({
status,
@@ -27,7 +27,6 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
import { FileReaderProcessor } from './processors/FileReaderProcessor';
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor';
import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor';
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor';
@@ -67,18 +66,19 @@ export class LocationReaders implements LocationReader {
private readonly rulesEnforcer: CatalogRulesEnforcer;
static defaultProcessors(options: {
logger: Logger;
config?: Config;
entityPolicy?: EntityPolicy;
}): LocationProcessor[] {
const {
logger,
config = new ConfigReader({}, 'missing-config'),
entityPolicy = new EntityPolicies(),
} = options;
return [
StaticLocationProcessor.fromConfig(config),
new FileReaderProcessor(),
new GithubReaderProcessor(config),
GithubApiReaderProcessor.fromConfig(config),
GithubReaderProcessor.fromConfig(config, logger),
new GitlabApiReaderProcessor(config),
new GitlabReaderProcessor(),
new BitbucketApiReaderProcessor(config),
@@ -94,7 +94,7 @@ export class LocationReaders implements LocationReader {
constructor({
logger = getVoidLogger(),
config,
processors = LocationReaders.defaultProcessors({ config }),
processors = LocationReaders.defaultProcessors({ logger, config }),
}: Options) {
this.logger = logger;
this.processors = processors;
@@ -1,162 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
getRawUrl,
getRequestOptions,
GithubApiReaderProcessor,
ProviderConfig,
readConfig,
} from './GithubApiReaderProcessor';
describe('GithubApiReaderProcessor', () => {
describe('getRequestOptions', () => {
it('sets the correct API version', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect((getRequestOptions(config).headers as any).Accept).toEqual(
'application/vnd.github.v3.raw',
);
});
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
};
expect(
(getRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getRawUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('passes through the happy path', () => {
const config: ProviderConfig = {
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
};
expect(
getRawUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
});
describe('readConfig', () => {
function config(
providers: { target: string; apiBaseUrl?: string; token?: string }[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
catalog: { processors: { githubApi: { providers } } },
},
},
]);
}
it('adds a default GitHub entry when missing', () => {
const output = readConfig(config([]));
expect(output).toEqual([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
});
it('injects the correct GitHub API base URL when missing', () => {
const output = readConfig(config([{ target: 'https://github.com' }]));
expect(output).toEqual([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
});
it('rejects custom targets with no API base URL', () => {
expect(() =>
readConfig(config([{ target: 'https://ghe.company.com' }])),
).toThrow(
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl',
);
});
it('rejects funky configs', () => {
expect(() => readConfig(config([{ target: 7 } as any]))).toThrow(
/target/,
);
expect(() => readConfig(config([{ noTarget: '7' } as any]))).toThrow(
/target/,
);
expect(() =>
readConfig(
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
),
).toThrow(/apiBaseUrl/);
expect(() =>
readConfig(config([{ target: 'https://github.com', token: 7 } as any])),
).toThrow(/token/);
});
});
describe('implementation', () => {
it('rejects unknown types', async () => {
const processor = new GithubApiReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'not-github/api',
target: 'https://github.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});
it('rejects unknown targets', async () => {
const processor = new GithubApiReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'github/api',
target: 'https://not.github.com/apa',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
);
});
});
});
@@ -1,192 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*/
apiBaseUrl: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous API access is used.
*/
token?: string;
};
export function getRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const oldPath = new URL(target).pathname.split('/');
const [, userOrOrg, repoName, blobOrRaw, ref, ...restOfPath] = oldPath;
if (
!userOrOrg ||
!repoName ||
(blobOrRaw !== 'blob' && blobOrRaw !== 'raw') ||
!restOfPath.join('/').match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or Invalid file path');
}
// Transform to API path
const newPath = [
'repos',
userOrOrg,
repoName,
'contents',
...restOfPath,
].join('/');
return new URL(`${provider.apiBaseUrl}/${newPath}?ref=${ref}`);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(configRoot: Config): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const config = configRoot.getOptionalConfig('catalog.processors.githubApi');
const providerConfigs = config?.getOptionalConfigArray('providers') ?? [];
const legacyToken = config?.getOptionalString('privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
} else {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubApiReaderProcessor implements LocationProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config) {
return new GithubApiReaderProcessor(readConfig(config));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'github/api') {
return false;
}
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.github.processors.githubApi.`,
);
}
try {
const url = getRawUrl(location.target, provider);
const options = getRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.buffer();
emit(result.data(location, data));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
getApiRequestOptions,
getApiUrl,
getRawRequestOptions,
getRawUrl,
GithubReaderProcessor,
ProviderConfig,
readConfig,
} from './GithubReaderProcessor';
describe('GithubReaderProcessor', () => {
describe('getApiRequestOptions', () => {
it('sets the correct API version', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect((getApiRequestOptions(config).headers as any).Accept).toEqual(
'application/vnd.github.v3.raw',
);
});
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
apiBaseUrl: '',
};
expect(
(getApiRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getApiRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getRawRequestOptions', () => {
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
target: '',
rawBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
target: '',
rawBaseUrl: '',
};
expect(
(getRawRequestOptions(withToken).headers as any).Authorization,
).toEqual('token A');
expect(
(getRawRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});
});
describe('getApiUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
};
expect(
getApiUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
expect(
getApiUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
it('happy path for ghe', () => {
const config: ProviderConfig = {
target: 'https://ghe.mycompany.net',
apiBaseUrl: 'https://ghe.mycompany.net/api/v3',
};
expect(
getApiUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname',
),
);
});
});
describe('getRawUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { target: '', apiBaseUrl: '' };
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
target: 'https://github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
};
expect(
getRawUrl(
'https://github.com/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml',
),
);
});
it('happy path for ghe', () => {
const config: ProviderConfig = {
target: 'https://ghe.mycompany.net',
rawBaseUrl: 'https://ghe.mycompany.net/raw',
};
expect(
getRawUrl(
'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml',
config,
),
).toEqual(
new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'),
);
});
});
describe('readConfig', () => {
function config(
providers: { target: string; apiBaseUrl?: string; token?: string }[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
catalog: { processors: { github: { providers } } },
},
},
]);
}
it('adds a default GitHub entry when missing', () => {
const output = readConfig(config([]), getVoidLogger());
expect(output).toEqual([
{
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
]);
});
it('injects the correct GitHub API base URL when missing', () => {
const output = readConfig(
config([{ target: 'https://github.com' }]),
getVoidLogger(),
);
expect(output).toEqual([
{
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
]);
});
it('rejects custom targets with no base URLs', () => {
expect(() =>
readConfig(
config([{ target: 'https://ghe.company.com' }]),
getVoidLogger(),
),
).toThrow(
'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl',
);
});
it('rejects funky configs', () => {
expect(() =>
readConfig(config([{ target: 7 } as any]), getVoidLogger()),
).toThrow(/target/);
expect(() =>
readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()),
).toThrow(/target/);
expect(() =>
readConfig(
config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
getVoidLogger(),
),
).toThrow(/apiBaseUrl/);
expect(() =>
readConfig(
config([{ target: 'https://github.com', token: 7 } as any]),
getVoidLogger(),
),
).toThrow(/token/);
});
});
describe('implementation', () => {
it('rejects unknown types', async () => {
const processor = new GithubReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'not-github/api',
target: 'https://github.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});
it('rejects unknown targets', async () => {
const processor = new GithubReaderProcessor([
{ target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
]);
const location: LocationSpec = {
type: 'github/api',
target: 'https://not.github.com/apa',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
);
});
});
});
@@ -15,33 +15,207 @@
*/
import { LocationSpec } from '@backstage/catalog-model';
import fetch, { RequestInit, HeadersInit } from 'node-fetch';
import { Config } from '@backstage/config';
import parseGitUri from 'git-url-parse';
import fetch, { HeadersInit, RequestInit } from 'node-fetch';
import { Logger } from 'winston';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { Config } from '@backstage/config';
export class GithubReaderProcessor implements LocationProcessor {
private privateToken: string;
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
constructor(config?: Config) {
this.privateToken =
config?.getOptionalString('catalog.processors.github.privateToken') ?? '';
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
apiBaseUrl?: string;
/**
* The base URL of the raw fetch endpoint of this provider, e.g.
* "https://raw.githubusercontent.com", with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
rawBaseUrl?: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous access is used.
*/
token?: string;
};
export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
return {
headers,
};
}
if (this.privateToken !== '') {
headers.Authorization = `token ${this.privateToken}`;
export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getApiUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
!filepath?.match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or invalid file path');
}
const requestOptions: RequestInit = {
headers,
};
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
return requestOptions;
// Converts for example
// from: https://github.com/a/b/blob/branchname/c.yaml
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw') ||
!filepath?.match(/\.ya?ml$/)
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(config: Config, logger: Logger): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// TODO(freben): Deprecate the old config root entirely in a later release
if (config.has('catalog.processors.githubApi')) {
logger.warn(
'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead',
);
}
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const providerConfigs =
config.getOptionalConfigArray('catalog.processors.github.providers') ??
config.getOptionalConfigArray('catalog.processors.githubApi.providers') ??
[];
const legacyToken =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
}
if (rawBaseUrl) {
rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
rawBaseUrl = 'https://raw.githubusercontent.com';
}
if (!apiBaseUrl && !rawBaseUrl) {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, rawBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubReaderProcessor implements LocationProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config, logger: Logger) {
return new GithubReaderProcessor(readConfig(config, logger));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
@@ -49,16 +223,30 @@ export class GithubReaderProcessor implements LocationProcessor {
optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'github') {
// The github/api type is for backward compatibility
if (location.type !== 'github' && location.type !== 'github/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`,
);
}
// TODO(freben): Should "hard" errors thrown by this line be treated as
// notFound instead of fatal?
const response = await fetch(url.toString(), this.getRequestOptions());
try {
const useApi =
provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl);
const url = useApi
? getApiUrl(location.target, provider)
: getRawUrl(location.target, provider);
const options = useApi
? getApiRequestOptions(provider)
: getRawRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.buffer();
@@ -80,41 +268,4 @@ export class GithubReaderProcessor implements LocationProcessor {
return true;
}
// Converts
// from: https://github.com/a/b/blob/master/c.yaml
// to: https://raw.githubusercontent.com/a/b/master/c.yaml
private buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
repoName,
blobKeyword,
...restOfPath
] = url.pathname.split('/');
if (
url.hostname !== 'github.com' ||
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
blobKeyword !== 'blob' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong GitHub URL');
}
// Removing the "blob" part
url.pathname = [empty, userOrOrg, repoName, ...restOfPath].join('/');
url.hostname = 'raw.githubusercontent.com';
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,14 +21,15 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.21",
"@backstage/plugin-techdocs": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.22",
"@backstage/plugin-techdocs": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"classnames": "^2.2.6",
"moment": "^2.26.0",
"react": "^16.13.1",
@@ -37,13 +38,12 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"swr": "^0.3.0",
"@types/react": "^16.9"
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"postpack": "backstage-cli postpack"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -38,8 +38,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-explore",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,21 +21,21 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3",
"react-router": "6.0.0-beta.0"
"react-router": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gcp-projects",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -32,8 +32,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-actions",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,11 +21,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/core-api": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/core-api": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gitops-profiles",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -32,8 +32,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,8 +31,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -43,9 +43,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-graphql-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -19,7 +19,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"apollo-server": "^2.16.0",
"apollo-server-express": "^2.16.0",
@@ -31,7 +31,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/supertest": "^2.0.8",
"eslint-plugin-graphql": "^4.0.0",
"msw": "^0.20.5",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-identity-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -33,7 +33,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"jest-fetch-mock": "^3.0.3"
},
"files": [
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-jenkins",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-lighthouse",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -34,9 +34,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@backstage/test-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-newrelic",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -31,8 +31,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-proxy-backend",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -19,8 +19,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.21",
"@backstage/config": "^0.1.1-alpha.21",
"@backstage/backend-common": "^0.1.1-alpha.22",
"@backstage/config": "^0.1.1-alpha.22",
"@types/express": "^4.17.6",
"@types/http-proxy-middleware": "^0.19.3",
"express": "^4.17.1",
@@ -35,7 +35,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
"@types/uuid": "^8.0.0",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-register-component",
"version": "0.1.1-alpha.21",
"version": "0.1.1-alpha.22",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.22",
"@backstage/core": "^0.1.1-alpha.22",
"@backstage/plugin-catalog": "^0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.21",
"@backstage/dev-utils": "^0.1.1-alpha.21",
"@backstage/cli": "^0.1.1-alpha.22",
"@backstage/dev-utils": "^0.1.1-alpha.22",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",

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