Merge branch 'master' into new-routing-jenkins

This commit is contained in:
Tim Jacomb
2020-09-16 14:46:49 +01:00
217 changed files with 8245 additions and 1939 deletions
+8
View File
@@ -0,0 +1,8 @@
comment: false # Ref: https://docs.codecov.io/docs/pull-request-comments
coverage:
status:
project:
default:
threshold: 0% # Ref: https://docs.codecov.io/docs/codecovyml-reference#coveragestatus
target: auto
+18
View File
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 5
labels:
- dependencies
- package-ecosystem: npm
directory: '/microsite/'
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 2
labels:
- dependencies
+4 -2
View File
@@ -93,9 +93,11 @@ jobs:
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn lerna -- run test --since origin/master -- --coverage
- name: test all packages
- name: test all packages (and upload coverage)
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: yarn lerna -- run test -- --coverage
run: |
yarn lerna -- run test -- --coverage
bash <(curl -s https://codecov.io/bash)
- name: verify plugin template
run: yarn lerna -- run diff -- --check
-13
View File
@@ -27,25 +27,12 @@ jobs:
steps:
- uses: actions/checkout@v2
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn tsc
- name: yarn build
+4 -2
View File
@@ -59,8 +59,10 @@ jobs:
- name: verify type dependencies
run: yarn lint:type-deps
- name: test
run: yarn lerna -- run test -- --coverage
- name: test (and upload coverage)
run: |
yarn lerna -- run test -- --coverage
bash <(curl -s https://codecov.io/bash)
# Publishes current version of packages that are not already present in the registry
- name: publish
@@ -0,0 +1,53 @@
name: Automatically add new TechDocs Issues and PRs to the GitHub project board
# Development of TechDocs in Backstage is managed by this Kanban board - https://github.com/spotify/backstage/projects/5
# New issues with TechDocs in their title or docs-like-code label will be added to the board.
on:
issues:
types: [opened, reopened, labeled, edited]
pull_request:
types: [opened, reopened, labeled, edited]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
assign_issue_or_pr_to_project:
runs-on: ubuntu-latest
name: Triage
steps:
- name: Assign new issue to Incoming based on its title.
uses: srggrs/assign-one-project-github-action@1.2.0
if: |
contains(github.event.issue.title, 'TechDocs') ||
contains(github.event.issue.title, 'techdocs') ||
contains(github.event.issue.title, 'Techdocs')
with:
project: 'https://github.com/spotify/backstage/projects/5'
column_name: 'Incoming'
- name: Assign new issue to Incoming based on its label.
uses: srggrs/assign-one-project-github-action@1.2.0
if: |
contains(github.event.issue.labels.*.name, 'docs-like-code')
with:
project: 'https://github.com/spotify/backstage/projects/5'
column_name: 'Incoming'
- name: Assign new PR to Incoming based on its title.
uses: srggrs/assign-one-project-github-action@1.2.0
if: |
contains(github.event.pull_request.title, 'TechDocs') ||
contains(github.event.pull_request.title, 'techdocs') ||
contains(github.event.pull_request.title, 'Techdocs')
with:
project: 'https://github.com/spotify/backstage/projects/5'
column_name: 'Incoming'
- name: Assign new PR to Incoming based on its label.
uses: srggrs/assign-one-project-github-action@1.2.0
if: |
contains(github.event.pull_request.labels.*.name, 'docs-like-code')
with:
project: 'https://github.com/spotify/backstage/projects/5'
column_name: 'Incoming'
+45 -1
View File
@@ -6,13 +6,40 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
### @backstage/core
- Introduced initial version of an inverted app/plugin relationship, where plugins export components for apps to use, instead registering themselves directly into the app. This enables more fine-grained control of plugin features, and also composition of plugins such as catalog pages with additional cards and tabs. This breaks the use of `RouteRef`s, and there will be more changes related to this in the future, but this change lays the initial foundation. See `packages/app` and followup PRs for how to update plugins for this change. [#2076](https://github.com/spotify/backstage/pull/2076)
- Switch to an automatic dependency injection mechanism for all Utility APIs, allowing plugins to ship default implementations of their APIs. See [https://backstage.io/docs/api/utility-apis](https://backstage.io/docs/api/utility-apis). [#2285](https://github.com/spotify/backstage/pull/2285)
> Collect changes for the next release below
### @backstage/cli
- Change `backstage-cli backend:build-image` to forward all args to `docker image build`, instead of just tag. Also add `--build` flag for building all dependent packages before packaging the workspace for the docker build. [#2299](https://github.com/spotify/backstage/pull/2299)
### @backstage/create-app
- Change root `tsc` output dir to `dist-types`, in order to allow for standalone plugin repos. [#2278](https://github.com/spotify/backstage/pull/2278)
## v0.1.1-alpha.21
- Added many more frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084)
### @backstage/core
- Material-UI: Bumped to 4.11.0, which is the version that create-app will
resolve to, because we wanted to get the renaming of ExpansionPanel to
Accordion into place. This gets rid of a lot of console deprecation warnings
in newly scaffolded apps.
### @backstage/cli
- Set `NODE_ENV` to `test` when running test. [#2214](https://github.com/spotify/backstage/pull/2214)
- Fix for backend plugins names requiring to be prefixed with `@backstage` to build. [#2224](https://github.com/spotify/backstage/pull/2224)
### @backstage/backend-common
- The backend plugin
[service builder](https://github.com/spotify/backstage/blob/master/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts)
no longer adds `express.json()` automatically to all routes. While convenient
@@ -22,14 +49,31 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
still had to cater to that manually. We therefore decided to let plugins add
`express.json()` themselves if they happen to deal with JSON data.
### @backstage/catalog-backend
- Add rules configuration for catalog location and entity kinds. The default rules should cover most use-cases, but you may need to allow specific entity kinds when using things like Template or Group entities. [#2118](https://github.com/spotify/backstage/pull/2118)
## v0.1.1-alpha.20
- Includes https://github.com/spotify/backstage/pull/2097 to resolve issues with create-plugin command.
### @backstage/cli
- Use config files according to `NODE_ENV` when serving and building frontend packages. [#2077](https://github.com/spotify/backstage/pull/2077)
- Pin `rollup-plugin-dts` to avoid a later broken version. [#2097](https://github.com/spotify/backstage/pull/2097)
## v0.1.1-alpha.19
### @backstage/backend-common
- Allow listen host and port to be configured separately, in order to support PORT environment variables. [#1950](https://github.com/spotify/backstage/pull/1950)
### @backstage/core
- Added new `DiscoveryApi` for discovering backend endpoint in the frontend, and use in most plugins. See [packages/app/src/apis.ts](https://github.com/spotify/backstage/blob/master/packages/app/src/apis.ts) for how to register in your app. [#2074](https://github.com/spotify/backstage/pull/2074)
### @backstage/create-app
- Added catalog and scaffolder frontend plugins to the template along with the sidebar. [#1942](https://github.com/spotify/backstage/pull/1942), [#2084](https://github.com/spotify/backstage/pull/2084)
- Many plugins have been added to the catalog and will for now be required to be added to separate apps as well. This will be solved as [#1536](https://github.com/spotify/backstage/issues/1536) gets sorted out, but for now you may need to install some plugins just to get pages to work.
### @backstage/catalog-backend
+1 -29
View File
@@ -62,35 +62,7 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md)
So...feel ready to jump in? Let's do this. 👏🏻💯
To run a Backstage app, you will need to have the following installed:
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12
- [yarn](https://classic.yarnpkg.com/en/docs/install)
After cloning this repo, open a terminal window and start the example app using the following commands from the project root:
```bash
yarn install # Install dependencies
yarn start # Start dev server, use --check to enable linting and type-checks
```
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
Depending on the work you're doing, you often also want to run the example backend. Start the backend in a separate terminal session using the following:
```bash
cd packages/backend
yarn start
yarn lerna run mock-data # Populate the backend with mock data
```
And that's it! You are good to go 👍
If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
Start by reading our [Getting Started](https://backstage.io/docs/getting-started/) page. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
# Coding Guidelines
+19
View File
@@ -0,0 +1,19 @@
# Owners
- See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines.
## Maintainers 🏓
- Patrik Oldsberg (@Rugvip, Spotify)
- Fredrik Adelöw (@freben, Spotify)
- Raghunandan Balachandran (@soapraj, Spotify)
- Ben Lambert (@benjdlambert, Spotify)
- Marcus Eide (@marcuseide, Spotify)
- Niklas Ek (@nikek, Spotify)
- Stefan Ålund (@stefanalund, Spotify)
- Kat Zhou (@katz95, Spotify)
## Hall of Fame 👏
- Andrew Thauer (@andrewthauer, Wealthsimple)
- Oliver Sand (@Fox32, SDA-SE)
+2 -27
View File
@@ -6,6 +6,7 @@
![](https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg)
[![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX)
![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)
[![Codecov](https://img.shields.io/codecov/c/github/spotify/backstage)](https://codecov.io/gh/spotify/backstage)
[![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/spotify/backstage/releases)
## What is Backstage?
@@ -31,33 +32,7 @@ A detailed project roadmap, including already delivered milestones, is available
## Getting Started
There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do.
Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on `@backstage` packages from NPM, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance.
On the other hand, if you want to contribute plugins or to the project in general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo.
### Creating a Standalone App
Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have
[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You will also need to have [Docker](https://docs.docker.com/engine/install/) installed to use some features like Software Templates and TechDocs.
Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory:
```bash
npx @backstage/create-app
```
You will be taken through a wizard to create your app, and the output should look something like this. You can read more about this process [here](https://backstage.io/docs/getting-started/create-an-app)
### Contributing to Backstage
You can read more in our [CONTRIBUTING.md](./CONTRIBUTING.md#get-started) guide, which can help you get setup with a Backstage development environment.
### Next steps
Take a look at the [Getting Started](https://backstage.io/docs/getting-started/index) guide to learn how to set up Backstage, and how to develop on the platform.
Check out [the documentation](https://backstage.io/docs/getting-started) on how to start using Backstage.
## Documentation
+26 -17
View File
@@ -34,6 +34,7 @@ organization:
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
sentry:
organization: spotify
@@ -56,6 +57,10 @@ catalog:
rules:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
privateToken:
$secret:
@@ -75,23 +80,27 @@ catalog:
privateToken:
$secret:
env: AZURE_PRIVATE_TOKEN
exampleEntityLocations:
github:
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
- https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml
locations:
# Backstage example components
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
# Example component for github-actions
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/github-actions/examples/sample.yaml
# Example component for techdocs
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/documented-component.yaml
# Backstage example APIs
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml
# Backstage example templates
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml
auth:
providers:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 102 KiB

@@ -1,4 +1,7 @@
# Installing in your Backstage App
---
id: installation
title: Installing in your Backstage App
---
The catalog plugin comes in two packages, `@backstage/plugin-catalog` and
`@backstage/plugin-catalog-backend`. Each has their own installation steps,
@@ -105,13 +105,6 @@ curl \
This should then have added the catalog, and also should now be listed under the
create page at http://localhost:3000/create.
Alternatively, if you want to get setup with some mock templates that are
already provided, run the following to load those templates:
```
yarn lerna run mock-data
```
The `type` field which is chosen in the request to add the `template.yaml` to
the Service Catalog here, will be come the `PreparerKey` which will be used to
select the `Preparer` when creating a job.
+8 -5
View File
@@ -15,12 +15,15 @@ like GitHub.
### Getting Started
The Software Templates are available under `/create`. If you've followed
[Installing in your Backstage App](./installation.md) in your separate App or
[Getting Started with Backstage](../../getting-started) for this repo, you
should be able to reach `http://localhost:3000/create`.
> Be sure to have covered [Installing in your Backstage App](./installation.md)
> for your separate App or
> [Getting Started with Backstage](../../getting-started) for this repo before
> proceeding.
You should get something that looks similar to this:
The Software Templates are available under `/create`. For local development you
should be able to reach them at `http://localhost:3000/create`.
Once there, you should see something that looks similar to this:
![Create Image](../../assets/software-templates/create.png)
@@ -1,4 +1,7 @@
# Installing in your Backstage App
---
id: installation
title: Installing in your Backstage App
---
The scaffolder plugin comes in two packages, `@backstage/plugin-scaffolder` and
`@backstage/plugin-scaffolder-backend`. Each has their own installation steps,
@@ -174,9 +177,6 @@ docs on creating private GitHub access tokens is available
Note that the need for private GitHub access tokens will be replaced with GitHub
Apps integration further down the line.
> **Right now it is only possible to scaffold repositories inside GitHub
> organizations, and not under personal accounts.**
The GitHub access token is passed along using the `GITHUB_ACCESS_TOKEN`
environment variable.
+10 -6
View File
@@ -6,14 +6,11 @@ sidebar_label: FAQ
This page answers frequently asked questions about [TechDocs](README.md).
_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)
- [What is the mkdocs-techdocs-core plugin?](#what-is-the-mkdocs-techdocs-core-plugin)
- [Does TechDocs support file formats other than Markdown (e.g. rst, asciidoc)?](#does-techdocs-support-file-formats-other-than-markdown-eg-rst-asciidoc-)
#### What static site generator is TechDocs using?
@@ -30,3 +27,10 @@ package is a MkDocs Plugin that works like a wrapper around multiple MkDocs
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.
#### Does TechDocs support file formats other than Markdown (e.g. rst, asciidoc) ?
Not right now. We are currently using MkDocs to generate the documentation from
source. So, they have to be in Markdown format. However, in future we want to
support other alternatives to MkDocs. That will make it possible to use other
file formats.
+18 -17
View File
@@ -8,16 +8,21 @@ sidebar_label: Overview
<!-- Intro, backstory, etc.: -->
Wait, what is TechDocs? TechDocs is Spotifys homegrown docs-like-code solution
built directly into Backstage. Today, it is now one of the core products in
Spotifys developer experience offering with 2,400+ documentation sites and
1,000+ engineers using it daily.
TechDocs is Spotifys homegrown docs-like-code solution built directly into
Backstage. This means engineers write their documentation in Markdown files
which live together with their code.
Today, it is one of the core products in Spotifys developer experience offering
with 2,400+ documentation sites and 1,000+ engineers using it daily. Read more
about TechDocs and the philosophy in its
[announcement blog post](https://backstage.io/blog/2020/09/08/announcing-tech-docs).
🎉
## Features
- A centralized place to discover documentation.
- A centralized place to discover and read documentation.
- A clear end-to-end docs-like-code solution. (_Coming soon in V.1_)
- A clear end-to-end docs-like-code solution.
- A tightly coupled feedback loop with the developer workflow. (_Coming soon in
V.3_)
@@ -26,12 +31,12 @@ Spotifys developer experience offering with 2,400+ documentation sites and
## Project roadmap
| Version | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [TechDocs V.0 ✅][v0] | Read docs in Backstage - Enable anyone to get a reader experience working in Backstage. [See V.0 Use Cases.](./#techdocs-v0) |
| [TechDocs V.1 🚧][v1] | TechDocs end to end (alpha) - Alpha of TechDocs that you can use end to end - and contribute to. [See V.1 Use Cases.](./#techdocs-v1) |
| [TechDocs V.2 🔮⌛][v2] | Platform stability and compatibility improvements. [See V.2 Use Cases.](./#techdocs-v2) |
| TechDocs V.3 🔮⌛ | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. |
| Version | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [TechDocs V.0 ✅][v0] | Read docs in Backstage - Enable anyone to get a reader experience working in Backstage. [See V.0 Use Cases.](#techdocs-v0) |
| [TechDocs V.1 ][v1] | TechDocs end to end (alpha) - Alpha of TechDocs that you can use end to end - and contribute to. [See V.1 Use Cases.](#techdocs-v1) |
| [TechDocs V.2 🔮⌛][v2] | Platform stability and compatibility improvements. [See V.2 Use Cases.](#techdocs-v2) |
| TechDocs V.3 🔮⌛ | Widget Architecture - TechDocs widget architecture available, so the community can create their own customized features. |
[v0]: https://github.com/spotify/backstage/milestone/15
[v1]: https://github.com/spotify/backstage/milestone/16
@@ -44,7 +49,7 @@ Spotifys developer experience offering with 2,400+ documentation sites and
#### TechDocs V.0
- As a user I can navigate to a manually curated docs explore page.
- As a user I can navigte to and read mock documentation that is manually
- As a user I can navigate to and read mock documentation that is manually
uploaded by the TechDocs core team.
#### TechDocs V.1
@@ -101,7 +106,3 @@ more to come...
https://github.com/spotify/backstage/blob/master/packages/techdocs-container
[techdocs/cli]:
https://github.com/spotify/backstage/blob/master/packages/techdocs-cli
## TechDocs Big Picture
![TechDocs Big Picture](../..//assets/techdocs/techdocs_big_picture.png)
+6
View File
@@ -0,0 +1,6 @@
---
id: architecture
title: Architecture
---
![TechDocs Big Picture](../../assets/techdocs/techdocs_big_picture.png)
+3 -3
View File
@@ -8,9 +8,9 @@ solution in Backstage.
### TechDocs Core Plugin
The TechDocs Core Plugin is a MkDocs plugin created as a wrapper around multiple
MkDocs plugins and Python Markdown extensions to standardize the configuration
of MkDocs used for TechDocs.
The TechDocs Core Plugin is an [MkDocs](https://www.mkdocs.org/) plugin created
as a wrapper around multiple MkDocs plugins and Python Markdown extensions to
standardize the configuration of MkDocs used for TechDocs.
[TechDocs Core](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/techdocs-core/README.md)
@@ -11,7 +11,7 @@ This section will guide you through:
- [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository)
- [Writing and previewing your documentation](#writing-and-previewing-your-documentation)
## Prerequisities
## Prerequisites
- A working Backstage instance with TechDocs installed (see
[TechDocs getting started](getting-started.md))
@@ -30,11 +30,6 @@ the documentation template.
Create an entity from the documentation template and you will get the needed
setup for free.
!!! warning Currently the Backstage Software Templates are limited to create
repositories inside GitHub organizations. You also need to generate an personal
access token and use as an environment variable. Read more about this
[here](../software-templates/installation.md#runtime-dependencies).
### Manually add documentation setup to already existing repository
Prerequisities:
@@ -81,12 +76,12 @@ updated documentation next time you run Backstage!
## Writing and previewing your documentation
Using the `techdocs-cli` you can preview your docs inside a local Backstage
instance and get automatic recompilation on changes. This is useful for when you
want to write your documentation.
instance and get live reload on changes. This is useful when you want to preview
your documentation while writing.
To do this you can run:
```bash
cd ~/<repository-path>/
npx techdocs-cli serve
cd /path/to/docs-repository/
npx @techdocs/cli serve
```
+17 -33
View File
@@ -6,35 +6,8 @@ title: Getting Started
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. Its based on the
developer portal weve been using internally at Spotify for over four years.
[Read more here](https://github.com/spotify/backstage).
## Prerequisities
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
- [Yarn](https://yarnpkg.com/getting-started/install)
## Creating a new Backstage app
> 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 TechDocs, run the following command:
```bash
npx @backstage/create-app
```
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.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
## Installing TechDocs
@@ -74,17 +47,28 @@ 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.
`app-config.yaml` file. We provide two different values to be configured,
`requestUrl` and `storageUrl`. The `requestUrl` is what the reader will request
its data from, and `storageUrl` is where the backend can find the stored
documentation.
The default storage URL:
The default storage and request URLs:
```yaml
techdocs:
storageUrl: http://localhost:7000/techdocs/static/docs
requestUrl: http://localhost:7000/techdocs/docs
```
If you want to configure this to point to another storage URL, change the value
of `storageUrl`.
If you want `techdocs-backend` to manage building and publishing, you want
`requestUrl` to point to the default value (or wherever `techdocs-backend` is
hosted). `storageUrl` should be where your publisher publishes your docs. Using
the default `LocalPublish` that is the default value.
If you have a setup where you are not using `techdocs-backend` for managing
building and publishing of your documentation, you want to change the
`requestUrl` to point to your storage. In this case `storageUrl` is not
required.
## Run Backstage locally
@@ -3,4 +3,34 @@ id: configure-app-with-plugins
title: Configuring App with plugins
---
## Adding existing plugins to your app
Coming soon!
### Adding a plugin page to the Sidebar
In a standard Backstage app created with
[@backstage/create-app](./create-an-app.md), the sidebar is managed inside
`packages/app/src/sidebar.tsx`. The file exports the entire `Sidebar` element of
your app, which you can extend with additional entries by adding new
`SidebarItem` elements.
For example, if you install the `api-docs` plugin, a matching `SidebarItem`
could be something like this:
```tsx
// Import icon from MUI
import ExtensionIcon from '@material-ui/icons/Extension';
// ... inside the AppSidebar component
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />;
```
You can also use your own SVGs directly as icon components. Just make sure they
are sized according to the Material UI's
[SvgIcon](https://material-ui.com/api/svg-icon/) default of 24x24px, and set the
extension to `.icon.svg`. For example:
```ts
import InternalToolIcon from './internal-tool.icon.svg';
```
+44 -33
View File
@@ -15,7 +15,13 @@ To create a Backstage app, you will need to have
[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12).
With `npx`:
Backstage provides a utility for creating new apps. It guides you through the
initial setup of selecting the name of the app and a database for the backend.
The database options are either SQLite or PostgreSQL, where the latter requires
you to set up a separate database instance. If in doubt, choose SQLite, but
don't worry about the choice, it's easy to change later!
The easiest way to run the create app package is with `npx`:
```bash
npx @backstage/create-app
@@ -25,50 +31,45 @@ This will create a new Backstage App inside the current folder. The name of the
app-folder is the name that was provided when prompted.
<p align='center'>
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/create-app_output.png' width='600' alt='create app'>
<img src='../assets/getting-started/create-app_output.png' width='600' alt='create app'>
</p>
Inside that directory, it will generate all the files and folder structure
needed for you to run your app.
### Folder structure
### General folder structure
Below is a simplified layout of the files and folders generated when creating an
app.
```
app
├── README.md
├── app-config.yaml
├── lerna.json
├── package.json
── prettier.config.js
├── tsconfig.json
── packages
│ └── app
│ ├── package.json
│ ├── tsconfig.json
│ ├── public
│ │ └── ...
│ └── src
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.tsx
│ ├── plugins.ts
│ └── setupTests.ts
└── plugins
└── welcome
├── README.md
├── package.json
├── tsconfig.json
└── src
├── index.ts
├── plugin.test.ts
├── plugin.ts
├── setupTests.ts
└── components
├── Timer
│ └── ...
└── WelcomePage
└── ...
── packages
  ├── app
  ── backend
```
- **app-config.yaml**: Main configuration file for the app. See
[Configuration](https://backstage.io/docs/conf/) for more information.
- **lerna.json**: Contains information about workspaces and other lerna
configuration needed for the monorepo setup.
- **package.json**: Root package.json for the project. _Note: Be sure that you
don't add any npm dependencies here as they probably should be installed in
the intended workspace rather than in the root._
- **packages/**: Lerna leaf packages or "workspaces". Everything here is going
to be a separate package, managed by lerna.
- **packages/app/**: An fully functioning Backstage frontend app, that acts as a
good starting point for you to get to know Backstage.
- **packages/backend/**: We include a backend that helps power features such as
[Authentication](https://backstage.io/docs/auth/),
[Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview),
[Software Templates](https://backstage.io/docs/features/software-templates/software-templates-index)
and [TechDocs](https://backstage.io/docs/features/techdocs/techdocs-overview)
amongst other things.
## Run the app
When the installation is complete you can open the app folder and start the app.
@@ -80,3 +81,13 @@ yarn start
_When `yarn start` is ready it should open up a browser window displaying your
app, if not you can navigate to `http://localhost:3000`._
In most cases you will want to start the backend as well, as it is required for
the catalog to work, along with many other plugins.
To start the backend, open a separate terminal session and run the following in
the root directory:
```bash
yarn workspace backend start
```
+38 -72
View File
@@ -1,84 +1,50 @@
---
id: index
title: Running Backstage Locally
title: Getting Started
---
First make sure you are using NodeJS with an Active LTS Release, currently v12.
This is made easy with a version manager such as nvm which allows for version
switching.
There are two different ways to get started with Backstage, either by creating a
standalone app, or by cloning this repo. Which method you use depends on what
you're planning to do.
Creating a standalone instance makes it simpler to customize the application for
your needs whilst staying up to date with the project. You will also depend on
`@backstage` packages from NPM, making the project much smaller. This is the
recommended approach if you want to kick the tyres of Backstage or setup your
own instance.
On the other hand, if you want to contribute plugins or to the project in
general, it's easier to fork and clone this project. That will let you stay up
to date with the latest changes, and gives you an easier path to make Pull
Requests towards this repo.
### Creating a Standalone App
Backstage provides the `@backstage/create-app` package to scaffold standalone
instances of Backstage. You will need to have
[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You
will also need to have [Docker](https://docs.docker.com/engine/install/)
installed to use some features like Software Templates and TechDocs.
Using `npx` you can then run the following to create an app in a chosen
subdirectory of your current working directory:
```bash
# Checking your version
node --version
> v14.7.0
# Adding a second node version
nvm install 12
> Downloading and installing node v12.18.3...
> Now using node v12.18.3 (npm v6.14.6)
npx @backstage/create-app
```
To get up and running with a local Backstage to evaluate it, let's clone it off
of GitHub and run an initial build.
You will be taken through a wizard to create your app, and the output should
look something like this. You can read more about this process
[here](https://backstage.io/docs/getting-started/create-an-app).
```bash
# Start from your local development folder
git clone git@github.com:spotify/backstage.git
cd backstage
### Contributing to Backstage
# Fetch our dependencies and run an initial build
yarn install
yarn tsc
yarn build
```
You can read more in our
[CONTRIBUTING](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)
guide, which can help you get setup with a Backstage development environment.
Phew! Now you have a local repository that's ready to run and to add any open
source contributions into.
### Next steps
We are now going to launch two things: an example Backstage frontend app, and an
example Backstage backend that the frontend talks to. You are going to need two
terminal windows, both starting from the Backstage project root.
In the first window, run
```bash
cd packages/backend
yarn start
```
That starts up a backend instance on port 7000.
In the other window, we will first populate the catalog with some nice mock data
to look at, and then launch the frontend. These commands are run from the
project root, not inside the backend directory.
```bash
yarn lerna run mock-data
yarn start
```
That starts up the frontend on port 3000, and should automatically open a
browser window showing it.
Congratulations! That should be it. Let us know how it went
[on discord](https://discord.gg/EBHEGzX), file issues for any
[feature](https://github.com/spotify/backstage/issues/new?labels=help+wanted&template=feature_template.md)
or
[plugin suggestions](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME),
or
[bugs](https://github.com/spotify/backstage/issues/new?labels=bug&template=bug_template.md)
you have, and feel free to
[contribute](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)!
## Creating a Plugin
The value of Backstage grows with every new plugin that gets added. Here is a
collection of tutorials that will guide you through setting up and extending an
instance of Backstage with your own plugins.
- [Development Environment](development-environment.md)
- [Create a Backstage Plugin](../plugins/create-a-plugin.md)
- [Structure of a Plugin](../plugins/structure-of-a-plugin.md)
- [Utility APIs](../api/utility-apis.md)
[Back to Docs](../README.md)
Take a look at the [Running Backstage Locally](./running-backstage-locally.md)
guide to learn how to set up Backstage, and how to develop on the platform.
@@ -0,0 +1,109 @@
---
id: running-backstage-locally
title: Running Backstage Locally
---
## Prerequisites
- Node.js
First make sure you are using NodeJS with an Active LTS Release, currently v12.
This is made easy with a version manager such as
[nvm](https://github.com/nvm-sh/nvm) which allows for version switching.
```bash
# Installing a new version
nvm install 12
> Downloading and installing node v12.18.3...
> Now using node v12.18.3 (npm v6.14.6)
# Checking your version
node --version
> v12.18.3
```
- yarn
Please refer to the
[installation instructions for yarn](https://classic.yarnpkg.com/en/docs/install/).
- Docker
We use Docker for few of our core features. So, you will need Docker installed
locally to use features like Software Templates and TechDocs. Please refer to
the
[installation instructions for Docker](https://docs.docker.com/engine/install/).
## Clone and Build
To get up and running with a local Backstage to evaluate it, let's clone it off
of GitHub and run an initial build.
```bash
# Start from your local development folder
git clone git@github.com:spotify/backstage.git
cd backstage
# Fetch our dependencies and run an initial build
yarn install
yarn tsc
yarn build
```
Phew! Now you have a local repository that's ready to run and to add any open
source contributions into.
We are now going to launch two things: an example Backstage frontend app, and an
example Backstage backend that the frontend talks to. You are going to need two
terminal windows, both starting from the Backstage project root.
In the first window, run
```bash
cd packages/backend
yarn start
```
That starts up a backend instance on port 7000.
In the other window, we will then launch the frontend. This command is run from
the project root, not inside the backend directory.
```bash
yarn start
```
That starts up the frontend on port 3000, and should automatically open a
browser window showing it.
## Authentication
When Backstage starts, you can choose to enter as a Guest user and start
exploring.
But you can also set up any of the available authentication methods. The easiest
option will be GitHub. To setup GitHub authentication in Backstage, see
[these instructions](https://github.com/spotify/backstage/tree/master/plugins/auth-backend#github).
---
Congratulations! That should be it. Let us know how it went
[on discord](https://discord.gg/EBHEGzX), file issues for any
[feature](https://github.com/spotify/backstage/issues/new?labels=help+wanted&template=feature_template.md)
or
[plugin suggestions](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME),
or
[bugs](https://github.com/spotify/backstage/issues/new?labels=bug&template=bug_template.md)
you have, and feel free to
[contribute](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)!
## Creating a Plugin
The value of Backstage grows with every new plugin that gets added. Here is a
collection of tutorials that will guide you through setting up and extending an
instance of Backstage with your own plugins.
- [Development Environment](development-environment.md)
- [Create a Backstage Plugin](../plugins/create-a-plugin.md)
- [Structure of a Plugin](../plugins/structure-of-a-plugin.md)
- [Utility APIs](../api/utility-apis.md)
+41
View File
@@ -0,0 +1,41 @@
---
id: logos
title: Logos
sidebar_label: Logo assets
---
Guidelines for how to use the Backstage logo and icon can be found
[here](/logo_assets/Backstage_Identity_Assets_Overview.pdf). The assets below
are all in `.svg` format. Other formats are available in the
[repository](https://github.com/spotify/backstage/tree/master/microsite/static/logo_assets).
## Backstage logo
<a href="/logo_assets/svg/Logo_White.svg">
<img src="/logo_assets/svg/Logo_White.svg" width="600" />
</a>
<a href="/logo_assets/svg/Logo_Teal.svg">
<img src="/logo_assets/svg/Logo_Teal.svg" width="600" />
</a>
<a href="/logo_assets/svg/Logo_Black.svg">
<img src="/logo_assets/svg/Logo_Black.svg" width="600" style="background-color:white" />
</a>
## Backstage icon
<div>
<a href="/logo_assets/svg/Icon_White.svg">
<img src="/logo_assets/svg/Icon_White.svg" width="180" height="180" />
</a>
<a href="/logo_assets/svg/Icon_Teal.svg">
<img src="/logo_assets/svg/Icon_Teal.svg" width="180" height="180" />
</a>
<a href="/logo_assets/svg/Icon_Gradient.svg">
<img src="/logo_assets/svg/Icon_Gradient.svg" width="180" height="180" />
</a>
<a href="/logo_assets/svg/Icon_Black.svg">
<img src="/logo_assets/svg/Icon_Black.svg" width="180" height="180" style="background-color:white" />
</a>
</div>
+4 -1
View File
@@ -52,7 +52,10 @@ configuration will lead to the proxy acting on backend requests to
The value inside each route is either a simple URL string, or an object on the
format accepted by
[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware).
[http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). It
is also possible to limit the forwarded HTTP methods with the configuration
`allowedMethods`, for example `allowedMethods: ['GET']` to enforce read-only
access.
If the value is a string, it is assumed to correspond to:
+8 -1
View File
@@ -53,6 +53,11 @@ async function verifyUrl(basePath, url) {
return { url, basePath, problem: 'not-relative' };
}
const staticPath = resolvePath(projectRoot, 'microsite/static', `.${url}`);
if (await fs.pathExists(staticPath)) {
return;
}
path = resolvePath(projectRoot, `.${url}`);
} else {
path = resolvePath(dirname(resolvePath(projectRoot, basePath)), url);
@@ -103,7 +108,9 @@ async function main() {
console.log(`Found ${badUrls.length} bad links within repo`);
for (const { url, basePath, problem } of badUrls) {
if (problem === 'missing') {
console.error(`Unable to reach ${url}, linked from ${basePath}`);
console.error(
`Unable to reach ${url} from root or microsite/static/, linked from ${basePath}`,
);
} else if (problem === 'not-relative') {
console.error('Links to /docs/ must be relative');
console.error(` From: ${basePath}`);
@@ -39,7 +39,7 @@ You can customize Backstage Software Templates to fit your organizations stan
## Getting started
The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`. If youve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first.
The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`.
![available-templates](assets/2020-08-05/templates.png)
@@ -6,6 +6,9 @@ authorURL: https://github.com/garyniemen
Since we [open sourced Backstage](https://backstage.io/blog/2020/03/16/announcing-backstage), one of the most requested features has been for a technical documentation plugin. Well, good news. The first open source version of TechDocs is here. Now lets start collaborating and making it better, together.
<iframe width="780" height="440" src="https://www.youtube.com/embed/mOLCgdPw1iA" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
<!--truncate-->
Internally, we call it TechDocs. Its the most used plugin at Spotify by far — accounting for about 20% of our Backstage traffic (even though it is just one of 130+ plugins). Its popularity is evidence of something simple: We made documentation so easy to create, find, and use — people actually use it.
@@ -14,8 +17,6 @@ We are quite sure the main reason for the success of TechDocs is our docs-like-c
![available-templates](assets/announcing-techdocs/docs-in-backstage.png)
Watch this video to see [Spotifys internal version of TechDocs](https://backstage.io/demos) in action.
But this is just one way to do it. Today were most excited for what the open version of TechDocs can become.
## Okay, lets start collaborating
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 262 KiB

+1 -1
View File
@@ -13,7 +13,7 @@
"rename-version": "docusaurus-rename-version"
},
"devDependencies": {
"docusaurus": "^2.0.0-alpha.61",
"docusaurus": "^2.0.0-alpha.64",
"js-yaml": "^3.14.0"
}
}
+3 -1
View File
@@ -81,7 +81,9 @@ const Background = props => {
<Block className="stripe-bottom bg-black-grey">
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.TextBox>
<Block.Title>Make documentation easy</Block.Title>
<Block.Title id="techdocs-demo">
Make documentation easy
</Block.Title>
<Block.Paragraph>
Documentation! Everyone needs it, no one wants to create it, and
no one can ever find it. Backstage follows a docs like code
+8 -2
View File
@@ -7,10 +7,12 @@
"overview/roadmap",
"overview/vision",
"overview/background",
"overview/adopting"
"overview/adopting",
"overview/logos"
],
"Getting Started": [
"getting-started/index",
"getting-started/running-backstage-locally",
"getting-started/installation",
"getting-started/development-environment",
"getting-started/create-an-app",
@@ -37,8 +39,10 @@
"label": "Software Catalog",
"ids": [
"features/software-catalog/software-catalog-overview",
"features/software-catalog/installation",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/well-known-annotations",
"features/software-catalog/extending-the-model",
"features/software-catalog/external-integrations",
"features/software-catalog/software-catalog-api"
@@ -49,6 +53,7 @@
"label": "Software Templates",
"ids": [
"features/software-templates/software-templates-index",
"features/software-templates/installation",
"features/software-templates/adding-templates",
"features/software-templates/extending/extending-index",
"features/software-templates/extending/extending-templater",
@@ -58,11 +63,12 @@
},
{
"type": "subcategory",
"label": "Docs-like-code",
"label": "TechDocs",
"ids": [
"features/techdocs/techdocs-overview",
"features/techdocs/getting-started",
"features/techdocs/concepts",
"features/techdocs/architecture",
"features/techdocs/creating-and-publishing",
"features/techdocs/faqs"
]
+7 -4
View File
@@ -67,8 +67,9 @@ const siteConfig = {
primaryColor: '#36BAA2',
secondaryColor: '#121212',
textColor: '#FFFFFF',
navigatorTitleTextColor: '#9e9e9e',
navigatorItemTextColor: '#616161',
navigatorTitleTextColor: '#e4e4e4',
navigatorItemTextColor: '#9e9e9e',
navGroupSubcategoryTitleColor: '#9e9e9e',
},
/* Colors for syntax highlighting */
@@ -93,8 +94,10 @@ const siteConfig = {
cleanUrl: true,
// Open Graph and Twitter card images.
ogImage: 'img/logo-gradient-on-dark.svg',
twitterImage: 'img/logo-gradient-on-dark.svg',
ogImage:
'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png',
twitterImage:
'logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png',
// For sites with a sizable amount of content, set collapsible to true.
// Expand/collapse the links and subcategories under categories.
+55 -3
View File
@@ -113,6 +113,10 @@ td {
color: $navigatorTitleTextColor;
}
.toc .toggleNav .navGroup .navGroupSubcategoryTitle {
color: $navGroupSubcategoryTitleColor;
}
.toc .toggleNav ul li a,
.onPageNav a {
color: $navigatorItemTextColor;
@@ -203,6 +207,54 @@ td {
border-radius: 0.25rem;
}
/*
* Fix for viewing Table of Contents bar on documentation
* and blog pages on smaller screens.
*/
@media only screen and (max-width: 1023px) {
/* Nav bar hides the docs toc bar */
.docMainWrapper {
margin-top: 4rem;
}
/* Toc bar does not have to be fixed */
.docsNavContainer {
position: unset;
width: 95vw;
z-index: 100;
margin-left: -2vw;
}
/* Toc bar does not have to be fixed when slider is active */
.docsSliderActive .toc .navBreadcrumb,
.tocActive .navBreadcrumb {
position: unset;
}
/* Fix unexpected width increase when toc is toggled */
.docsSliderActive .toc .navBreadcrumb,
.tocActive .navBreadcrumb {
width: inherit;
}
/* This pseudo-element stops toc toggle button to be clicked */
header.postHeader::before {
height: 2em !important;
margin-top: -2em !important;
}
/* This pseudo-element stops toc toggle button to be clicked */
#__docusaurus.postHeaderTitle::before {
height: 1em;
margin-top: -1em;
}
/* Useless button causing trouble */
.tocToggler {
display: none;
}
}
/* content */
.postContainer blockquote {
color: $textColor;
@@ -288,17 +340,17 @@ code {
background: linear-gradient(70.44deg, #121212 75%, #5d817b 100%);
}
.bg-teal-top-right {
background:
background:
/* linear-gradient(
178.64deg,
rgba(255, 255, 255, 0) 57.61%,
rgba(255, 255, 255, 0.17) 127.71%
), */
), */
/* linear-gradient(
144.35deg,
rgba(98, 197, 179, 0) 56.68%,
rgba(98, 197, 179, 0.59) 109.25%
), */
), */
/* linear-gradient(
192.29deg,
rgba(155, 240, 225, 0) 54.17%,
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

@@ -0,0 +1 @@
<svg id="Assets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 337.46 428.5"><defs><style>.cls-1{fill:#121212;}</style></defs><title>06 Icon_Black</title><path class="cls-1" d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1 @@
<svg id="Assets" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 982.38 1247.39"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="392.22" y1="1235.97" x2="609.18" y2="5.5" gradientUnits="userSpaceOnUse"><stop offset="0.07" stop-color="#36baa2"/><stop offset="0.34" stop-color="#61ddc8"/><stop offset="0.53" stop-color="#7df3e1"/></linearGradient></defs><title>07 Large Icon_Gradient</title><path class="cls-1" d="M702.28,857.87c-57,54.61-134.4,84.68-218,84.68-64.94,0-129.45-18.31-186.61-53L0,708.94v71.84L316.14,972.65c53.11,32.19,111.56,47.78,168.15,47.78,72.55,0,142.06-25.62,193.35-74.77,37.49-35.93,58.2-79,57.29-120.8v-5.2A253.66,253.66,0,0,1,702.28,857.87Zm0-113.47c-57,54.6-134.4,84.68-218,84.68-64.94,0-129.45-18.32-186.61-53L0,595.47V667.3L316.14,859.16C369.25,891.36,427.7,907,484.29,907c72.55,0,142.06-25.62,193.35-74.77,37.49-35.93,58.2-79,57.29-120.79v-5.31A252.11,252.11,0,0,1,702.28,744.4Zm0,227c-57,54.6-134.4,84.68-218,84.68-64.94,0-129.45-18.32-186.61-53L0,822.42v71.84l316.14,191.86c53.11,32.2,111.56,47.79,168.15,47.79,72.55,0,142.06-25.63,193.35-74.78,37.49-35.92,58.2-79,57.29-120.78v-5.22A253.79,253.79,0,0,1,702.28,971.36Zm0,113.48c-57,54.6-134.4,84.68-218,84.68-64.94,0-129.45-18.32-186.61-53L0,935.9v71.84L316.14,1199.6c53.11,32.2,111.56,47.79,168.15,47.79,72.55,0,142.06-25.62,193.35-74.78,37.49-35.92,58.2-79,57.29-120.78v-5.21A253.73,253.73,0,0,1,702.28,1084.84ZM951.79,586.27c-2,2-3.79,4.1-5.88,6.09A283.08,283.08,0,0,1,881.13,638a278.45,278.45,0,0,1-110.44,29.63h-.15l0,77.63h.11C825,741.78,878.61,721,921.28,680.14c2.33-2.24,4.38-4.54,6.59-6.8A239.4,239.4,0,0,0,951,646a185,185,0,0,0,18.29-31.37,141.27,141.27,0,0,0,12.57-47.87c.27-3.6.6-7.2.56-10.79v-6.38c-2.17,3.12-4.26,6.26-6.61,9.36A284.07,284.07,0,0,1,951.79,586.27Zm.1-113.36c-2,2-3.92,4-6,6a288.89,288.89,0,0,1-64.72,45.66,276.05,276.05,0,0,1-76.4,25.37,287,287,0,0,1-41.23,4.55c1.48,4.7,2.63,9.47,3.64,14.27a155,155,0,0,1,3.32,34.94v28.1h.16a245.44,245.44,0,0,0,73.78-16.15,235.88,235.88,0,0,0,76.81-49c2.33-2.24,4.38-4.53,6.59-6.8A237.77,237.77,0,0,0,951,532.55a184.13,184.13,0,0,0,18.29-31.36,141.22,141.22,0,0,0,12.57-47.86c.27-3.6.6-7.2.56-10.8v-6c-2.09,3-4.29,6-6.56,9A285.06,285.06,0,0,1,951.89,472.91Zm-.1,226.84c-2,2-3.8,4.09-5.88,6.09C898.8,751,837.08,777.38,770.69,781.11h-.09l0,77.63h.06c54.26-3.48,107.92-24.23,150.59-65.12,21.84-20.92,37.77-43,48-65.47a141.28,141.28,0,0,0,12.57-47.86c.27-3.61.6-7.21.56-10.8V663.1c-2.17,3.13-4.26,6.27-6.61,9.36A283,283,0,0,1,951.79,699.75ZM770.69,894.59h0l0,77.63C825,968.74,878.61,948,921.28,907.1c40.67-39,61.55-81.83,61.09-124.13v-6.4a276.79,276.79,0,0,1-36.46,42.76C898.79,864.47,837.08,890.86,770.69,894.59ZM878.3,182.25,578,0,227.93,335.43,0,553.82,316.14,745.68c53.11,32.2,111.56,47.79,168.15,47.79,72.55,0,142.06-25.62,193.35-74.77,55.78-53.45,74.29-122.64,39.87-180.11a142.23,142.23,0,0,0-19.79-25.82A260,260,0,0,0,753.87,519h.43a247.6,247.6,0,0,0,90.17-16.86,235.71,235.71,0,0,0,76.81-49c2.32-2.21,4.52-4.45,6.7-6.69a238.54,238.54,0,0,0,23.08-27.3,184.08,184.08,0,0,0,18.25-31.34A141.52,141.52,0,0,0,982,340.11C986.32,283.66,953,227.53,878.3,182.25Zm-289,475.23C533.09,711.36,442.61,719.3,369,674.66L150.25,541.89,346.58,353.71,569,488.74C649.19,537.34,644.77,604.36,589.33,657.48Zm240.5-261.25C776,447.8,689,462.79,615.33,418.14L409.67,293.29l187.5-179.81L799.11,236.07C874.62,281.85,884.43,343.9,829.83,396.23Z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1 @@
<svg id="Assets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 337.46 428.5"><defs><style>.cls-1{fill:#7df3e1;}</style></defs><title>04 Icon_Teal</title><path class="cls-1" d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1 @@
<svg id="Assets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 337.46 428.5"><defs><style>.cls-1{fill:#fff;}</style></defs><title>02 Icon_White</title><path class="cls-1" d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

+4 -4
View File
@@ -2228,10 +2228,10 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
docusaurus@^2.0.0-alpha.61:
version "2.0.0-alpha.63"
resolved "https://registry.npmjs.org/docusaurus/-/docusaurus-2.0.0-alpha.63.tgz#40402d47b18c42b62e93beb78ce06e000cb88312"
integrity sha512-R19pAqcTemJMt7Qykd7ogB2i6R86vbE4/vA/l5/Uuh7xg7ixSKOVZ5M7d9uSoX5jCALOwEyp5iJdleznXUVwAw==
docusaurus@^2.0.0-alpha.64:
version "2.0.0-alpha.64"
resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.64.tgz#7833960e9d338403894a27b79058aa4076a676d3"
integrity sha512-ARCx0GwAvc5qx7AHvRVZidZuoDTfaaGXzgmkU23NahU6jzO/aK2Q1bH8IKNEQ5C2JuDerQ/hHDh80N20ijk82g==
dependencies:
"@babel/core" "^7.9.0"
"@babel/plugin-proposal-class-properties" "^7.8.3"
+7 -2
View File
@@ -10,8 +10,10 @@ nav:
- Vision: 'overview/vision.md'
- The Spotify story: 'overview/background.md'
- Strategies for adopting: 'overview/adopting.md'
- Logo assets: 'overview/logos.md'
- Getting started:
- Running Backstage locally: 'getting-started/index.md'
- Getting Started: 'getting-started/index.md'
- Running Backstage locally: 'getting-started/running-backstage-locally.md'
- Installation: 'getting-started/installation.md'
- Local development: 'getting-started/development-environment.md'
- Demo deployment: https://backstage-demo.roadie.io
@@ -28,22 +30,25 @@ nav:
- Overview: 'features/software-catalog/index.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.md'
- Configuration: 'features/software-catalog/configuration.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- API: 'features/software-catalog/api.md'
- Software creation templates:
- Overview: 'features/software-templates/index.md'
- Installation: 'features/software-templates/installation.md'
- Adding templates: 'features/software-templates/adding-templates.md'
- Extending the Scaffolder:
- Overview: 'features/software-templates/extending/index.md'
- Create your own Templater: 'features/software-templates/extending/create-your-own-templater.md'
- Create your own Publisher: 'features/software-templates/extending/create-your-own-publisher.md'
- Create your own Preparer: 'features/software-templates/extending/create-your-own-preparer.md'
- Docs-like-code:
- TechDocs:
- Overview: 'features/techdocs/README.md'
- Getting Started: 'features/techdocs/getting-started.md'
- Concepts: 'features/techdocs/concepts.md'
- TechDocs Architecture: 'features/techdocs/architecture.md'
- Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md'
- FAQ: 'features/techdocs/FAQ.md'
- Plugins:
+14 -1
View File
@@ -19,6 +19,7 @@ import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
createRouteRef,
} from '@backstage/core';
import React, { FC } from 'react';
import Root from './components/Root';
@@ -30,6 +31,8 @@ import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -55,11 +58,16 @@ const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const deprecatedAppRoutes = app.getRoutes();
const catalogRouteRef = createRouteRef({
path: '/catalog',
title: 'Service Catalog',
});
const AppRoutes = () => (
<Routes>
<Navigate key="/" to="/catalog" />
<Route
path="/catalog/*"
path={`${catalogRouteRef.path}/*`}
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs/*" element={<DocsRouter />} />
@@ -68,6 +76,11 @@ const AppRoutes = () => (
element={<TechRadarRouter width={1500} height={800} />}
/>
<Route path="/graphiql" element={<GraphiQLRouter />} />
<Route path="/lighthouse/*" element={<LighthouseRouter />} />
<Route
path="/register-component"
element={<RegisterComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
{...deprecatedAppRoutes}
</Routes>
);
+6 -14
View File
@@ -43,21 +43,13 @@ 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.
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. For more information, see
[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog).
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.
For convenience we already include some statically configured example locations
in `app-config.yaml` under `catalog.locations`. For local development you can override these in your own `app-config.local.yaml`.
## Authentication
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: example-apis
description: A collection of all Backstage example APIs
spec:
type: github
targets:
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml
@@ -0,0 +1,16 @@
apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
name: example-components
description: A collection of all Backstage example components
spec:
type: github
targets:
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
- https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
+1 -1
View File
@@ -41,7 +41,7 @@
"@spotify/eslint-config": "^7.0.1",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "5.4.x",
"@svgr/plugin-svgo": "4.3.x",
"@svgr/plugin-svgo": "5.4.x",
"@svgr/rollup": "5.4.x",
"@svgr/webpack": "5.4.x",
"@types/start-server-webpack-plugin": "^2.2.0",
@@ -299,7 +299,11 @@ export const microsoftAuthApiRef = createApiRef<
* Provides authentication for custom identity providers.
*/
export const oauth2ApiRef = createApiRef<
OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
SessionStateApi &
BackstageIdentityApi
>({
id: 'core.auth.oauth2',
description: 'Example of how to use oauth2 custom provider',
@@ -1,36 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Auth0Auth from './Auth0Auth';
describe('Auth0Auth', () => {
it('should normalize scope', () => {
const tests = [
{
arguments: ['read_user api write_repository'],
expect: new Set(['read_user', 'api', 'write_repository']),
},
{
arguments: ['read_repository sudo'],
expect: new Set(['read_repository', 'sudo']),
},
];
for (const test of tests) {
expect(Auth0Auth.normalizeScopes(...test.arguments)).toEqual(test.expect);
}
});
});
@@ -15,26 +15,13 @@
*/
import Auth0Icon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { Auth0Session } from './types';
import {
OpenIdConnectApi,
ProfileInfoApi,
ProfileInfo,
SessionStateApi,
SessionState,
BackstageIdentityApi,
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { auth0AuthApiRef } from '../../../definitions/auth';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { OAuth2 } from '../oauth2';
type CreateOptions = {
discoveryApi: DiscoveryApi;
@@ -44,106 +31,27 @@ type CreateOptions = {
provider?: AuthProvider & { id: string };
};
export type Auth0AuthResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'auth0',
title: 'Auth0',
icon: Auth0Icon,
};
class Auth0Auth
implements
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
class Auth0Auth {
static create({
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
}: CreateOptions): typeof auth0AuthApiRef.T {
return OAuth2.create({
discoveryApi,
environment,
oauthRequestApi,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: Auth0AuthResponse): Auth0Session {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: Auth0Auth.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
environment,
defaultScopes: ['openid', `email`, `profile`],
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set(['openid', `email`, `profile`]),
sessionScopes: (session: Auth0Session) => session.providerInfo.scopes,
sessionShouldRefresh: (session: Auth0Session) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});
return new Auth0Auth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<Auth0Session>) {}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scope?: string | string[]): Set<string> {
if (!scope) {
return new Set();
}
const scopeList = Array.isArray(scope)
? scope
: scope.split(/[\s|,]/).filter(Boolean);
return new Set(scopeList);
}
}
export default Auth0Auth;
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './types';
export { default as Auth0Auth } from './Auth0Auth';
@@ -14,33 +14,37 @@
* limitations under the License.
*/
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import GitlabAuth from './GitlabAuth';
describe('GitlabAuth', () => {
it('should get access token', async () => {
const getSession = jest
.fn()
.mockResolvedValue({ providerInfo: { accessToken: 'access-token' } });
const gitlabAuth = new GitlabAuth({ getSession } as any);
const getSession = jest.fn();
expect(await gitlabAuth.getAccessToken()).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
jest.mock('../../../../lib/AuthSessionManager', () => ({
...(jest.requireActual('../../../../lib/AuthSessionManager') as any),
RefreshingAuthSessionManager: class {
getSession = getSession;
},
}));
describe('GitlabAuth', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should normalize scope', () => {
const tests = [
{
arguments: ['read_user api write_repository'],
expect: new Set(['read_user', 'api', 'write_repository']),
},
{
arguments: ['read_repository sudo'],
expect: new Set(['read_repository', 'sudo']),
},
];
it.each([
[
'read_user api write_repository',
['read_user', 'api', 'write_repository'],
],
['read_repository sudo', ['read_repository', 'sudo']],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
const googleAuth = GitlabAuth.create({
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
for (const test of tests) {
expect(GitlabAuth.normalizeScope(...test.arguments)).toEqual(test.expect);
}
googleAuth.getAccessToken(scope);
expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) });
});
});
@@ -15,24 +15,13 @@
*/
import GitlabIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { GitlabSession } from './types';
import {
OAuthApi,
SessionStateApi,
SessionState,
ProfileInfo,
BackstageIdentity,
AuthRequestOptions,
} from '../../../definitions/auth';
import { gitlabAuthApiRef } from '../../../definitions/auth';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { OAuth2 } from '../oauth2';
type CreateOptions = {
discoveryApi: DiscoveryApi;
@@ -42,94 +31,26 @@ type CreateOptions = {
provider?: AuthProvider & { id: string };
};
export type GitlabAuthResponse = {
providerInfo: {
accessToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'gitlab',
title: 'Gitlab',
icon: GitlabIcon,
};
class GitlabAuth implements OAuthApi, SessionStateApi {
class GitlabAuth {
static create({
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
}: CreateOptions): typeof gitlabAuthApiRef.T {
return OAuth2.create({
discoveryApi,
environment,
provider,
oauthRequestApi,
sessionTransform(res: GitlabAuthResponse): GitlabSession {
return {
...res,
providerInfo: {
accessToken: res.providerInfo.accessToken,
scopes: GitlabAuth.normalizeScope(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
provider,
environment,
defaultScopes: ['read_user'],
});
const sessionManager = new StaticAuthSessionManager({
connector,
defaultScopes: new Set(['read_user']),
sessionScopes: (session: GitlabSession) => session.providerInfo.scopes,
});
return new GitlabAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GitlabSession>) {}
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
const session = await this.sessionManager.getSession({
...options,
scopes: GitlabAuth.normalizeScope(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
async logout() {
await this.sessionManager.removeSession();
}
static normalizeScope(scope?: string): Set<string> {
if (!scope) {
return new Set();
}
const scopeList = Array.isArray(scope) ? scope : scope.split(' ');
return new Set(scopeList);
}
}
export default GitlabAuth;
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './types';
export { default as GitlabAuth } from './GitlabAuth';
@@ -15,101 +15,23 @@
*/
import GoogleAuth from './GoogleAuth';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
const PREFIX = 'https://www.googleapis.com/auth/';
const getSession = jest.fn();
jest.mock('../../../../lib/AuthSessionManager', () => ({
...(jest.requireActual('../../../../lib/AuthSessionManager') as any),
RefreshingAuthSessionManager: class {
getSession = getSession;
},
}));
describe('GoogleAuth', () => {
it('should get refreshed access token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const googleAuth = new GoogleAuth({ getSession } as any);
expect(await googleAuth.getAccessToken()).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get refreshed id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const googleAuth = new GoogleAuth({ getSession } as any);
expect(await googleAuth.getIdToken()).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get optional id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const googleAuth = new GoogleAuth({ getSession } as any);
expect(await googleAuth.getIdToken({ optional: true })).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should share popup closed errors', async () => {
const error = new Error('NOPE');
error.name = 'RejectedError';
const getSession = jest
.fn()
.mockResolvedValueOnce({
providerInfo: {
accessToken: 'access-token',
expiresAt: theFuture,
scopes: new Set([`${PREFIX}not-enough`]),
},
})
.mockRejectedValue(error);
const googleAuth = new GoogleAuth({ getSession } as any);
// Make sure we have a session before we do the double request, so that we get past the !this.currentSession check
await expect(googleAuth.getAccessToken()).resolves.toBe('access-token');
const promise1 = googleAuth.getAccessToken('more');
const promise2 = googleAuth.getAccessToken('more');
await expect(promise1).rejects.toBe(error);
await expect(promise2).rejects.toBe(error);
expect(getSession).toBeCalledTimes(3);
});
it('should wait for all session refreshes', async () => {
const initialSession = {
providerInfo: {
idToken: 'token1',
expiresAt: theFuture,
scopes: new Set(),
},
};
const getSession = jest
.fn()
.mockResolvedValueOnce(initialSession)
.mockResolvedValue({
providerInfo: {
idToken: 'token2',
expiresAt: theFuture,
scopes: new Set(),
},
});
const googleAuth = new GoogleAuth({ getSession } as any);
// Grab the expired session first
await expect(googleAuth.getIdToken()).resolves.toBe('token1');
expect(getSession).toBeCalledTimes(1);
initialSession.providerInfo.expiresAt = thePast;
const promise1 = googleAuth.getIdToken();
const promise2 = googleAuth.getIdToken();
const promise3 = googleAuth.getIdToken();
await expect(promise1).resolves.toBe('token2');
await expect(promise2).resolves.toBe('token2');
await expect(promise3).resolves.toBe('token2');
expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client
afterEach(() => {
jest.resetAllMocks();
});
it.each([
@@ -136,6 +58,12 @@ describe('GoogleAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
expect(GoogleAuth.normalizeScopes(scope)).toEqual(new Set(scopes));
const googleAuth = GoogleAuth.create({
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
googleAuth.getAccessToken(scope);
expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) });
});
});
@@ -15,27 +15,13 @@
*/
import GoogleIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { GoogleSession } from './types';
import {
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
ProfileInfo,
SessionStateApi,
SessionState,
BackstageIdentityApi,
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { googleAuthApiRef } from '../../../definitions/auth';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { OAuth2 } from '../oauth2';
type CreateOptions = {
discoveryApi: DiscoveryApi;
@@ -45,140 +31,49 @@ type CreateOptions = {
provider?: AuthProvider & { id: string };
};
export type GoogleAuthResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'google',
title: 'Google',
icon: GoogleIcon,
};
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
class GoogleAuth
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
class GoogleAuth {
static create({
discoveryApi,
oauthRequestApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: GoogleAuthResponse): GoogleSession {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: GoogleAuth.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
}: CreateOptions): typeof googleAuthApiRef.T {
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
return OAuth2.create({
discoveryApi,
oauthRequestApi,
provider,
environment,
defaultScopes: [
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
]),
sessionScopes: (session: GoogleSession) => session.providerInfo.scopes,
sessionShouldRefresh: (session: GoogleSession) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
],
scopeTransform(scopes: string[]) {
return scopes.map(scope => {
if (scope === 'openid') {
return scope;
}
if (scope === 'profile' || scope === 'email') {
return `${SCOPE_PREFIX}userinfo.${scope}`;
}
if (scope.startsWith(SCOPE_PREFIX)) {
return scope;
}
return `${SCOPE_PREFIX}${scope}`;
});
},
});
return new GoogleAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const session = await this.sessionManager.getSession({
...options,
scopes: GoogleAuth.normalizeScopes(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s]/).filter(Boolean);
const normalizedScopes = scopeList.map(scope => {
if (scope === 'openid') {
return scope;
}
if (scope === 'profile' || scope === 'email') {
return `${SCOPE_PREFIX}userinfo.${scope}`;
}
if (scope.startsWith(SCOPE_PREFIX)) {
return scope;
}
return `${SCOPE_PREFIX}${scope}`;
});
return new Set(normalizedScopes);
}
}
export default GoogleAuth;
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './types';
export { default as GoogleAuth } from './GoogleAuth';
@@ -15,29 +15,14 @@
*/
import MicrosoftIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { MicrosoftSession } from './types';
import {
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
ProfileInfo,
SessionStateApi,
SessionState,
BackstageIdentityApi,
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { microsoftAuthApiRef } from '../../../definitions/auth';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { OAuth2 } from '../oauth2';
type CreateOptions = {
discoveryApi: DiscoveryApi;
@@ -47,126 +32,33 @@ type CreateOptions = {
provider?: AuthProvider & { id: string };
};
export type MicrosoftAuthResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'microsoft',
title: 'Microsoft',
icon: MicrosoftIcon,
};
class MicrosoftAuth
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
class MicrosoftAuth {
static create({
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
discoveryApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
}: CreateOptions): typeof microsoftAuthApiRef.T {
return OAuth2.create({
discoveryApi,
environment,
oauthRequestApi,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: MicrosoftAuthResponse): MicrosoftSession {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: MicrosoftAuth.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
},
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
environment,
defaultScopes: [
'openid',
'offline_access',
'profile',
'email',
'User.Read',
]),
sessionScopes: (session: MicrosoftSession) => session.providerInfo.scopes,
sessionShouldRefresh: (session: MicrosoftSession) => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
],
});
return new MicrosoftAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(
private readonly sessionManager: SessionManager<MicrosoftSession>,
) {}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const session = await this.sessionManager.getSession({
...options,
scopes: MicrosoftAuth.normalizeScopes(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s|,]/).filter(Boolean);
return new Set(scopeList);
}
}
export default MicrosoftAuth;
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './types';
export { default as MicrosoftAuth } from './MicrosoftAuth';
@@ -0,0 +1,152 @@
/*
* 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 OAuth2 from './OAuth2';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
const PREFIX = 'https://www.googleapis.com/auth/';
const scopeTransform = (x: string[]) => x;
describe('OAuth2', () => {
it('should get refreshed access token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await oauth2.getAccessToken('my-scope my-scope2')).toBe(
'access-token',
);
expect(getSession).toBeCalledTimes(1);
expect(getSession.mock.calls[0][0].scopes).toEqual(
new Set(['my-scope', 'my-scope2']),
);
});
it('should transform scopes', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
});
expect(await oauth2.getAccessToken('my-scope')).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
expect(getSession.mock.calls[0][0].scopes).toEqual(
new Set(['my-prefix/my-scope']),
);
});
it('should get refreshed id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await oauth2.getIdToken()).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get optional id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform,
});
expect(await oauth2.getIdToken({ optional: true })).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should share popup closed errors', async () => {
const error = new Error('NOPE');
error.name = 'RejectedError';
const getSession = jest
.fn()
.mockResolvedValueOnce({
providerInfo: {
accessToken: 'access-token',
expiresAt: theFuture,
scopes: new Set([`${PREFIX}not-enough`]),
},
})
.mockRejectedValue(error);
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform,
});
// Make sure we have a session before we do the double request, so that we get past the !this.currentSession check
await expect(oauth2.getAccessToken()).resolves.toBe('access-token');
const promise1 = oauth2.getAccessToken('more');
const promise2 = oauth2.getAccessToken('more');
await expect(promise1).rejects.toBe(error);
await expect(promise2).rejects.toBe(error);
expect(getSession).toBeCalledTimes(3);
});
it('should wait for all session refreshes', async () => {
const initialSession = {
providerInfo: {
idToken: 'token1',
expiresAt: theFuture,
scopes: new Set(),
},
};
const getSession = jest
.fn()
.mockResolvedValueOnce(initialSession)
.mockResolvedValue({
providerInfo: {
idToken: 'token2',
expiresAt: theFuture,
scopes: new Set(),
},
});
const oauth2 = new OAuth2({
sessionManager: { getSession } as any,
scopeTransform,
});
// Grab the expired session first
await expect(oauth2.getIdToken()).resolves.toBe('token1');
expect(getSession).toBeCalledTimes(1);
initialSession.providerInfo.expiresAt = thePast;
const promise1 = oauth2.getIdToken();
const promise2 = oauth2.getIdToken();
const promise3 = oauth2.getIdToken();
await expect(promise1).resolves.toBe('token2');
await expect(promise2).resolves.toBe('token2');
await expect(promise3).resolves.toBe('token2');
expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client
});
});
@@ -33,15 +33,23 @@ import {
ProfileInfoApi,
SessionState,
SessionStateApi,
BackstageIdentityApi,
} from '../../../definitions/auth';
import { OAuth2Session } from './types';
type Options = {
sessionManager: SessionManager<OAuth2Session>;
scopeTransform: (scopes: string[]) => string[];
};
type CreateOptions = {
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
provider?: AuthProvider & { id: string };
defaultScopes?: string[];
scopeTransform?: (scopes: string[]) => string[];
};
export type OAuth2Response = {
@@ -61,15 +69,20 @@ const DEFAULT_PROVIDER = {
icon: OAuth2Icon,
};
const SCOPE_PREFIX = '';
class OAuth2
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
static create({
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
defaultScopes = [],
scopeTransform = x => x,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
discoveryApi,
@@ -82,7 +95,10 @@ class OAuth2
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OAuth2.normalizeScopes(res.providerInfo.scope),
scopes: OAuth2.normalizeScopes(
scopeTransform,
res.providerInfo.scope,
),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
@@ -93,11 +109,7 @@ class OAuth2
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
]),
defaultScopes: new Set(defaultScopes),
sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,
sessionShouldRefresh: (session: OAuth2Session) => {
const expiresInSec =
@@ -106,20 +118,26 @@ class OAuth2
},
});
return new OAuth2(sessionManager);
return new OAuth2({ sessionManager, scopeTransform });
}
private readonly sessionManager: SessionManager<OAuth2Session>;
private readonly scopeTransform: (scopes: string[]) => string[];
constructor(options: Options) {
this.sessionManager = options.sessionManager;
this.scopeTransform = options.scopeTransform;
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<OAuth2Session>) {}
async getAccessToken(
scope?: string | string[],
options?: AuthRequestOptions,
) {
const normalizedScopes = OAuth2.normalizeScopes(scope);
const normalizedScopes = OAuth2.normalizeScopes(this.scopeTransform, scope);
const session = await this.sessionManager.getSession({
...options,
scopes: normalizedScopes,
@@ -148,16 +166,19 @@ class OAuth2
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
private static normalizeScopes(
scopeTransform: (scopes: string[]) => string[],
scopes?: string | string[],
): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s]/).filter(Boolean);
: scopes.split(/[\s|,]/).filter(Boolean);
return new Set(scopeList);
return new Set(scopeTransform(scopeList));
}
}
@@ -13,102 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import OktaAuth from './OktaAuth';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
import OktaAuth from './OktaAuth';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
const PREFIX = 'okta.';
const getSession = jest.fn();
jest.mock('../../../../lib/AuthSessionManager', () => ({
...(jest.requireActual('../../../../lib/AuthSessionManager') as any),
RefreshingAuthSessionManager: class {
getSession = getSession;
},
}));
describe('OktaAuth', () => {
it('should get refreshed access token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oktaAuth = new OktaAuth({ getSession } as any);
expect(await oktaAuth.getAccessToken()).toBe('access-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get refreshed id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const oktaAuth = new OktaAuth({ getSession } as any);
expect(await oktaAuth.getIdToken()).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should get optional id token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const oktaAuth = new OktaAuth({ getSession } as any);
expect(await oktaAuth.getIdToken({ optional: true })).toBe('id-token');
expect(getSession).toBeCalledTimes(1);
});
it('should share popup closed errors', async () => {
const error = new Error('NOPE');
error.name = 'RejectedError';
const getSession = jest
.fn()
.mockResolvedValueOnce({
providerInfo: {
accessToken: 'access-token',
expiresAt: theFuture,
scopes: new Set([`not-a-scope`]),
},
})
.mockRejectedValue(error);
const oktaAuth = new OktaAuth({ getSession } as any);
// Make sure we have a session before we do the double request, so that we get past the !this.currentSession check
await expect(oktaAuth.getAccessToken()).resolves.toBe('access-token');
const promise1 = oktaAuth.getAccessToken('more');
const promise2 = oktaAuth.getAccessToken('more');
await expect(promise1).rejects.toBe(error);
await expect(promise2).rejects.toBe(error);
expect(getSession).toBeCalledTimes(3);
});
it('should wait for all session refreshes', async () => {
const initialSession = {
providerInfo: {
idToken: 'token1',
expiresAt: theFuture,
scopes: new Set(),
},
};
const getSession = jest
.fn()
.mockResolvedValueOnce(initialSession)
.mockResolvedValue({
providerInfo: {
idToken: 'token2',
expiresAt: theFuture,
scopes: new Set(),
},
});
const oktaAuth = new OktaAuth({ getSession } as any);
// Grab the expired session first
await expect(oktaAuth.getIdToken()).resolves.toBe('token1');
expect(getSession).toBeCalledTimes(1);
initialSession.providerInfo.expiresAt = thePast;
const promise1 = oktaAuth.getIdToken();
const promise2 = oktaAuth.getIdToken();
const promise3 = oktaAuth.getIdToken();
await expect(promise1).resolves.toBe('token2');
await expect(promise2).resolves.toBe('token2');
await expect(promise3).resolves.toBe('token2');
expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client
afterEach(() => {
jest.resetAllMocks();
});
it.each([
@@ -127,6 +50,12 @@ describe('OktaAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
expect(OktaAuth.normalizeScopes(scope)).toEqual(new Set(scopes));
const auth = OktaAuth.create({
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
auth.getAccessToken(scope);
expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) });
});
});
@@ -15,27 +15,13 @@
*/
import OktaIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { OktaSession } from './types';
import {
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
ProfileInfo,
SessionStateApi,
SessionState,
BackstageIdentityApi,
AuthRequestOptions,
BackstageIdentity,
} from '../../../definitions/auth';
import { oktaAuthApiRef } from '../../../definitions/auth';
import {
OAuthRequestApi,
AuthProvider,
DiscoveryApi,
} from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { OAuth2 } from '../oauth2';
type CreateOptions = {
discoveryApi: DiscoveryApi;
@@ -45,17 +31,6 @@ type CreateOptions = {
provider?: AuthProvider & { id: string };
};
export type OktaAuthResponse = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds: number;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
const DEFAULT_PROVIDER = {
id: 'okta',
title: 'Okta',
@@ -74,110 +49,33 @@ const OKTA_OIDC_SCOPES: Set<String> = new Set([
const OKTA_SCOPE_PREFIX: string = 'okta.';
class OktaAuth
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
class OktaAuth {
static create({
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
}: CreateOptions): typeof oktaAuthApiRef.T {
return OAuth2.create({
discoveryApi,
environment,
oauthRequestApi,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform(res: OktaAuthResponse): OktaSession {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OktaAuth.normalizeScopes(res.providerInfo.scope),
expiresAt: new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
),
},
};
environment,
defaultScopes: ['openid', 'email', 'profile', 'offline_access'],
scopeTransform(scopes) {
return scopes.map(scope => {
if (OKTA_OIDC_SCOPES.has(scope)) {
return scope;
}
if (scope.startsWith(OKTA_SCOPE_PREFIX)) {
return scope;
}
return `${OKTA_SCOPE_PREFIX}${scope}`;
});
},
});
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set(['openid', 'email', 'profile', 'offline_access']),
sessionScopes: session => session.scopes,
sessionShouldRefresh: session => {
const expiresInSec =
(session.providerInfo.expiresAt.getTime() - Date.now()) / 1000;
return expiresInSec < 60 * 5;
},
});
return new OktaAuth(sessionManager);
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<OktaSession>) {}
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
const session = await this.sessionManager.getSession({
...options,
scopes: OktaAuth.normalizeScopes(scope),
});
return session?.providerInfo.accessToken ?? '';
}
async getIdToken(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.providerInfo.idToken ?? '';
}
async logout() {
await this.sessionManager.removeSession();
}
async getBackstageIdentity(
options: AuthRequestOptions = {},
): Promise<BackstageIdentity | undefined> {
const session = await this.sessionManager.getSession(options);
return session?.backstageIdentity;
}
async getProfile(options: AuthRequestOptions = {}) {
const session = await this.sessionManager.getSession(options);
return session?.profile;
}
static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(/[\s|,]/).filter(Boolean);
const normalizedScopes = scopeList.map(scope => {
if (OKTA_OIDC_SCOPES.has(scope)) {
return scope;
}
if (scope.startsWith(OKTA_SCOPE_PREFIX)) {
return scope;
}
return `${OKTA_SCOPE_PREFIX}${scope}`;
});
return new Set(normalizedScopes);
}
}
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './types';
export { default as OktaAuth } from './OktaAuth';
@@ -1,27 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ProfileInfo, BackstageIdentity } from '../../../definitions';
export type OktaSession = {
providerInfo: {
idToken: string;
accessToken: string;
scopes: Set<string>;
expiresAt: Date;
};
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
};
@@ -86,3 +86,9 @@ export const Languages = () => (
<CodeSnippet text={PYTHON} language="python" showLineNumbers />
</InfoCard>
);
export const CopyCode = () => (
<InfoCard title="Copy Code">
<CodeSnippet text={JAVASCRIPT} language="javascript" showCopyCodeButton />
</InfoCard>
);
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CodeSnippet } from './CodeSnippet';
@@ -55,4 +55,14 @@ describe('<CodeSnippet />', () => {
expect(queryByText(/2/)).toBeInTheDocument();
expect(queryByText(/3/)).toBeInTheDocument();
});
it('copy code using button', async () => {
document.execCommand = jest.fn();
const rendered = render(
wrapInTestApp(<CodeSnippet {...minProps} showCopyCodeButton />),
);
const button = rendered.getByTitle('Text copied to clipboard');
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
});
});
@@ -20,19 +20,22 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { useTheme } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { CopyTextButton } from '../CopyTextButton';
type Props = {
text: string;
language: string;
showLineNumbers?: boolean;
showCopyCodeButton?: boolean;
};
const defaultProps = {
showLineNumbers: false,
showCopyCodeButton: false,
};
export const CodeSnippet: FC<Props> = props => {
const { text, language, showLineNumbers } = {
const { text, language, showLineNumbers, showCopyCodeButton } = {
...defaultProps,
...props,
};
@@ -41,13 +44,20 @@ export const CodeSnippet: FC<Props> = props => {
const mode = theme.palette.type === 'dark' ? dark : docco;
return (
<SyntaxHighlighter
language={language}
style={mode}
showLineNumbers={showLineNumbers}
>
{text}
</SyntaxHighlighter>
<div style={{ position: 'relative' }}>
<SyntaxHighlighter
language={language}
style={mode}
showLineNumbers={showLineNumbers}
>
{text}
</SyntaxHighlighter>
{showCopyCodeButton && (
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<CopyTextButton text={text} />
</div>
)}
</div>
);
};
@@ -56,4 +66,5 @@ CodeSnippet.propTypes = {
text: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
showLineNumbers: PropTypes.bool,
showCopyCodeButton: PropTypes.bool,
};
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CopyTextButton } from './CopyTextButton';
import {
@@ -76,7 +76,7 @@ describe('<CopyTextButton />', () => {
),
);
const button = rendered.getByTitle('mockTooltip');
button.click();
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
rendered.getByText('mockTooltip');
});
@@ -63,7 +63,7 @@ export const CopyTextButton: FC<Props> = props => {
};
const classes = useStyles(props);
const errorApi = useApi(errorApiRef);
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [open, setOpen] = useState(false);
const handleCopyClick: MouseEventHandler = e => {
@@ -82,9 +82,8 @@ export const CopyTextButton: FC<Props> = props => {
return (
<>
<input
<textarea
ref={inputRef}
type="text"
style={{ position: 'absolute', top: -9999, left: 9999 }}
defaultValue={text}
/>
@@ -15,26 +15,26 @@
*/
import React from 'react';
import { ProgressCard } from './ProgressCard';
import { GaugeCard } from './GaugeCard';
import { Grid } from '@material-ui/core';
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export default {
title: 'Progress Card',
component: ProgressCard,
component: GaugeCard,
};
export const Default = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard title="Progress" progress={0.3} />
<GaugeCard title="Progress" progress={0.3} />
</Grid>
<Grid item>
<ProgressCard title="Progress" progress={0.57} />
<GaugeCard title="Progress" progress={0.57} />
</Grid>
<Grid item>
<ProgressCard title="Progress" progress={0.89} />
<GaugeCard title="Progress" progress={0.89} />
</Grid>
</Grid>
);
@@ -42,21 +42,17 @@ export const Default = () => (
export const Subhead = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard
title="Progress"
subheader="With a subheader"
progress={0.3}
/>
<GaugeCard title="Progress" subheader="With a subheader" progress={0.3} />
</Grid>
<Grid item>
<ProgressCard
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.57}
/>
</Grid>
<Grid item>
<ProgressCard
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.89}
@@ -68,13 +64,13 @@ export const Subhead = () => (
export const LinkInFooter = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.3} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.3} />
</Grid>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.57} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.57} />
</Grid>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.89} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.89} />
</Grid>
</Grid>
);

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