Merge branch 'master' of github.com:spotify/backstage into mob/catalog-in-gql
* 'master' of github.com:spotify/backstage: (73 commits) chore(gql): graphql should be public chore(deps): bump chokidar from 3.4.0 to 3.4.1 (#1818) chore(deps-dev): bump @types/nodegit from 0.26.5 to 0.26.7 (#1817) chore(deps-dev): bump @testing-library/react-hooks from 3.3.0 to 3.4.1 (#1820) Mob/techdocs end to end (#1736) chore(scaffolder): updating the wording for the sample templates chore(deps): bump react-use from 14.2.0 to 15.3.3 workflows: run cli checks on changes to create-app create-app: sync version create-app: fix deps create-app: add lint command feat(create-app): add missing template backend package plugins/graphql: fix schema packaging create-app: packaging fixes fix(create-app): update entrypoint feat: fallback to string value if config value is not json yarn.lock: security fix (#1797) fix(cli): add create-app import to e2e tests cli: just print warning if postpack fails broken link to create an app ...
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- '.github/workflows/cli-win.yml'
|
||||
- 'packages/cli/**'
|
||||
- 'packages/create-app/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
paths:
|
||||
- '.github/workflows/cli.yml'
|
||||
- 'packages/cli/**'
|
||||
- 'packages/create-app/**'
|
||||
- 'packages/core/**'
|
||||
- 'packages/core-api/**'
|
||||
- 'yarn.lock'
|
||||
@@ -13,6 +14,18 @@ jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432/tcp
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
@@ -44,6 +57,11 @@ jobs:
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation
|
||||
env:
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
run: |
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
| Organization | Contact | Description of Use |
|
||||
| ---------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D
|
||||
| Organization | Contact | Description of Use |
|
||||
| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
|
||||
|
||||
@@ -64,6 +64,14 @@ So...feel ready to jump in? Let's do this. Head over to the [Getting Started gui
|
||||
|
||||
If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
|
||||
|
||||
# Coding Guidelines
|
||||
|
||||
All code is formatted with `prettier` using the configuration in the repo. If possible we recommend configuring your editor to format automatically, but you can also use the `yarn prettier --write <file>` command to format files.
|
||||
|
||||
If you're contributing to the backend or CLI tooling, be mindful of cross-platform support. [This](https://shapeshed.com/writing-cross-platform-node/) blog post is a good guide of what to keep in mind when writing cross-platform NodeJS.
|
||||
|
||||
Also be sure to skim through our [ADRs](https://github.com/spotify/backstage/tree/master/docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](https://github.com/spotify/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for.
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
|
||||
|
||||
@@ -56,7 +56,7 @@ Our vision for Backstage is for it to become the trusted standard toolbox (read:
|
||||
|
||||
The Backstage platform consists of a number of different components:
|
||||
|
||||
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md).
|
||||
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/getting-started/create-an-app.md).
|
||||
- [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_.
|
||||
- [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.
|
||||
- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins.
|
||||
|
||||
@@ -21,7 +21,7 @@ organization:
|
||||
name: Spotify
|
||||
|
||||
techdocs:
|
||||
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
|
||||
storageUrl: http://localhost:7000/techdocs/static/docs
|
||||
|
||||
sentry:
|
||||
organization: spotify
|
||||
@@ -48,6 +48,9 @@ auth:
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_CLIENT_SECRET
|
||||
enterpriseInstanceUrl:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
|
||||
gitlab:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
|
||||
@@ -16,7 +16,7 @@ function inject_config() {
|
||||
with_entries(select(.key | startswith("APP_CONFIG_")) | .key |= sub("APP_CONFIG_"; "")) |
|
||||
to_entries |
|
||||
reduce .[] as $item (
|
||||
{}; setpath($item.key | split("_"); $item.value | fromjson)
|
||||
{}; setpath($item.key | split("_"); $item.value | try fromjson catch $item.value)
|
||||
)')"
|
||||
|
||||
>&2 echo "Runtime app config: $config"
|
||||
|
||||
@@ -94,6 +94,7 @@ better yet, a pull request.
|
||||
- [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md)
|
||||
- [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md)
|
||||
- [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md)
|
||||
- [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md)
|
||||
- [Contribute](../CONTRIBUTING.md)
|
||||
- [Support](overview/support.md)
|
||||
- [FAQ](FAQ.md)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# ADR007: Use MSW to mock http requests
|
||||
|
||||
## Context
|
||||
|
||||
Network request mocking can be a total pain sometimes, in all different types of
|
||||
tests, unit tests to e2e tests always have their own implementation of mocking
|
||||
these requests. There's been traction in the outer community towards using this
|
||||
library to mock network requests by using an express style declaration for
|
||||
routes. react-testing-library suggests using this library instead of mocking
|
||||
fetch directly wether this be in a browser or in node.
|
||||
|
||||
https://github.com/mswjs/msw
|
||||
|
||||
## Decision
|
||||
|
||||
Moving forward, we have decided that any `fetch` or `XMLHTTPRequest` that
|
||||
happens, should be mocked by using `msw`.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```ts
|
||||
import { setupWorker, rest } from 'msw';
|
||||
|
||||
const worker = setupWorker(
|
||||
rest.get('*/user/:userId', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json({
|
||||
firstName: 'John',
|
||||
lastName: 'Maverick',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Start the Mock Service Worker
|
||||
worker.start();
|
||||
```
|
||||
|
||||
and in a more real life scenario, taken from
|
||||
[CatalogClient.test.ts](https://github.com/spotify/backstage/blob/f3245c4f8f0b6b2625c4a6d5d50161b612fb4757/plugins/catalog/src/api/CatalogClient.test.ts)
|
||||
|
||||
```ts
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const entities = await client.getEntities();
|
||||
expect(entities).toEqual(defaultResponse);
|
||||
});
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
- A little more code to write
|
||||
- Gradually will replace the codebase with `msw`
|
||||
@@ -1,20 +1,23 @@
|
||||
# TechDocs FAQ
|
||||
|
||||
This page answer frequently asked questions about [TechDocs].
|
||||
This page answers frequently asked questions about [TechDocs](README.md).
|
||||
|
||||
#### Technology
|
||||
_Got a question that you think others might be interested in knowing the answer to? Edit this file
|
||||
[here](https://github.com/spotify/backstage/edit/master/docs/features/techdocs/FAQ.md)._
|
||||
|
||||
## Technology
|
||||
|
||||
- [What static site generator is TechDocs using?](./#what-static-site-generator-is-techdocs-using)
|
||||
- [What is the mkdocs-techdocs-core plugin?](./#what-is-the-mkdocs-techdocs-core-plugin)
|
||||
|
||||
## What static site generator is TechDocs using?
|
||||
#### What static site generator is TechDocs using?
|
||||
|
||||
TechDocs is using [MkDocs](https://www.mkdocs.org/) to build project
|
||||
doucmentation under the hood. Documentation built with the
|
||||
documentation under the hood. Documentation built with the
|
||||
[techdocs-container](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/README.md)
|
||||
is using the MkDocs Material Theme.
|
||||
|
||||
## What is the mkdocs-techdocs-core plugin?
|
||||
#### What is the mkdocs-techdocs-core plugin?
|
||||
|
||||
The
|
||||
[mkdocs-techdocs-core](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/techdocs-core/README.md)
|
||||
@@ -23,7 +26,3 @@ plugins (e.g.
|
||||
[MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)) as
|
||||
well as a selection of Python Markdown extensions that TechDocs supports.
|
||||
|
||||
_Add a question that you think others might be interested in? Edit the file
|
||||
[here](https://github.com/spotify/backstage/edit/master/docs/features/techdocs/FAQ.md)._
|
||||
|
||||
[techdocs]: README.md
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Concepts
|
||||
|
||||
This page describes concepts that has been introduced with Spotify's
|
||||
docs-like-code solution in Backstage.
|
||||
This page describes concepts that are introduced with Spotify's docs-like-code solution in Backstage.
|
||||
|
||||
### TechDocs Core Plugin
|
||||
|
||||
@@ -20,34 +19,34 @@ MkDocs.
|
||||
|
||||
[TechDocs Container](../../../packages/techdocs-container/README.md)
|
||||
|
||||
### TechDocs publisher (Coming Soon)
|
||||
### TechDocs publisher (coming soon)
|
||||
|
||||
### TechDocs CLI
|
||||
|
||||
The TechDocs CLI was created to make it easy to write, generate and preview
|
||||
documentation for publishing. Currently it mostly acts as a wrapper around the
|
||||
TechDocs container and provides a easy to use interface for our docker
|
||||
TechDocs container and provides an easy-to-use interface for our docker
|
||||
container.
|
||||
|
||||
[TechDocs CLI](../../../packages/techdocs-cli/README.md)
|
||||
|
||||
### TechDocs Reader
|
||||
|
||||
Documentation generated by TechDocs is generated as static html sites. The
|
||||
TechDocs Reader was therefore created to be able to integrate pre-generated html
|
||||
Documentation generated by TechDocs is generated as static HTML sites. The
|
||||
TechDocs Reader was therefore created to be able to integrate pre-generated HTML
|
||||
sites with the Backstage UI.
|
||||
|
||||
The TechDocs Reader purpose is also to open up the opportunity to integrate
|
||||
TechDocs widgets for a customized full-featured TechDocs experience.
|
||||
([Coming Soon V.2](https://github.com/spotify/backstage/milestone/17))
|
||||
([coming soon V.2](https://github.com/spotify/backstage/milestone/17))
|
||||
|
||||
[TechDocs Reader](../../../plugins/techdocs/src/reader/README.md)
|
||||
|
||||
### Transformers
|
||||
|
||||
Transformers is different pieces of functionality used inside the TechDocs
|
||||
Reader. The reason to why transformers were introduced is to provide a way to
|
||||
transform the html content on pre and post render. (e.g. rewrite docs links or
|
||||
modify css)
|
||||
Transformers are different pieces of functionality used inside the TechDocs
|
||||
Reader. The reason why transformers were introduced was to provide a way to
|
||||
transform the HTML content on pre and post render (e.g. rewrite docs links or
|
||||
modify css).
|
||||
|
||||
[Transformers API docs](../../../plugins/techdocs/src/reader/transformers/README.md)
|
||||
|
||||
@@ -5,22 +5,18 @@ This section will guide you through:
|
||||
- Creating a basic setup for your documentation
|
||||
- Writing and previewing your documentation in a local Backstage environment
|
||||
- Creating a build ready for publication
|
||||
- Publishing your documentation and making your Backstage instance read from
|
||||
your published docs.
|
||||
- Publishing your documentation and making your Backstage instance read your published docs.
|
||||
|
||||
## Prerequisities
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- Static file hosting
|
||||
- A working Backstage instance with TechDocs installed
|
||||
[TechDocs getting started](getting-started.md)
|
||||
(see [TechDocs getting started](getting-started.md))
|
||||
|
||||
## Create a basic documentation setup
|
||||
|
||||
Create a directory that contains your documentation. Inside this directory you
|
||||
should create a file called `mkdocs.yml`. As an example you can create a
|
||||
directory called `hello-docs` in your home directory (also known as `~`). Below
|
||||
is a basic example of how it could look.
|
||||
In your home directory (also known as `~`), create a directory that contains your documentation (for example, `hello-docs`). Inside this directory, create a file called `mkdocs.yml`. Below is a basic example of how it could look.
|
||||
|
||||
The `~/hello-docs/mkdocs.yml` file should have the following content:
|
||||
|
||||
@@ -34,7 +30,7 @@ plugins:
|
||||
- techdocs-core
|
||||
```
|
||||
|
||||
And then the `~/hello-docs/docs/index.md` should have the following content:
|
||||
The `~/hello-docs/docs/index.md` should have the following content:
|
||||
|
||||
```md
|
||||
# example docs
|
||||
@@ -58,7 +54,7 @@ npx techdocs-cli serve
|
||||
## Build production ready documentation
|
||||
|
||||
To get a build suitable for publication you can build your docs using the
|
||||
`spotify/techdocs` container.
|
||||
`spotify/techdocs` container:
|
||||
|
||||
```bash
|
||||
cd ~/hello-docs/
|
||||
@@ -69,20 +65,15 @@ You should now have a folder called `~/hello-docs/site/`.
|
||||
|
||||
## Deploy to a file server
|
||||
|
||||
In order to serve documentation to TechDocs, our Backstage plugin needs to
|
||||
download the HTML rendered from the
|
||||
[Create documentation](#create-documentation) step above. This will likely exist
|
||||
on an external file server, or a storage solution such as Google Cloud Storage.
|
||||
In order to serve documentation to TechDocs, our Backstage plugin needs to download the HTML rendered from the previous step. This will likely exist on an external file server, or a storage solution such as Google Cloud Storage.
|
||||
|
||||
When deploying documentation, it should be deployed on that file server /
|
||||
storage solution with the following convention: `{id}/{file}`. For example, if
|
||||
we wanted to upload the `getting-started/index.html` file for the `backstage`
|
||||
When deploying documentation, it should be deployed on that file server/storage solution with the following convention: `{id}/{file}`. For example, if
|
||||
you want to upload the `getting-started/index.html` file for the `backstage`
|
||||
documentation site, we would upload it to our file server as
|
||||
`backstage/getting-started/index.html`.
|
||||
|
||||
To explain further how this would look like for multiple documentation sites,
|
||||
take a look at this example file tree that would be represented on your file
|
||||
server:
|
||||
To explain further what this would look like for multiple documentation sites,
|
||||
take a look at this example file tree that would be represented on your file server:
|
||||
|
||||
```md
|
||||
/backstage/index.html /backstage/getting-started/index.html
|
||||
@@ -96,7 +87,7 @@ In this file tree, we have two documentation sites available: `backstage` and
|
||||
on `http://example.com` as the server URL.
|
||||
|
||||
When you configure the TechDocs plugin in Backstage to use `http://example.com`
|
||||
as the file server / storage solution, it will translate the following URLs to
|
||||
as the file server/storage solution, it will translate the following URLs to
|
||||
the file server:
|
||||
|
||||
| Backstage URL | File Server URL |
|
||||
@@ -104,10 +95,8 @@ the file server:
|
||||
| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html |
|
||||
| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html |
|
||||
|
||||
Then deploying new sites is easy. It's as simple as copying over the `site/`
|
||||
folder produced in the [Create documentation](#create-documentation) step above
|
||||
and copying it over to the file server / storage solution under the ID of the
|
||||
documentation site. It will then become immediately available in Backstage under
|
||||
Then deploying new sites is easy: simply copy over the `site/`
|
||||
folder produced in the [Create documentation](#build-production-ready-documentation) step above to the file server/storage solution under the ID of the documentation site. It will then become immediately available in Backstage under
|
||||
the same ID as you can see in the table above.
|
||||
|
||||
So, if the URL to your file server is `http://example.com/`, your
|
||||
@@ -120,7 +109,7 @@ In order for Backstage to show your documentation, it needs to know where you
|
||||
uploaded it.
|
||||
|
||||
Make sure you have Backstage set up using
|
||||
[TechDocs getting started](getting-started.md)
|
||||
[TechDocs getting started](getting-started.md).
|
||||
|
||||
To point Backstage to your docs storage, add or change the following lines in
|
||||
your Backstage `app-config.yaml`:
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# Getting Started
|
||||
|
||||
> TechDocs is not feature complete and currently you can't set up a complete
|
||||
> TechDocs is not yet feature complete - currently you can't set up a complete
|
||||
> end-to-end working TechDocs plugin without customizing the plugin itself.
|
||||
|
||||
> With TechDocs V.0 you can expect a demonstration of how to integrate docs into
|
||||
> Backstage. Currently it can create docs using
|
||||
> [mkdocs](https://www.mkdocs.org/), as well as reading published docs. If you
|
||||
> publish generated docs and passing in a storageUrl in your `app-config.yaml`
|
||||
> you can view it in Backstage by going to
|
||||
> What you can expect from TechDocs V.0 is a demonstration of how to integrate docs into
|
||||
> Backstage. TechDocs can create docs using
|
||||
> [mkdocs](https://www.mkdocs.org/), as well as read published docs. If you
|
||||
> publish generated docs and pass in a `storageUrl` in your `app-config.yaml`,
|
||||
> you can view them in Backstage by going to
|
||||
> `http://localhost:3000/docs/<remote-folder>`.
|
||||
|
||||
Getting started with TechDocs is easy. TechDocs functions as a plugin to
|
||||
Backstage, why you will need to use Backstage to use TechDocs.
|
||||
TechDocs functions as a plugin to
|
||||
Backstage, so you will need to use Backstage to use TechDocs.
|
||||
|
||||
## What is Backstage?
|
||||
|
||||
Backstage is an open platform for building developer portals. It’s based on the
|
||||
developer portal we’ve been using internally at Spotify for over four years.
|
||||
[Read more](https://github.com/spotify/backstage).
|
||||
[Read more here](https://github.com/spotify/backstage).
|
||||
|
||||
## Prerequisities
|
||||
|
||||
In order to use Backstage and TechDocs, you will need to have the following
|
||||
In order to use Backstage and TechDocs, you need to have the following
|
||||
installed:
|
||||
|
||||
- [Node.js](https://nodejs.org) Active LTS (long term support), currently v12
|
||||
@@ -29,48 +29,41 @@ installed:
|
||||
|
||||
## Creating a new Backstage app
|
||||
|
||||
> If you have already created a Backstage application for this purpose, jump to
|
||||
> If you have already created a Backstage application, jump to
|
||||
> [Installing TechDocs](#installing-techdocs), otherwise complete this step.
|
||||
|
||||
To create a new Backstage application for us to set up TechDocs, you will need
|
||||
to run the following command:
|
||||
To create a new Backstage application for TechDocs, run the following command:
|
||||
|
||||
```bash
|
||||
npx @backstage/cli create-app
|
||||
```
|
||||
|
||||
You will then be prompted to enter a name for your application. Once you do so,
|
||||
this will create a new Backstage application for you in a new folder. For
|
||||
example, if we chose the name `hello-world` for our application, it would create
|
||||
a new `hello-world` folder containing our new Backstage application.
|
||||
You will then be prompted to enter a name for your application. Once that's done, a new Backstage application will be created in a new folder. For
|
||||
example, if you choose the name `hello-world`, a new `hello-world` folder is created containing your new Backstage application.
|
||||
|
||||
## Installing TechDocs
|
||||
|
||||
Inside of our new Backstage application, TechDocs is not provided by default.
|
||||
For this reason we will need to manually set up TechDocs. It should take less
|
||||
TechDocs is not provided with the Backstage application by default, so you will now need to set up TechDocs manually. It should take less
|
||||
than a minute.
|
||||
|
||||
### Adding the package
|
||||
|
||||
We will need to add our plugin to your Backstage application. To do so, you can
|
||||
navigate to your new Backstage application folder and then run a single command
|
||||
to install TechDocs.
|
||||
The first step is to add the TechDocs plugin to your Backstage application. Navigate to your new Backstage application folder:
|
||||
|
||||
```bash
|
||||
cd hello-world/
|
||||
```
|
||||
|
||||
Then you need to navigate to your `packages/app` folder to install TechDocs:
|
||||
Then navigate to your `packages/app` folder to install TechDocs:
|
||||
|
||||
```bash
|
||||
cd packages/app
|
||||
yarn add @backstage/plugin-techdocs
|
||||
```
|
||||
|
||||
After a short while, it should successfully install the TechDocs plugin. Now we
|
||||
just need to set up some basic configuration!
|
||||
After a short while, the TechDocs plugin should be successfully installed.
|
||||
|
||||
Enter the following command:
|
||||
Next, you need to set up some basic configuration. Enter the following command:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
@@ -85,7 +78,7 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs';
|
||||
### Setting the configuration
|
||||
|
||||
TechDocs allows for configuration of the docs storage URL through your
|
||||
app-config file. The URL provided here is demo docs used to testing.
|
||||
`app-config` file. The URL provided here is for demo docs to use for testing purposes.
|
||||
|
||||
To use the demo docs, add the following lines to `app-config.yaml`:
|
||||
|
||||
@@ -94,16 +87,17 @@ techdocs:
|
||||
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
|
||||
```
|
||||
|
||||
## Run Backstage Locally
|
||||
## Run Backstage locally
|
||||
|
||||
Change folder to your Backstage application root.
|
||||
Change folder to your Backstage application root and run the following command:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
Open browser at [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/).
|
||||
|
||||
## Extra Reading
|
||||
## Additional reading
|
||||
|
||||
[Back to Docs](README.md)
|
||||
* [Creating and publishing your docs](creating-and-publishing.md)
|
||||
* [Back to README](README.md)
|
||||
|
||||
@@ -15,7 +15,7 @@ To create a Backstage app, you will need to have
|
||||
With `npx`:
|
||||
|
||||
```bash
|
||||
npx @backstage/cli create-app
|
||||
npx @backstage/create-app
|
||||
```
|
||||
|
||||
This will create a new Backstage App inside the current folder. The name of the
|
||||
|
||||
@@ -91,6 +91,7 @@ nav:
|
||||
- ADR004 - Module Export Structure: 'architecture-decisions/adr004-module-export-structure.md'
|
||||
- ADR005 - Catalog Core Entities: 'architecture-decisions/adr005-catalog-core-entities.md'
|
||||
- ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md'
|
||||
- ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md'
|
||||
- Contribute: '../CONTRIBUTING.md'
|
||||
- Support: 'overview/support.md'
|
||||
- FAQ: FAQ.md
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently 'yarn start' 'yarn start-backend'",
|
||||
"start": "yarn workspace example-app start",
|
||||
"start-backend": "yarn workspace example-backend start",
|
||||
"build": "lerna run build",
|
||||
"tsc": "tsc",
|
||||
"clean": "backstage-cli clean && lerna run clean",
|
||||
@@ -35,6 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@spotify/eslint-config-oss": "^1.0.1",
|
||||
"@spotify/prettier-config": "^8.0.0",
|
||||
"concurrently": "^5.2.0",
|
||||
"husky": "^4.2.3",
|
||||
"lerna": "^3.20.2",
|
||||
"lint-staged": "^10.1.0",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0",
|
||||
"react-use": "^15.3.3",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,7 +39,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM node:12
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# This will copy the contents of the dist-workspace when running the build-image command.
|
||||
# Do not use this Dockerfile outside of that command, as it will copy in the source code instead.
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
"pg": "^8.3.0",
|
||||
"sqlite3": "^4.2.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from '@backstage/config-loader';
|
||||
import knex from 'knex';
|
||||
import knex, { PgConnectionConfig } from 'knex';
|
||||
import healthcheck from './plugins/healthcheck';
|
||||
import auth from './plugins/auth';
|
||||
import catalog from './plugins/catalog';
|
||||
@@ -47,11 +47,36 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
const database = knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
// Supported DBs are sqlite and postgres
|
||||
const isPg = [
|
||||
'POSTGRES_USER',
|
||||
'POSTGRES_HOST',
|
||||
'POSTGRES_PASSWORD',
|
||||
].every(key => config.getOptional(`backend.${key}`));
|
||||
|
||||
let knexConfig;
|
||||
|
||||
if (isPg) {
|
||||
knexConfig = {
|
||||
client: 'pg',
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
port: config.getOptionalNumber('backend.POSTGRES_PORT'),
|
||||
host: config.getString('backend.POSTGRES_HOST'),
|
||||
user: config.getString('backend.POSTGRES_USER'),
|
||||
password: config.getString('backend.POSTGRES_PASSWORD'),
|
||||
database: `backstage_plugin_${plugin}`,
|
||||
} as PgConnectionConfig,
|
||||
};
|
||||
} else {
|
||||
knexConfig = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
}
|
||||
|
||||
const database = knex(knexConfig);
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRouter } from '@backstage/plugin-techdocs-backend';
|
||||
import {
|
||||
createRouter,
|
||||
DirectoryPreparer,
|
||||
Preparers,
|
||||
Generators,
|
||||
LocalPublish,
|
||||
TechdocsGenerator
|
||||
} from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
export default async function createPlugin({ logger, config }: PluginEnvironment) {
|
||||
const generators = new Generators();
|
||||
const techdocsGenerator = new TechdocsGenerator();
|
||||
generators.register('techdocs', techdocsGenerator);
|
||||
|
||||
const directoryPreparer = new DirectoryPreparer();
|
||||
const preparers = new Preparers();
|
||||
|
||||
preparers.register('dir', directoryPreparer);
|
||||
|
||||
const publisher = new LocalPublish();
|
||||
|
||||
const dockerClient = new Docker();
|
||||
|
||||
return await createRouter({
|
||||
preparers,
|
||||
generators,
|
||||
publisher,
|
||||
dockerClient,
|
||||
logger,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.16",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"yaml": "^1.9.2"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const fetch = require('node-fetch');
|
||||
const killTree = require('tree-kill');
|
||||
const { resolve: resolvePath, join: joinPath } = require('path');
|
||||
const Browser = require('zombie');
|
||||
@@ -28,6 +29,7 @@ const {
|
||||
waitForExit,
|
||||
print,
|
||||
} = require('./helpers');
|
||||
const pgtools = require('pgtools');
|
||||
|
||||
async function main() {
|
||||
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
|
||||
@@ -36,8 +38,9 @@ async function main() {
|
||||
print('Building dist workspace');
|
||||
const workspaceDir = await buildDistWorkspace('workspace', rootDir);
|
||||
|
||||
const isPostgres = Boolean(process.env.POSTGRES_USER);
|
||||
print('Creating a Backstage App');
|
||||
const appDir = await createApp('test-app', workspaceDir, rootDir);
|
||||
const appDir = await createApp('test-app', isPostgres, workspaceDir, rootDir);
|
||||
|
||||
print('Creating a Backstage Plugin');
|
||||
const pluginName = await createPlugin('test-plugin', appDir);
|
||||
@@ -45,6 +48,9 @@ async function main() {
|
||||
print('Starting the app');
|
||||
await testAppServe(pluginName, appDir);
|
||||
|
||||
print('Testing the backend startup');
|
||||
await testBackendStart(appDir, isPostgres);
|
||||
|
||||
print('All tests successful, removing test dir');
|
||||
await fs.remove(rootDir);
|
||||
}
|
||||
@@ -63,6 +69,7 @@ async function buildDistWorkspace(workspaceName, rootDir) {
|
||||
'build-workspace',
|
||||
workspaceDir,
|
||||
'@backstage/cli',
|
||||
'@backstage/create-app',
|
||||
'@backstage/core',
|
||||
'@backstage/dev-utils',
|
||||
'@backstage/test-utils',
|
||||
@@ -97,12 +104,11 @@ async function pinYarnVersion(dir) {
|
||||
/**
|
||||
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
|
||||
*/
|
||||
async function createApp(appName, workspaceDir, rootDir) {
|
||||
async function createApp(appName, isPostgres, workspaceDir, rootDir) {
|
||||
const child = spawnPiped(
|
||||
[
|
||||
'node',
|
||||
resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'),
|
||||
'create-app',
|
||||
resolvePath(workspaceDir, 'packages/create-app/bin/backstage-create-app'),
|
||||
'--skip-install',
|
||||
],
|
||||
{
|
||||
@@ -119,6 +125,14 @@ async function createApp(appName, workspaceDir, rootDir) {
|
||||
await waitFor(() => stdout.includes('Enter a name for the app'));
|
||||
child.stdin.write(`${appName}\n`);
|
||||
|
||||
await waitFor(() => stdout.includes('Select database for the backend'));
|
||||
|
||||
if (!isPostgres) {
|
||||
// Simulate down arrow press
|
||||
child.stdin.write(`\u001B\u005B\u0042`);
|
||||
}
|
||||
child.stdin.write(`\n`);
|
||||
|
||||
print('Waiting for app create script to be done');
|
||||
await waitForExit(child);
|
||||
|
||||
@@ -201,7 +215,7 @@ async function createPlugin(pluginName, appDir) {
|
||||
await waitForExit(child);
|
||||
|
||||
const pluginDir = resolvePath(appDir, 'plugins', pluginName);
|
||||
for (const cmd of [['lint'], ['test', '--no-watch']]) {
|
||||
for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) {
|
||||
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
|
||||
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
|
||||
}
|
||||
@@ -250,5 +264,87 @@ async function testAppServe(pluginName, appDir) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates PG databases (drops if exists before) */
|
||||
async function createDB(database) {
|
||||
const config = {
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: process.env.POSTGRES_PORT,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
};
|
||||
|
||||
try {
|
||||
await pgtools.dropdb({ config }, database);
|
||||
} catch (_) {
|
||||
/* do nothing*/
|
||||
}
|
||||
return pgtools.createdb(config, database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start serving the newly created backend and make sure that all db migrations works correctly
|
||||
*/
|
||||
async function testBackendStart(appDir, isPostgres) {
|
||||
if (isPostgres) {
|
||||
print('Creating DBs');
|
||||
await Promise.all(
|
||||
[
|
||||
'catalog',
|
||||
'scaffolder',
|
||||
'auth',
|
||||
'identity',
|
||||
'proxy',
|
||||
'techdocs',
|
||||
].map(name => createDB(`backstage_plugin_${name}`)),
|
||||
);
|
||||
print('Created DBs');
|
||||
}
|
||||
|
||||
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], {
|
||||
cwd: appDir,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
child.stderr.on('data', data => {
|
||||
stderr = stderr + data.toString('utf8');
|
||||
});
|
||||
let successful = false;
|
||||
|
||||
try {
|
||||
await waitFor(() => stdout.includes('Listening on ') || stderr !== '');
|
||||
if (stderr !== '') {
|
||||
// Skipping the whole block
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
print('Try to fetch entities from the backend');
|
||||
// Try fetch entities, should be ok
|
||||
await fetch('http://localhost:7000/catalog/entities').then(res =>
|
||||
res.json(),
|
||||
);
|
||||
print('Entities fetched successfully');
|
||||
successful = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Backend failed to startup: ${error}`);
|
||||
} finally {
|
||||
print('Stopping the child process');
|
||||
// Kill entire process group, otherwise we'll end up with hanging serve processes
|
||||
killTree(child.pid);
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForExit(child);
|
||||
} catch (error) {
|
||||
if (!successful) {
|
||||
throw new Error(`Backend failed to startup: ${stderr}`);
|
||||
}
|
||||
print('Backend startup test finished successfully');
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', handleError);
|
||||
main(process.argv.slice(2)).catch(handleError);
|
||||
|
||||
@@ -76,6 +76,12 @@ function handleError(err) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for fn() to be true
|
||||
* Checks every 100ms
|
||||
* .cancel() is available
|
||||
* @returns {Promise} Promise of resolution
|
||||
*/
|
||||
function waitFor(fn) {
|
||||
return new Promise(resolve => {
|
||||
const handle = setInterval(() => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@sucrase/webpack-loader": "^2.0.0",
|
||||
"@svgr/plugin-jsx": "4.3.x",
|
||||
"@svgr/plugin-svgo": "4.3.x",
|
||||
"@svgr/rollup": "4.3.x",
|
||||
"@svgr/rollup": "5.4.x",
|
||||
"@svgr/webpack": "4.3.x",
|
||||
"@types/start-server-webpack-plugin": "^2.2.0",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
@@ -68,14 +68,16 @@
|
||||
"jest-css-modules": "^2.1.0",
|
||||
"jest-esm-transformer": "^1.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"ora": "^4.0.3",
|
||||
"pgtools": "^0.3.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"rollup": "2.10.x",
|
||||
"rollup": "2.23.x",
|
||||
"rollup-plugin-dts": "^1.4.6",
|
||||
"rollup-plugin-esbuild": "^2.0.0",
|
||||
"rollup-plugin-image-files": "^1.4.2",
|
||||
|
||||
@@ -39,5 +39,12 @@ export const pre = async () => {
|
||||
|
||||
export const post = async () => {
|
||||
// postpack isn't called by yarn right now, so it needs to be called manually
|
||||
await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true });
|
||||
try {
|
||||
await fs.move(PKG_BACKUP_PATH, PKG_PATH, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to restore package.json during postpack, ${error}. ` +
|
||||
'Your package will be fine but you may have ended up with some garbage in the repo.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,17 +22,6 @@ import { version } from './lib/version';
|
||||
const main = (argv: string[]) => {
|
||||
program.name('backstage-cli').version(version);
|
||||
|
||||
program
|
||||
.command('create-app')
|
||||
.description('Creates a new app in a new directory')
|
||||
.option(
|
||||
'--skip-install',
|
||||
'Skip the install and builds steps after creating the app',
|
||||
)
|
||||
.action(
|
||||
lazyAction(() => import('./commands/create-app/createApp'), 'default'),
|
||||
);
|
||||
|
||||
program
|
||||
.command('app:build')
|
||||
.description('Build an app for a production release')
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function runCheck(cmd: string, ...args: string[]) {
|
||||
}
|
||||
|
||||
export async function waitForExit(
|
||||
child: ChildProcess & { exitCode?: number },
|
||||
child: ChildProcess & { exitCode: number | null },
|
||||
name?: string,
|
||||
): Promise<void> {
|
||||
if (typeof child.exitCode === 'number') {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
app:
|
||||
title: Scaffolded Backstage App
|
||||
baseUrl: http://localhost:3000
|
||||
|
||||
organization:
|
||||
name: Acme Corporation
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import WelcomePage from './components/WelcomePage';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'welcome',
|
||||
register({ router }) {
|
||||
router.registerRoute('/', WelcomePage);
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"],
|
||||
"exclude": ["**/node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^14.2.0"
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
@@ -36,7 +36,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/yup": "^0.28.2"
|
||||
},
|
||||
|
||||
@@ -40,14 +40,15 @@ describe('readEnv', () => {
|
||||
APP_CONFIG_numbers_a: '1',
|
||||
APP_CONFIG_numbers_b: '2',
|
||||
APP_CONFIG_numbers_c: 'false',
|
||||
APP_CONFIG_numbers_d: undefined,
|
||||
APP_CONFIG_numbers_d: 'abc',
|
||||
APP_CONFIG_numbers_e: undefined,
|
||||
APP_CONFIG_very_deep_nested_config_object: '{}',
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
data: {
|
||||
foo: 'bar',
|
||||
numbers: { a: 1, b: 2, c: false },
|
||||
numbers: { a: 1, b: 2, c: false, d: 'abc' },
|
||||
very: { deep: { nested: { config: { object: {} } } } },
|
||||
},
|
||||
context: 'env',
|
||||
@@ -55,6 +56,37 @@ describe('readEnv', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should accept string values', () => {
|
||||
expect(readEnv({ APP_CONFIG_foo: '"abc"', APP_CONFIG_bar: 'xyz' })).toEqual(
|
||||
[
|
||||
{
|
||||
data: {
|
||||
foo: 'abc',
|
||||
bar: 'xyz',
|
||||
},
|
||||
context: 'env',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept complex objects', () => {
|
||||
expect(
|
||||
readEnv({
|
||||
APP_CONFIG_foo: '{ "a": 123, "b": "123", "c": [] }',
|
||||
APP_CONFIG_bar: '[123, "abc", {}]',
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
data: {
|
||||
foo: { a: 123, b: '123', c: [] },
|
||||
bar: [123, 'abc', {}],
|
||||
},
|
||||
context: 'env',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['APP_CONFIG__foo'],
|
||||
['APP_CONFIG_foo_'],
|
||||
@@ -68,12 +100,17 @@ describe('readEnv', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([['hello'], ['"hello'], ['{'], ['}'], ['123abc']])(
|
||||
'should reject invalid value %p',
|
||||
it.each([['hello'], ['"hello'], ['{'], ['}']])(
|
||||
'should fallback to string when invalid json value %p',
|
||||
value => {
|
||||
expect(() => readEnv({ APP_CONFIG_foo: value })).toThrow(
|
||||
/^Failed to parse JSON-serialized config value for key 'foo', SyntaxError: /,
|
||||
);
|
||||
expect(readEnv({ APP_CONFIG_foo: value })).toEqual([
|
||||
{
|
||||
data: {
|
||||
foo: value,
|
||||
},
|
||||
context: 'env',
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export function readEnv(env: {
|
||||
);
|
||||
}
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
const [, parsedValue] = safeJsonParse(value);
|
||||
if (parsedValue === null) {
|
||||
throw new Error('value may not be null');
|
||||
}
|
||||
@@ -89,3 +89,11 @@ export function readEnv(env: {
|
||||
|
||||
return data ? [{ data, context: 'env' }] : [];
|
||||
}
|
||||
|
||||
function safeJsonParse(str: string): [Error | null, any] {
|
||||
try {
|
||||
return [null, JSON.parse(str)];
|
||||
} catch (err) {
|
||||
return [err, str];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"lodash": "^4.17.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0",
|
||||
"react-use": "^15.3.3",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -46,7 +46,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('RefreshingAuthSessionManager', () => {
|
||||
expect(refreshSession).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should forward option to instantly show auth popup', async () => {
|
||||
it('should forward option to instantly show auth popup and not attempt refresh', async () => {
|
||||
const createSession = jest.fn();
|
||||
const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE'));
|
||||
const manager = new RefreshingAuthSessionManager({
|
||||
@@ -151,7 +151,7 @@ describe('RefreshingAuthSessionManager', () => {
|
||||
scopes: new Set(),
|
||||
instantPopup: true,
|
||||
});
|
||||
expect(refreshSession).toBeCalledTimes(1);
|
||||
expect(refreshSession).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should remove session straight away', async () => {
|
||||
|
||||
@@ -90,7 +90,12 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
|
||||
// The user may still have a valid refresh token in their cookies. Attempt to
|
||||
// initiate a fresh session through the backend using that refresh token.
|
||||
if (!this.currentSession) {
|
||||
//
|
||||
// We skip this check if an instant login popup is requested, as we need to
|
||||
// stay in a synchronous call stack from the user interaction. The downside
|
||||
// is that that the user will sometimes be requested to log in even if they
|
||||
// already had an existing session.
|
||||
if (!this.currentSession && !options.instantPopup) {
|
||||
try {
|
||||
const newSession = await this.collapsedSessionRefresh();
|
||||
this.currentSession = newSession;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-syntax-highlighter": "^12.2.1",
|
||||
"react-use": "^14.2.0"
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.16",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useNavigate } from 'react-router';
|
||||
interface IErrorPageProps {
|
||||
status: string;
|
||||
statusMessage: string;
|
||||
additionalInfo?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
@@ -38,7 +39,11 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
|
||||
export const ErrorPage = ({
|
||||
status,
|
||||
statusMessage,
|
||||
additionalInfo,
|
||||
}: IErrorPageProps) => {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -49,6 +54,9 @@ export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
|
||||
<Typography variant="body1" className={classes.subtitle}>
|
||||
ERROR {status}: {statusMessage}
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.subtitle}>
|
||||
{additionalInfo}
|
||||
</Typography>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Looks like someone dropped the mic!
|
||||
</Typography>
|
||||
|
||||
@@ -85,6 +85,14 @@ export const App = () => (
|
||||
</Page>
|
||||
);
|
||||
|
||||
export const Documentation = () => (
|
||||
<Page theme={pageTheme.documentation}>
|
||||
<Header title="Documentation component page" type="documentation">
|
||||
{labels}
|
||||
</Header>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export const Other = () => (
|
||||
<Page theme={pageTheme.other}>
|
||||
<Header title="Other/generic component page" type="other">
|
||||
|
||||
@@ -65,6 +65,11 @@ export const gradients: Record<string, Gradient> = {
|
||||
waveColor: '#9BF0E1',
|
||||
opacity: ['0.72', '0.0'],
|
||||
},
|
||||
pinkSea: {
|
||||
colors: ['#C8077A', '#C2297D'],
|
||||
waveColor: '#ea93c3',
|
||||
opacity: ['0.8', '0.0'],
|
||||
},
|
||||
};
|
||||
|
||||
export const pageTheme: Record<string, PageTheme> = {
|
||||
@@ -72,7 +77,7 @@ export const pageTheme: Record<string, PageTheme> = {
|
||||
gradient: gradients.teal,
|
||||
},
|
||||
documentation: {
|
||||
gradient: gradients.eveningSea,
|
||||
gradient: gradients.pinkSea,
|
||||
},
|
||||
tool: {
|
||||
gradient: gradients.purpleSky,
|
||||
|
||||
@@ -129,19 +129,27 @@ const recentlyViewedIntroText =
|
||||
|
||||
export const SidebarIntro: FC = () => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const [
|
||||
{ starredItemsDismissed, recentlyViewedItemsDismissed },
|
||||
setDismissedIntro,
|
||||
] = useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE, {
|
||||
const defaultValue = {
|
||||
starredItemsDismissed: false,
|
||||
recentlyViewedItemsDismissed: false,
|
||||
});
|
||||
};
|
||||
const [dismissedIntro, setDismissedIntro] = useLocalStorage<
|
||||
SidebarIntroLocalStorage
|
||||
>(SIDEBAR_INTRO_LOCAL_STORAGE);
|
||||
|
||||
const { starredItemsDismissed, recentlyViewedItemsDismissed } =
|
||||
dismissedIntro ?? {};
|
||||
|
||||
const dismissStarred = () => {
|
||||
setDismissedIntro(state => ({ ...state, starredItemsDismissed: true }));
|
||||
setDismissedIntro(state => ({
|
||||
...defaultValue,
|
||||
...state,
|
||||
starredItemsDismissed: true,
|
||||
}));
|
||||
};
|
||||
const dismissRecentlyViewed = () => {
|
||||
setDismissedIntro(state => ({
|
||||
...defaultValue,
|
||||
...state,
|
||||
recentlyViewedItemsDismissed: true,
|
||||
}));
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
export * from './Content';
|
||||
export * from './ContentHeader';
|
||||
export * from './ErrorBoundary';
|
||||
export * from './ErrorPage';
|
||||
export * from './Header';
|
||||
export * from './HeaderLabel';
|
||||
export * from './HeaderTabs';
|
||||
export * from './HomepageTimer';
|
||||
export * from './InfoCard';
|
||||
export * from './ItemCard';
|
||||
export * from './Page';
|
||||
export * from './Sidebar';
|
||||
export * from './SignInPage';
|
||||
export * from './TabbedCard';
|
||||
export * from './HeaderTabs';
|
||||
export * from './ItemCard';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
ignorePatterns: ['templates/**'],
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
# @backstage/create-app
|
||||
|
||||
This package provides a CLI for creating apps.
|
||||
You can use the flag `--skip-install` to skip the install.
|
||||
|
||||
## Installation
|
||||
|
||||
With `npx`:
|
||||
|
||||
```sh
|
||||
$ npx @backstage/create-app
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Figure out whether we're running inside the backstage repo or as an installed dependency
|
||||
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
|
||||
|
||||
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
require('..');
|
||||
} else {
|
||||
require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
compilerOptions: {
|
||||
module: 'CommonJS',
|
||||
},
|
||||
});
|
||||
|
||||
require('../src');
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/create-app",
|
||||
"description": "Create app package for Backstage",
|
||||
"version": "0.1.1-alpha.16",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"directory": "packages/create-app"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"bin": {
|
||||
"backstage-create-app": "bin/backstage-create-app"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli build --outputs cjs",
|
||||
"lint": "backstage-cli lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^4.1.1",
|
||||
"chalk": "^4.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"handlebars": "^4.7.3",
|
||||
"inquirer": "^7.0.4",
|
||||
"ora": "^4.0.3",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/react-dev-utils": "^9.0.4",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/ora": "^3.2.0",
|
||||
"ts-node": "^8.6.2"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"templates"
|
||||
]
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
import { Task, templatingTask } from './lib/tasks';
|
||||
import { paths } from './lib/paths';
|
||||
import { version } from './lib/version';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
|
||||
@@ -104,8 +104,17 @@ export default async (cmd: Command): Promise<void> => {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'dbType',
|
||||
message: chalk.blue('Select database for the backend [required]'),
|
||||
// @ts-ignore
|
||||
choices: ['PostgreSQL', 'SQLite'],
|
||||
},
|
||||
];
|
||||
const answers: Answers = await inquirer.prompt(questions);
|
||||
answers.dbTypePG = answers.dbType === 'PostgreSQL';
|
||||
answers.dbTypeSqlite = answers.dbType === 'SQLite';
|
||||
|
||||
const templateDir = paths.resolveOwn('templates/default-app');
|
||||
const tempDir = resolvePath(os.tmpdir(), answers.name);
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 program from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { exitWithError } from './lib/errors';
|
||||
import { version } from './lib/version';
|
||||
import createApp from './createApp';
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
program.name('backstage-create-app').version(version);
|
||||
|
||||
program
|
||||
.description('Creates a new app in a new directory')
|
||||
.option(
|
||||
'--skip-install',
|
||||
'Skip the install and builds steps after creating the app',
|
||||
)
|
||||
.action(createApp)
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp(chalk.yellow);
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', rejection => {
|
||||
if (rejection instanceof Error) {
|
||||
exitWithError(rejection);
|
||||
} else {
|
||||
exitWithError(new Error(`Unknown rejection: '${rejection}'`));
|
||||
}
|
||||
});
|
||||
|
||||
main(process.argv);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 chalk from 'chalk';
|
||||
|
||||
export class CustomError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExitCodeError extends CustomError {
|
||||
readonly code: number;
|
||||
|
||||
constructor(code: number, command?: string) {
|
||||
if (command) {
|
||||
super(`Command '${command}' exited with code ${code}`);
|
||||
} else {
|
||||
super(`Child exited with code ${code}`);
|
||||
}
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export function exitWithError(error: Error): never {
|
||||
if (error instanceof ExitCodeError) {
|
||||
process.stderr.write(`\n${chalk.red(error.message)}\n\n`);
|
||||
process.exit(error.code);
|
||||
} else {
|
||||
process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 fs from 'fs-extra';
|
||||
import { dirname, resolve as resolvePath } from 'path';
|
||||
|
||||
export type ResolveFunc = (...paths: string[]) => string;
|
||||
|
||||
// Common paths and resolve functions used by the cli.
|
||||
// Currently assumes it is being executed within a monorepo.
|
||||
export type Paths = {
|
||||
// Root dir of the cli itself, containing package.json
|
||||
ownDir: string;
|
||||
|
||||
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
|
||||
ownRoot: string;
|
||||
|
||||
// The location of the app that the cli is being executed in
|
||||
targetDir: string;
|
||||
|
||||
// The monorepo root package of the app that the cli is being executed in.
|
||||
targetRoot: string;
|
||||
|
||||
// Resolve a path relative to own repo
|
||||
resolveOwn: ResolveFunc;
|
||||
|
||||
// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
|
||||
resolveOwnRoot: ResolveFunc;
|
||||
|
||||
// Resolve a path relative to the app
|
||||
resolveTarget: ResolveFunc;
|
||||
|
||||
// Resolve a path relative to the app repo root
|
||||
resolveTargetRoot: ResolveFunc;
|
||||
};
|
||||
|
||||
// Looks for a package.json that has name: "root" to identify the root of the monorepo
|
||||
export function findRootPath(topPath: string): string {
|
||||
let path = topPath;
|
||||
|
||||
// Some sanity check to avoid infinite loop
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const packagePath = resolvePath(path, 'package.json');
|
||||
const exists = fs.pathExistsSync(packagePath);
|
||||
if (exists) {
|
||||
try {
|
||||
const data = fs.readJsonSync(packagePath);
|
||||
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
|
||||
return path;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse package.json file while searching for root, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newPath = dirname(path);
|
||||
if (newPath === path) {
|
||||
throw new Error(
|
||||
`No package.json with name "root" found as a parent of ${topPath}`,
|
||||
);
|
||||
}
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Iteration limit reached when searching for root package.json at ${topPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Finds the root of the cli package itself
|
||||
export function findOwnDir() {
|
||||
// Known relative locations of package in dist/dev
|
||||
const pathDist = '..';
|
||||
const pathDev = '../..';
|
||||
|
||||
// Check the closest dir first
|
||||
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
|
||||
const isDist = fs.pathExistsSync(pkgInDist);
|
||||
|
||||
const path = isDist ? pathDist : pathDev;
|
||||
return resolvePath(__dirname, path);
|
||||
}
|
||||
|
||||
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
|
||||
export function findOwnRootPath(ownDir: string) {
|
||||
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
|
||||
if (!isLocal) {
|
||||
throw new Error(
|
||||
'Tried to access monorepo package root dir outside of Backstage repository',
|
||||
);
|
||||
}
|
||||
|
||||
return resolvePath(ownDir, '../..');
|
||||
}
|
||||
|
||||
export function findPaths(): Paths {
|
||||
const ownDir = findOwnDir();
|
||||
const targetDir = fs.realpathSync(process.cwd());
|
||||
|
||||
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
|
||||
let ownRoot = '';
|
||||
const getOwnRoot = () => {
|
||||
if (!ownRoot) {
|
||||
ownRoot = findOwnRootPath(ownDir);
|
||||
}
|
||||
return ownRoot;
|
||||
};
|
||||
|
||||
// We're not always running in a monorepo, so we lazy init this to only crash commands
|
||||
// that require a monorepo when we're not in one.
|
||||
let targetRoot = '';
|
||||
const getTargetRoot = () => {
|
||||
if (!targetRoot) {
|
||||
targetRoot = findRootPath(targetDir);
|
||||
}
|
||||
return targetRoot;
|
||||
};
|
||||
|
||||
return {
|
||||
ownDir,
|
||||
get ownRoot() {
|
||||
return getOwnRoot();
|
||||
},
|
||||
targetDir,
|
||||
get targetRoot() {
|
||||
return getTargetRoot();
|
||||
},
|
||||
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
|
||||
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
|
||||
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
|
||||
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
|
||||
};
|
||||
}
|
||||
|
||||
export const paths = findPaths();
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import handlebars from 'handlebars';
|
||||
import ora from 'ora';
|
||||
import { basename, dirname } from 'path';
|
||||
import recursive from 'recursive-readdir';
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
|
||||
export class Task {
|
||||
static log(name: string = '') {
|
||||
process.stdout.write(`${chalk.green(name)}\n`);
|
||||
}
|
||||
|
||||
static error(message: string = '') {
|
||||
process.stdout.write(`\n${chalk.red(message)}\n\n`);
|
||||
}
|
||||
|
||||
static section(name: string) {
|
||||
const title = chalk.green(`${name}:`);
|
||||
process.stdout.write(`\n ${title}\n`);
|
||||
}
|
||||
|
||||
static exit(code: number = 0) {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
static async forItem(
|
||||
task: string,
|
||||
item: string,
|
||||
taskFunc: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH));
|
||||
|
||||
const spinner = ora({
|
||||
prefixText: chalk.green(` ${paddedTask}${chalk.cyan(item)}`),
|
||||
spinner: 'arc',
|
||||
color: 'green',
|
||||
}).start();
|
||||
|
||||
try {
|
||||
await taskFunc();
|
||||
spinner.succeed();
|
||||
} catch (error) {
|
||||
spinner.fail();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function templatingTask(
|
||||
templateDir: string,
|
||||
destinationDir: string,
|
||||
context: any,
|
||||
) {
|
||||
const files = await recursive(templateDir).catch(error => {
|
||||
throw new Error(`Failed to read template directory: ${error.message}`);
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
const destinationFile = file.replace(templateDir, destinationDir);
|
||||
await fs.ensureDir(dirname(destinationFile));
|
||||
|
||||
if (file.endsWith('.hbs')) {
|
||||
await Task.forItem('templating', basename(file), async () => {
|
||||
const destination = destinationFile.replace(/\.hbs$/, '');
|
||||
|
||||
const template = await fs.readFile(file);
|
||||
const compiled = handlebars.compile(template.toString());
|
||||
const contents = compiled({ name: basename(destination), ...context });
|
||||
|
||||
await fs.writeFile(destination, contents).catch(error => {
|
||||
throw new Error(
|
||||
`Failed to create file: ${destination}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await Task.forItem('copying', basename(file), async () => {
|
||||
await fs.copyFile(file, destinationFile).catch(error => {
|
||||
const destination = destinationFile;
|
||||
throw new Error(
|
||||
`Failed to copy file to ${destination} : ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 fs from 'fs-extra';
|
||||
import { paths } from './paths';
|
||||
|
||||
export function findVersion() {
|
||||
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
|
||||
return JSON.parse(pkgContent).version;
|
||||
}
|
||||
|
||||
export const version = findVersion();
|
||||
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
app:
|
||||
title: Scaffolded Backstage App
|
||||
baseUrl: http://localhost:3000
|
||||
|
||||
organization:
|
||||
name: Acme Corporation
|
||||
|
||||
backend:
|
||||
baseUrl: http://localhost:7000
|
||||
listen: 0.0.0.0:7000
|
||||
cors:
|
||||
origin: http://localhost:3000
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
|
||||
proxy:
|
||||
'/test':
|
||||
target: 'https://example.com'
|
||||
changeOrigin: true
|
||||
|
||||
techdocs:
|
||||
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
|
||||
@@ -16,13 +16,13 @@
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^14.2.0"
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"cross-env": "^7.0.0",
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 890 B After Width: | Height: | Size: 890 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM node:12
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
|
||||
CMD ["node", "packages/backend"]
|
||||
@@ -0,0 +1,68 @@
|
||||
# example-backend
|
||||
|
||||
This package is an EXAMPLE of a Backstage backend.
|
||||
|
||||
The main purpose of this package is to provide a test bed for Backstage plugins
|
||||
that have a backend part. Feel free to experiment locally or within your fork
|
||||
by adding dependencies and routes to this backend, to try things out.
|
||||
|
||||
Our goal is to eventually amend the create-app flow of the CLI, such that a
|
||||
production ready version of a backend skeleton is made alongside the frontend
|
||||
app. Until then, feel free to experiment here!
|
||||
|
||||
## Development
|
||||
|
||||
To run the example backend, first go to the project root and run
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn tsc
|
||||
yarn build
|
||||
```
|
||||
|
||||
You should only need to do this once.
|
||||
|
||||
After that, go to the `packages/backend` directory and run
|
||||
|
||||
```bash
|
||||
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
|
||||
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
|
||||
LOG_LEVEL=debug \
|
||||
yarn start
|
||||
```
|
||||
|
||||
Substitute `x` for actual values, or leave them as
|
||||
dummy values just to try out the backend without using the auth or sentry features.
|
||||
|
||||
The backend starts up on port 7000 per default.
|
||||
|
||||
## Populating The Catalog
|
||||
|
||||
If you want to use the catalog functionality, you need to add so called locations
|
||||
to the backend. These are places where the backend can find some entity descriptor
|
||||
data to consume and serve.
|
||||
|
||||
To get started, you can issue the following after starting the backend, from inside
|
||||
the `plugins/catalog-backend` directory:
|
||||
|
||||
```bash
|
||||
yarn mock-data
|
||||
```
|
||||
|
||||
You should then start seeing data on `localhost:7000/catalog/entities`.
|
||||
|
||||
The catalog currently runs in-memory only, so feel free to try it out, but it will
|
||||
need to be re-populated on next startup.
|
||||
|
||||
## Authentication
|
||||
|
||||
We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
|
||||
|
||||
Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli backend:build",
|
||||
"build-image": "backstage-cli backend:build-image example-backend",
|
||||
"start": "backstage-cli backend:dev",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean",
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^{{version}}",
|
||||
"@backstage/catalog-model": "^{{version}}",
|
||||
"@backstage/config": "^0.1.1-alpha.13",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.13",
|
||||
"@backstage/plugin-auth-backend": "^{{version}}",
|
||||
"@backstage/plugin-catalog-backend": "^{{version}}",
|
||||
"@backstage/plugin-identity-backend": "^{{version}}",
|
||||
"@backstage/plugin-proxy-backend": "^{{version}}",
|
||||
"@backstage/plugin-rollbar-backend": "^{{version}}",
|
||||
"@backstage/plugin-scaffolder-backend": "^{{version}}",
|
||||
"@backstage/plugin-sentry-backend": "^{{version}}",
|
||||
"@backstage/plugin-techdocs-backend": "^{{version}}",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
{{#if dbTypePG}}
|
||||
"pg": "^8.3.0",
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
"sqlite3": "^4.2.0",
|
||||
{{/if}}
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.15",
|
||||
"@types/dockerode": "^2.5.32",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"@types/helmet": "^0.0.47"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { PluginEnvironment } from './types';
|
||||
|
||||
describe('test', () => {
|
||||
it('unbreaks the test runner', () => {
|
||||
const unbreaker = {} as PluginEnvironment;
|
||||
expect(unbreaker).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Hi!
|
||||
*
|
||||
* Note that this is an EXAMPLE Backstage backend. Please check the README.
|
||||
*
|
||||
* Happy hacking!
|
||||
*/
|
||||
|
||||
import {
|
||||
createServiceBuilder,
|
||||
getRootLogger,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from '@backstage/config-loader';
|
||||
{{#if dbTypePG}}
|
||||
import knex, { PgConnectionConfig } from 'knex';
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
import knex from 'knex';
|
||||
{{/if}}
|
||||
import auth from './plugins/auth';
|
||||
import catalog from './plugins/catalog';
|
||||
import identity from './plugins/identity';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
const config = ConfigReader.fromConfigs(loadedConfigs);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
|
||||
{{#if dbTypePG}}
|
||||
const knexConfig = {
|
||||
client: 'pg',
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
port: process.env.POSTGRES_PORT,
|
||||
host: process.env.POSTGRES_HOST,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: `backstage_plugin_${plugin}`,
|
||||
} as PgConnectionConfig,
|
||||
};
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
const knexConfig = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
{{/if}}
|
||||
const database = knex(knexConfig);
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
return { logger, database, config };
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configs = await loadConfig();
|
||||
const configReader = ConfigReader.fromConfigs(configs);
|
||||
const createEnv = makeCreateEnv(configs);
|
||||
|
||||
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
|
||||
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(configReader)
|
||||
.addRouter('/catalog', await catalog(catalogEnv))
|
||||
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
|
||||
.addRouter('/auth', await auth(authEnv))
|
||||
.addRouter('/identity', await identity(identityEnv))
|
||||
.addRouter('/techdocs', await techdocs(techdocsEnv))
|
||||
.addRouter('/proxy', await proxy(proxyEnv));
|
||||
|
||||
await service.start().catch(err => {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
main().catch(error => {
|
||||
console.error(`Backend failed to start up, ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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 { createRouter } from '@backstage/plugin-auth-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
database,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config, database });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
createRouter,
|
||||
DatabaseEntitiesCatalog,
|
||||
DatabaseLocationsCatalog,
|
||||
DatabaseManager,
|
||||
HigherOrderOperations,
|
||||
LocationReaders,
|
||||
runPeriodically,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { useHotCleanup } from '@backstage/backend-common';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
database,
|
||||
}: PluginEnvironment) {
|
||||
const locationReader = new LocationReaders(logger);
|
||||
|
||||
const db = await DatabaseManager.createDatabase(database, { logger });
|
||||
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
|
||||
const locationsCatalog = new DatabaseLocationsCatalog(db);
|
||||
const higherOrderOperation = new HigherOrderOperations(
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
locationReader,
|
||||
logger,
|
||||
);
|
||||
|
||||
useHotCleanup(
|
||||
module,
|
||||
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
|
||||
);
|
||||
|
||||
return await createRouter({
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { createRouter } from '@backstage/plugin-identity-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { createRouter } from '@backstage/plugin-proxy-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
CookieCutter,
|
||||
createRouter,
|
||||
FilePreparer,
|
||||
GithubPreparer,
|
||||
Preparers,
|
||||
GithubPublisher,
|
||||
CreateReactAppTemplater,
|
||||
Templaters,
|
||||
} from '@backstage/plugin-scaffolder-backend';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
const cookiecutterTemplater = new CookieCutter();
|
||||
const craTemplater = new CreateReactAppTemplater();
|
||||
const templaters = new Templaters();
|
||||
templaters.register('cookiecutter', cookiecutterTemplater);
|
||||
templaters.register('cra', craTemplater);
|
||||
|
||||
const filePreparer = new FilePreparer();
|
||||
const githubPreparer = new GithubPreparer();
|
||||
const preparers = new Preparers();
|
||||
|
||||
preparers.register('file', filePreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
|
||||
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
||||
const publisher = new GithubPublisher({ client: githubClient });
|
||||
|
||||
const dockerClient = new Docker();
|
||||
return await createRouter({
|
||||
preparers,
|
||||
templaters,
|
||||
publisher,
|
||||
logger,
|
||||
dockerClient,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { createRouter } from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 Knex from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
database: Knex;
|
||||
config: Config;
|
||||
};
|
||||
@@ -25,7 +25,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^14.2.0",
|
||||
"react-use": "^15.3.3",
|
||||
"react-router-dom": "6.0.0-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||