Merge branch 'master' of github.com:spotify/backstage into shmidt-i/circle-ci-plugin-new-route-api

This commit is contained in:
Ivan Shmidt
2020-09-07 22:15:12 +02:00
187 changed files with 9837 additions and 3115 deletions
+1
View File
@@ -1,5 +1,6 @@
**/node_modules/**
**/dist/**
**/dist-types/**
**/storybook-static/**
**/coverage/**
**/build/**
+26
View File
@@ -0,0 +1,26 @@
---
name: 'RFC'
about: 'Request For Comments (RFC) from the community'
labels: rfc
title: '[RFC] <name>'
---
**Status:** Open for comments
<!--- Open for comments |Closed for comments (RFC no longer maintained) --->
## Need
<!--- Why are we proposing this change? Why is this the problem were trying to address and what benefits/impact do we expect to get from this --->
## Proposal
<!--- The proposed approach. Describe the proposal in as much detail as needed for reviewers to give concrete feedback. Take special care in this section to describe any implications on data privacy or security. --->
## Alternatives
<!--- What alternatives to the proposed solution were considered? What criteria/data was used to discard these --->
## Risks
<!--- What other things happening could conflict or compete (for example for resources) with the proposal? What risk are there and how do we plan to handle them --->
+2
View File
@@ -4,7 +4,9 @@
That makes it easier to understand the change so we can :shipit: faster. -->
#### :heavy_check_mark: Checklist
<!--- Put an `x` in all the boxes that apply: -->
- [ ] All tests are passing `yarn test`
- [ ] Screenshots attached (for UI changes)
- [ ] Relevant documentation updated
+17 -1
View File
@@ -69,11 +69,14 @@ jobs:
- name: verify doc links
run: node docs/verify-links.js
- name: prettier
run: yarn prettier:check
- name: lint
run: yarn lerna -- run lint --since origin/master
- name: type checking and declarations
run: yarn tsc --incremental false
run: yarn tsc:full
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
@@ -96,3 +99,16 @@ jobs:
- name: verify plugin template
run: yarn lerna -- run diff -- --check
- name: ensure clean working directory
run: |
if files=$(git ls-files --exclude-standard --others --modified) && [[ -z "$files" ]]; then
exit 0
else
echo ""
echo "Working directory has been modified:"
echo ""
git status --short
echo ""
exit 1
fi
+1 -1
View File
@@ -49,6 +49,6 @@ jobs:
- run: yarn tsc
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli
- name: run E2E test
run: yarn workspace e2e-test start
+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
- run: yarn tsc
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli
- name: run E2E test
run: |
sudo sysctl fs.inotify.max_user_watches=524288
+18 -3
View File
@@ -39,7 +39,22 @@ jobs:
run: yarn install --frozen-lockfile
# End of yarn setup
- name: lint
run: yarn lerna -- run lint
# Tests are broken on Windows, disabled for now
# - name: test
# run: yarn lerna -- run test
- name: type checking and declarations
run: yarn tsc:full
- name: verify type dependencies
run: yarn lint:type-deps
- name: test
run: yarn lerna -- run test
- name: Discord notification
if: ${{ failure() }}
uses: Ilshidur/action-discord@0.2.0
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Windows master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
+4 -4
View File
@@ -51,7 +51,7 @@ jobs:
run: yarn lerna -- run lint
- name: type checking and declarations
run: yarn tsc --incremental false
run: yarn tsc:full
- name: build
run: yarn build
@@ -71,9 +71,9 @@ jobs:
# Tags the commit with the version in the core package if the tag doesn't exist
- uses: Klemensas/action-autotag@1.2.3
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
package_root: "packages/core"
tag_prefix: "v"
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
package_root: 'packages/core'
tag_prefix: 'v'
- name: Discord notification
if: ${{ failure() }}
+6 -22
View File
@@ -22,33 +22,17 @@ 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: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# Skip caching of microsite dependencies, it keeps the global cache size
# smaller, which make Windows builds a lot faster for the rest of the project.
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
working-directory: microsite
- name: build microsite
run: yarn workspace backstage-microsite build
run: yarn build
working-directory: microsite
@@ -26,36 +26,24 @@ 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: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
# We avoid caching in this workflow, as we're running an install of both the top-level
# dependencies and the microsite. We leave it to the main master workflow to produce the
# cache, as that results in a smaller bundle.
- name: top-level yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: microsite yarn install
run: yarn install --frozen-lockfile
working-directory: microsite
- name: build microsite
run: yarn workspace backstage-microsite build
run: yarn build
working-directory: microsite
- name: build storybook
run: yarn workspace storybook build-storybook
+1 -4
View File
@@ -89,10 +89,7 @@ typings/
# Nuxt.js build / generate output
.nuxt
dist
# Microsite build output
microsite/build
microsite/i18n
dist-types
# Gatsby files
.cache/
+8
View File
@@ -0,0 +1,8 @@
.yarn
dist
microsite/build
coverage
*.hbs
templates
plugins/scaffolder-backend/sample-templates
.vscode
+15 -16
View File
@@ -4,12 +4,12 @@ This code of conduct outlines our expectations for participants within the **Spo
Our open source community strives to:
* **Be friendly and patient.**
* **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
* **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
* **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one.
* **Be careful in the words that we choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable.
* **Try to understand why we disagree**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that were different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
- **Be friendly and patient.**
- **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
- **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
- **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one.
- **Be careful in the words that we choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable.
- **Try to understand why we disagree**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that were different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
## Definitions
@@ -18,7 +18,7 @@ Harassment includes, but is not limited to:
- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation
- Unwelcome comments regarding a persons lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment
- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person's gender identity. You must address people by the name they give you when not addressing them by their username or handle
- Physical contact and simulated physical contact (eg, textual descriptions like “*hug*” or “*backrub*”) without consent or after a request to stop
- Physical contact and simulated physical contact (eg, textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
- Threats of violence, both physical and psychological
- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm
- Deliberate intimidation
@@ -39,7 +39,6 @@ Our open source community prioritizes marginalized peoples safety over privil
- Communicating in a tone you dont find congenial
- Criticizing racist, sexist, cissexist, or otherwise oppressive behavior or assumptions
### Diversity Statement
We encourage everyone to participate and are committed to building a community for all. Although we will fail at times, we seek to treat everyone both as fairly and equally as possible. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong.
@@ -53,18 +52,18 @@ If you experience or witness unacceptable behavior—or have any other concerns
- Your contact information.
- Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please
include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link.
include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link.
- Any additional information that may be helpful.
After filing a report, a representative will contact you personally, review the incident, follow up with any additional questions, and make a decision as to how to respond. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. If the complaint originates from a member of the response team, it will be handled by a different member of the response team. We will respect confidentiality requests for the purpose of protecting victims of abuse.
### Attribution & Acknowledgements
We all stand on the shoulders of giants across many open source communities. We'd like to thank the communities and projects that established code of conducts and diversity statements as our inspiration:
We all stand on the shoulders of giants across many open source communities. We'd like to thank the communities and projects that established code of conducts and diversity statements as our inspiration:
* [Django](https://www.djangoproject.com/conduct/reporting/)
* [Python](https://www.python.org/community/diversity/)
* [Ubuntu](http://www.ubuntu.com/about/about-ubuntu/conduct)
* [Contributor Covenant](http://contributor-covenant.org/)
* [Geek Feminism](http://geekfeminism.org/about/code-of-conduct/)
* [Citizen Code of Conduct](http://citizencodeofconduct.org/)
- [Django](https://www.djangoproject.com/conduct/reporting/)
- [Python](https://www.python.org/community/diversity/)
- [Ubuntu](http://www.ubuntu.com/about/about-ubuntu/conduct)
- [Contributor Covenant](http://contributor-covenant.org/)
- [Geek Feminism](http://geekfeminism.org/about/code-of-conduct/)
- [Citizen Code of Conduct](http://citizencodeofconduct.org/)
+6 -1
View File
@@ -1,9 +1,14 @@
FROM nginx:mainline
# The purpose of this image is to serve the frontend app content separately.
# By default the Backstage backend uses the app-backend plugin to serve the
# app from the backend itself, but it may be desirable to move the frontend
# content serving to a separate deployment, in which case this image can be used.
# This dockerfile requires the app to be built on the host first, as it
# simply copies in the build output into the image.
# The safest way to build this image is to use `yarn docker-build`
# The safest way to build this image is to use `yarn docker-build:app`
RUN apt-get update && apt-get -y install jq && rm -rf /var/lib/apt/lists/*
+11
View File
@@ -0,0 +1,11 @@
app:
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
+3 -7
View File
@@ -1,15 +1,11 @@
app:
title: Backstage Example App
baseUrl: http://localhost:3000
baseUrl: http://localhost:7000
backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
database:
client: sqlite3
connection: ':memory:'
@@ -130,8 +126,8 @@ auth:
$secret:
env: GITLAB_BASE_URL
saml:
entryPoint: "http://localhost:7001/"
issuer: "passport-saml"
entryPoint: 'http://localhost:7001/'
issuer: 'passport-saml'
okta:
development:
clientId:
+1 -2
View File
@@ -3,10 +3,9 @@ kind: Component
metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
Backstage is an open-source developer portal that puts the developer experience first.
annotations:
github.com/project-slug: spotify/backstage
backstage.io/github-actions-id: spotify/backstage
backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
spec:
type: library
+2 -6
View File
@@ -1,14 +1,10 @@
# Make sure that before you
# run the docker-compose that you have run
# $ yarn docker-build:all
# $ yarn docker-build
version: '3'
services:
frontend:
image: 'spotify/backstage:latest'
ports:
- '3000:80'
backend:
backstage:
image: 'example-backend:latest'
ports:
- '7000:7000'
+2 -3
View File
@@ -71,10 +71,9 @@ import {
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
ConfigApi
ConfigApi,
} from '@backstage/core';
const apis = (config: ConfigApi) => {
const builder = ApiRegistry.builder();
@@ -84,7 +83,7 @@ const apis = (config: ConfigApi) => {
// The error API uses the alert API to send error notifications to the user.
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
return builder.build();
}
};
const app = createApp({
apis,
+2 -2
View File
@@ -116,8 +116,8 @@ components. If youd like to help build up our design system, you can also add
components weve designed to the Storybook as well.
**[Figma](https://www.figma.com/@backstage)** - we're stoked to be using Figma
Community to share our design assets. You can duplicate our UI Kit
and design your own plugin for Backstage.
Community to share our design assets. You can duplicate our UI Kit and design
your own plugin for Backstage.
**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be
directed to the _#design_ channel.
@@ -235,6 +235,9 @@ The `backstage.io/` prefix is reserved for use by Backstage core components.
Values can be of any length, but are limited to being strings.
There is a list of [well-known annotations](well-known-annotations.md), but
anybody is free to add more annotations as they see fit.
### `tags` [optional]
A list of single-valued strings, for example to classify catalog entities in
@@ -290,7 +293,7 @@ Exactly equal to `backstage.io/v1alpha1` and `Component`, respectively.
The type of component as a string, e.g. `website`. This field is required.
The software catalog accepts any type value, but an organisation should take
The software catalog accepts any type value, but an organization should take
great care to establish a proper taxonomy for these. Tools including Backstage
itself may read this field and behave differently depending on its value. For
example, a website type component may present tooling in the Backstage interface
@@ -304,9 +307,9 @@ The current set of well-known and common values for this field is:
### `spec.lifecycle` [required]
The lifecyle state of the component, e.g. `production`. This field is required.
The lifecycle state of the component, e.g. `production`. This field is required.
The software catalog accepts any lifecycle value, but an organisation should
The software catalog accepts any lifecycle value, but an organization should
take great care to establish a proper taxonomy for these.
The current set of well-known and common values for this field is:
@@ -418,7 +421,7 @@ potentially search and group templates by these tags.
The type of component as a string, e.g. `website`. This field is optional but
recommended.
The software catalog accepts any type value, but an organisation should take
The software catalog accepts any type value, but an organization should take
great care to establish a proper taxonomy for these. Tools including Backstage
itself may read this field and behave differently depending on its value. For
example, a website type component may present tooling in the Backstage interface
@@ -508,7 +511,7 @@ Exactly equal to `backstage.io/v1alpha1` and `API`, respectively.
The type of the API definition as a string, e.g. `openapi`. This field is
required.
The software catalog accepts any type value, but an organisation should take
The software catalog accepts any type value, but an organization should take
great care to establish a proper taxonomy for these. Tools including Backstage
itself may read this field and behave differently depending on its value. For
example, an OpenAPI type API may be displayed using an OpenAPI viewer tooling in
+2 -2
View File
@@ -1,7 +1,7 @@
---
id: software-catalog-overview
title: Backstage Service Catalog (alpha)
sidebar_label: Backstage Service Catalog
sidebar_label: Overview
---
## What is a Service Catalog?
@@ -48,7 +48,7 @@ There are 3 ways to add components to the catalog:
1. Manually register components
2. Creating new components through Backstage
3. Integrating with and [external source](external-integrations.md)
3. Integrating with an [external source](external-integrations.md)
### Manually register components
@@ -0,0 +1,134 @@
---
id: well-known-annotations
title: Well-known Annotations on Catalog Entities
sidebar_label: Well-known Annotations
---
This section lists a number of well known
[annotations](descriptor-format.md#annotations-optional), that have defined
semantics. They can be attached to catalog entities and consumed by plugins as
needed.
## Annotations
This is a (non-exhaustive) list of annotations that are known to be in active
use.
### backstage.io/managed-by-location
```yaml
# Example:
metadata:
annotations:
backstage.io/managed-by-location: github:http://github.com/spotify/backstage/catalog-info.yaml
```
The value of this annotation is a so called location reference string, that
points to the source from which the entity was originally fetched. This
annotation is added automatically by the catalog as it fetches the data from a
registered location, and is not meant to normally be written by humans. The
annotation may point to any type of generic location that the catalog supports,
so it cannot be relied on to always be specifically of type `github`, nor that
it even represents a single file. Note also that a single location can be the
source of many entities, so it represents a many-to-one relationship.
The format of the value is `<type>:<target>`. Note that the target may also
contain colons, so it is not advisable to naively split the value on `:` and
expecting a two-item array out of it. The format of the target part is
type-dependent and could conceivably even be an empty string, but the separator
colon is always present.
### backstage.io/techdocs-ref
```yaml
# Example:
metadata:
annotations:
backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
```
The value of this annotation is a location reference string (see above). If this
annotation is specified, it is expected to point to a repository that the
TechDocs system can read and generate docs from.
### backstage.io/jenkins-github-folder
```yaml
# Example:
metadata:
annotations:
backstage.io/jenkins-github-folder: folder-name/job-name
```
The value of this annotation is the path to a job on Jenkins, that builds this
entity.
Specifying this annotation may enable Jenkins related features in Backstage for
that entity.
### github.com/project-slug
```yaml
# Example:
metadata:
annotations:
github.com/project-slug: spotify/backstage
```
The value of this annotation is the so-called slug that identifies a project on
[GitHub](https://github.com) that is related to this entity. It is on the format
`<organization>/<project>`, and is the same as can be seen in the URL location
bar of the browser when viewing that project.
Specifying this annotation will enable GitHub related features in Backstage for
that entity.
### sentry.io/project-slug
```yaml
# Example:
metadata:
annotations:
sentry.io/project-slug: pump-station
```
The value of this annotation is the so-called slug (or alternatively, the ID) of
a [Sentry](https://sentry.io) project within your organization. The organization
slug is currently not configurable on a per-entity basis, but is assumed to be
the same for all entities in the catalog.
Specifying this annotation may enable Sentry related features in Backstage for
that entity.
### rollbar.com/project-slug
```yaml
# Example:
metadata:
annotations:
rollbar.com/project-slug: spotify/pump-station
```
The value of this annotation is the so-called slug (or alternatively, the ID) of
a [Rollbar](https://rollbar.com) project within your organization. The value can
be the format of `[organization]/[project-slug]` or just `[project-slug]`. When
the organization slug is omitted the `app-config.yaml` will be used as a
fallback (`rollbar.organization` followed by `organization.name`).
Specifying this annotation may enable Rollbar related features in Backstage for
that entity.
## Deprecated Annotations
The following annotations are deprecated, and only listed here to aid in
migrating away from them.
### backstage.io/github-actions-id
This annotation was used for a while to enable the GitHub Actions feature. This
is now instead using the [github.com/project-slug](#github-com-project-slug)
annotation, with the same value format.
## Links
- [Descriptor Format: annotations](descriptor-format.md#annotations-optional)
@@ -7,18 +7,18 @@ Publishers are responsible for pushing and storing the templated skeleton after
the values have been templated by the `Templater`. See
[Create your own templater](./create-your-own-templater.md) for more info.
They receive a directory or location where the templater has sucessfully run
and is now ready to store somewhere. They also are given some other options
which are sent from the frontend, such as the `storePath` which is a string of
where the frontend thinks we should save this templated folder.
They receive a directory or location where the templater has sucessfully run and
is now ready to store somewhere. They also are given some other options which
are sent from the frontend, such as the `storePath` which is a string of where
the frontend thinks we should save this templated folder.
Currently we provide the following `publishers`:
- `github`
This publisher is passed through to the `createRouter` function of the
`@spotify/plugin-scaffolder-backend`. Currently, only one publisher is supported,
but PR's are always welcome.
`@spotify/plugin-scaffolder-backend`. Currently, only one publisher is
supported, but PR's are always welcome.
An full example backend can be found
[here](https://github.com/spotify/backstage/blob/d91c10f654475a60829fa33a5c81018e517a319a/packages/backend/src/plugins/scaffolder.ts),
+3 -2
View File
@@ -1,10 +1,11 @@
---
id: software-templates-index
title: Software Templates
title: Backstage Software Templates
sidebar_label: Overview
---
The Software Templates part of Backstage is a tool that can help you create
Components inside Backstage. By default, it has the ability to load skeletons of
Components inside Backstage. By default, it has the ability to load skeletons of
code, template in some variables, and then publish the template to some location
like GitHub.
@@ -30,9 +30,9 @@ 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
!!! 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
+6 -7
View File
@@ -10,21 +10,20 @@ title: Other
Run the following commands if you have Docker environment
```bash
$ yarn install
$ yarn docker-build
$ docker run --rm -it -p 80:80 spotify/backstage
$ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:latest
```
Then open http://localhost/ on your browser.
### Running with `docker-compose`
Run the following commands if you have docker and docker-compose for a full
example, with the example backend also deployed.
There is also a `docker-compose.yaml` that you can use to replace the previous
`docker run` command:
```bash
$ yarn docker-build:all
$ yarn install
$ yarn docker-build
$ docker-compose up
```
Then open http://localhost:3000 on your browser to see the example app with an
example backend.
@@ -65,6 +65,7 @@ yarn storybook # Start local storybook, useful for working on components in @bac
yarn workspace @backstage/plugin-welcome start # Serve welcome plugin only, also supports --check
yarn tsc # Run typecheck, use --watch for watch mode
yarn tsc:full # Run full type checking, for example without skipLibCheck, use in CI
yarn build # Build published versions of packages, depends on tsc
+2 -1
View File
@@ -4,7 +4,8 @@ title: Running Backstage Locally
---
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.
This is made easy with a version manager such as nvm which allows for version
switching.
```bash
# Checking your version
+5 -7
View File
@@ -171,21 +171,19 @@ The frontend container can be built with a provided command.
```bash
yarn install
yarn tsc
yarn build
yarn run docker-build
yarn run docker-build:app
```
Running this will simply generate a Docker container containing the contents of
the UIs `dist` directory. The resulting container will be about 50MB in size.
the UIs `dist` directory.
The backend container can be built by running the following command in the
`packages/backend` directory.
The backend container can be built by running the following command:
```bash
yarn run build-image
yarn run docker-build
```
This will create a ~500MB container called `example-backend`.
This will create a container called `example-backend`.
The lighthouse-audit-service container is already publicly available in Docker
Hub and can be downloaded and ran with
+7 -7
View File
@@ -18,10 +18,10 @@ spec:
component: frontend
spec:
containers:
- name: app
image: spotify/backstage:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: app
protocol: TCP
- name: app
image: spotify/backstage:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: app
protocol: TCP
+7 -7
View File
@@ -18,10 +18,10 @@ spec:
component: backend
spec:
containers:
- name: backend
image: spotify/backstage-backend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 7000
name: backend
protocol: TCP
- name: backend
image: spotify/backstage-backend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 7000
name: backend
protocol: TCP
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v1
appVersion: "1.0"
appVersion: '1.0'
description: A Helm chart for Spotify Backstage
name: backstage
version: 0.1.1-alpha.12
+25 -19
View File
@@ -1,12 +1,12 @@
app:
enabled: true
nameOverride: ""
fullnameOverride: ""
nameOverride: ''
fullnameOverride: ''
replicaCount: 1
serviceAccount:
create: false
Name: ""
image:
Name: ''
image:
repository: spotify/backstage
tag: latest
pullPolicy: Always
@@ -15,20 +15,23 @@ app:
port: 80
ingress:
enabled: false
annotations: {}
annotations:
{}
# kubernetes.io/ingress.class: "nginx"
hosts:
- host: backstage.local
paths:
- /
- host: backstage.local
paths:
- /
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
imagePullSecrets: []
podSecurityContext: {}
podSecurityContext:
{}
# fsGroup: 2000
securityContext: {}
securityContext:
{}
# capabilities:
# drop:
# - ALL
@@ -48,12 +51,12 @@ app:
backend:
enabled: false
nameOverride: ""
fullnameOverride: ""
nameOverride: ''
fullnameOverride: ''
replicaCount: 1
serviceAccount:
create: false
Name: ""
Name: ''
image:
repository: spotify/backstage-backend
tag: latest
@@ -63,20 +66,23 @@ backend:
port: 7000
ingress:
enabled: false
annotations: {}
annotations:
{}
# kubernetes.io/ingress.class: "nginx"
hosts:
- host: backstage.local
paths:
- /backend
- host: backstage.local
paths:
- /backend
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
imagePullSecrets: []
podSecurityContext: {}
podSecurityContext:
{}
# fsGroup: 2000
securityContext: {}
securityContext:
{}
# capabilities:
# drop:
# - ALL
+11 -11
View File
@@ -7,14 +7,14 @@ metadata:
component: ingress
spec:
rules:
- host: <HOSTNAME>
http:
paths:
- backend:
serviceName: backstage
servicePort: frontend
path: /
- backend:
serviceName: backstage-backend
servicePort: backend
path: /backend
- host: <HOSTNAME>
http:
paths:
- backend:
serviceName: backstage
servicePort: frontend
path: /
- backend:
serviceName: backstage-backend
servicePort: backend
path: /backend
+8 -8
View File
@@ -11,10 +11,10 @@ spec:
app: backstage
component: frontend
ports:
- name: frontend
port: 80
protocol: TCP
targetPort: app
- name: frontend
port: 80
protocol: TCP
targetPort: app
---
apiVersion: v1
kind: Service
@@ -29,7 +29,7 @@ spec:
app: backstage
component: backend
ports:
- name: backend
port: 7000
protocol: TCP
targetPort: backend
- name: backend
port: 7000
protocol: TCP
targetPort: backend
+4
View File
@@ -0,0 +1,4 @@
# Build output
build
i18n
+25 -1
View File
@@ -2,12 +2,36 @@ This website was created with [Docusaurus](https://docusaurus.io/).
# What's In This Document
- [Get Started in 5 Minutes](#get-started-in-5-minutes)
- [Getting Started](#getting-started)
- [Directory Structure](#directory-structure)
- [Editing Content](#editing-content)
- [Adding Content](#adding-content)
- [Full Documentation](#full-documentation)
# Getting Started
## Installation
```
$ yarn install
```
## Local Development
```
$ yarn start
```
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
## Build
```
$ yarn build
```
This command generates static content into the `build` directory, which is what will be deployed to GitHub pages from the master branch.
## Directory Structure
Your project file structure should look something like this
@@ -7,4 +7,3 @@ description: View GitHub pull requests for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/github-pull-requests
iconUrl: https://roadie.io/static/7f13bb8d861d8dedc5112fb939d215f9/351f2/GitHub-Mark-Light-120px-plus.png
npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests'
-1
View File
@@ -7,4 +7,3 @@ description: View Rollbar errors for your services in Backstage.
documentation: https://github.com/spotify/backstage/tree/master/plugins/rollbar
iconUrl: https://rollbar.com/assets/media/rollbar-mark-color.png
npmPackageName: '@backstage/plugin-rollbar'
-1
View File
@@ -7,4 +7,3 @@ description: View Travis CI builds for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/travis-ci
iconUrl: https://roadie.io/static/af2941eaf0af675facb281d566f42e14/45f2b/travis-ci-mascot-200x200.png
npmPackageName: '@roadiehq/backstage-plugin-travis-ci'
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "0.1.1-alpha.21",
"version": "0.0.0",
"name": "backstage-microsite",
"license": "Apache-2.0",
"private": true,
+7 -3
View File
@@ -31,7 +31,7 @@
]
}
],
"Features": [
"Core Features": [
{
"type": "subcategory",
"label": "Software Catalog",
@@ -46,7 +46,7 @@
},
{
"type": "subcategory",
"label": "Software creation templates",
"label": "Software Templates",
"ids": [
"features/software-templates/software-templates-index",
"features/software-templates/adding-templates",
@@ -92,7 +92,11 @@
{
"type": "subcategory",
"label": "Publishing",
"ids": ["plugins/publishing", "plugins/publish-private", "plugins/add-to-marketplace"]
"ids": [
"plugins/publishing",
"plugins/publish-private",
"plugins/add-to-marketplace"
]
}
],
"Configuration": [
+6650
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -10,6 +10,7 @@
"start-backend": "yarn workspace example-backend start",
"build": "lerna run build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
"test": "lerna run test --since origin/master -- --coverage",
@@ -18,11 +19,12 @@
"lint:all": "lerna run lint --",
"lint:type-deps": "node scripts/check-type-dependencies.js",
"docgen": "lerna run docgen",
"docker-build": "yarn workspace example-app build && docker build . -t spotify/backstage",
"docker-build:all": "yarn tsc && yarn build && yarn docker-build && yarn workspace example-backend build-image",
"docker-build:app": "yarn workspace example-app build && docker build . -t spotify/backstage",
"docker-build": "yarn tsc && yarn workspace example-backend build-image",
"create-plugin": "backstage-cli create-plugin",
"remove-plugin": "backstage-cli remove-plugin",
"release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push --force-publish; fi",
"prettier:check": "prettier --check .",
"lerna": "lerna",
"storybook": "yarn workspace storybook start",
"build-storybook": "yarn workspace storybook build-storybook"
@@ -30,8 +32,7 @@
"workspaces": {
"packages": [
"packages/*",
"plugins/*",
"microsite"
"plugins/*"
]
},
"version": "1.0.0",
+2
View File
@@ -27,6 +27,7 @@ import { apis } from './apis';
import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -58,6 +59,7 @@ const AppRoutes = () => (
path="/catalog/*"
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs/*" element={<DocsRouter />} />
<Navigate key="/" to="/catalog" />
{...deprecatedAppRoutes}
</Routes>
@@ -21,12 +21,14 @@ import {
Router as CircleCIRouter,
isPluginApplicableToEntity as isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import React from 'react';
import {
AboutCard,
EntityPageLayout,
useEntity,
AboutCard,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
@@ -75,6 +77,16 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
title="Sentry"
element={<SentryRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ApiDocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -95,9 +107,13 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
title="Sentry"
element={<SentryRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
<EntityPageLayout>
<EntityPageLayout.Content
@@ -105,6 +121,11 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -33,7 +33,7 @@ export function requestLoggingHandler(logger?: Logger): RequestHandler {
return morgan('combined', {
stream: {
write(message: String) {
actualLogger.info(message);
actualLogger.info(message.trimRight());
},
},
});
+7 -2
View File
@@ -2,10 +2,15 @@ FROM node:12
WORKDIR /usr/src/app
# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
# The skeleton contains the package.json of each package in the monorepo,
# and along with yarn.lock and the root package.json, that's enough to run yarn install.
ADD yarn.lock package.json skeleton.tar ./
RUN yarn install --frozen-lockfile --production
# This will copy the contents of the dist-workspace when running the build-image command.
# Do not use this Dockerfile outside of that command, as it will copy in the source code instead.
COPY . .
RUN yarn install --frozen-lockfile --production
CMD ["node", "packages/backend"]
+1 -1
View File
@@ -10,7 +10,7 @@
},
"scripts": {
"build": "backstage-cli backend:build",
"build-image": "backstage-cli backend:build-image example-backend",
"build-image": "backstage-cli backend:build-image --build --tag example-backend",
"start": "backstage-cli backend:dev",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
+4 -1
View File
@@ -106,7 +106,10 @@ export function findOwnRootDir(ownDir: string) {
*/
export function findPaths(searchDir: string): Paths {
const ownDir = findOwnDir(searchDir);
const targetDir = fs.realpathSync(process.cwd());
// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
const targetDir = fs
.realpathSync(process.cwd())
.replace(/^[a-z]:/, str => str.toUpperCase());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
+1 -1
View File
@@ -35,7 +35,7 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module',
},
ignorePatterns: ['.eslintrc.js', '**/dist/**'],
ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'],
rules: {
'no-console': 0, // Permitted in console programs
'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()'
+1 -1
View File
@@ -39,7 +39,7 @@ module.exports = {
version: 'detect',
},
},
ignorePatterns: ['.eslintrc.js', '**/dist/**'],
ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'],
rules: {
'import/newline-after-import': 'error',
'import/no-duplicates': 'warn',
+1
View File
@@ -25,6 +25,7 @@
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": false,
"skipLibCheck": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
@@ -15,28 +15,65 @@
*/
import fs from 'fs-extra';
import { join as joinPath, relative as relativePath } from 'path';
import { createDistWorkspace } from '../../lib/packager';
import { paths } from '../../lib/paths';
import { run } from '../../lib/run';
import { Command } from 'commander';
const PKG_PATH = 'package.json';
export default async (imageTag: string) => {
export default async (cmd: Command) => {
// Skip the preparation steps if we're being asked for help
if (cmd.args.includes('--help')) {
await run('docker', ['image', 'build', '--help']);
return;
}
const pkgPath = paths.resolveTarget(PKG_PATH);
const pkg = await fs.readJson(pkgPath);
const appConfigs = await findAppConfigs();
const tempDistWorkspace = await createDistWorkspace([pkg.name], {
buildDependencies: Boolean(cmd.build),
files: [
'package.json',
'yarn.lock',
'app-config.yaml',
...appConfigs,
{ src: paths.resolveTarget('Dockerfile'), dest: 'Dockerfile' },
],
skeleton: 'skeleton.tar',
});
console.log(`Dist workspace ready at ${tempDistWorkspace}`);
await run('docker', ['build', '.', '-t', imageTag], {
// all args are forwarded to docker build
await run('docker', ['image', 'build', '.', ...cmd.args], {
cwd: tempDistWorkspace,
});
await fs.remove(tempDistWorkspace);
};
/**
* Find all config files to copy into the image
*/
async function findAppConfigs(): Promise<string[]> {
const files = [];
for (const name of await fs.readdir(paths.targetRoot)) {
if (name.startsWith('app-config.') && name.endsWith('.yaml')) {
files.push(name);
}
}
if (paths.targetRoot !== paths.targetDir) {
const dirPath = relativePath(paths.targetRoot, paths.targetDir);
for (const name of await fs.readdir(paths.targetDir)) {
if (name.startsWith('app-config.') && name.endsWith('.yaml')) {
files.push(joinPath(dirPath, name));
}
}
}
return files;
}
+1
View File
@@ -19,5 +19,6 @@ import { paths } from '../../lib/paths';
export default async function clean() {
await fs.remove(paths.resolveTarget('dist'));
await fs.remove(paths.resolveTarget('dist-types'));
await fs.remove(paths.resolveTarget('coverage'));
}
@@ -59,7 +59,7 @@ describe('createPlugin', () => {
await createTemporaryPluginFolder(tempDir);
await movePlugin(tempDir, pluginDir, id);
await expect(fs.pathExists(pluginDir)).resolves.toBe(true);
expect(pluginDir).toMatch(`/plugins\/${id}`);
expect(pluginDir).toMatch(path.join('', 'plugins', id));
} finally {
await del(tempDir, { force: true });
await del(rootDir, { force: true });
+172
View File
@@ -0,0 +1,172 @@
/*
* 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 { CommanderStatic } from 'commander';
import { exitWithError } from '../lib/errors';
export function registerCommands(program: CommanderStatic) {
program
.command('app:build')
.description('Build an app for a production release')
.option('--stats', 'Write bundle stats to output directory')
.action(lazy(() => import('./app/build').then(m => m.default)));
program
.command('app:serve')
.description('Serve an app for local development')
.option('--check', 'Enable type checking and linting')
.action(lazy(() => import('./app/serve').then(m => m.default)));
program
.command('backend:build')
.description('Build a backend plugin')
.action(lazy(() => import('./backend/build').then(m => m.default)));
program
.command('backend:build-image')
.allowUnknownOption(true)
.helpOption(', --backstage-cli-help') // Let docker handle --help
.option('--build', 'Build packages before packing them into the image')
.description(
'Bundles the package into a docker image. All extra args are forwarded to docker image build',
)
.action(lazy(() => import('./backend/buildImage').then(m => m.default)));
program
.command('backend:dev')
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.option('--inspect', 'Enable debugger')
.action(lazy(() => import('./backend/dev').then(m => m.default)));
program
.command('app:diff')
.option('--check', 'Fail if changes are required')
.option('--yes', 'Apply all changes')
.description('Diff an existing app with the creation template')
.action(lazy(() => import('./app/diff').then(m => m.default)));
program
.command('create-plugin')
.description('Creates a new plugin in the current repository')
.action(
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
);
program
.command('remove-plugin')
.description('Removes plugin in the current repository')
.action(
lazy(() => import('./remove-plugin/removePlugin').then(m => m.default)),
);
program
.command('plugin:build')
.description('Build a plugin')
.action(lazy(() => import('./plugin/build').then(m => m.default)));
program
.command('plugin:serve')
.description('Serves the dev/ folder of a plugin')
.option('--check', 'Enable type checking and linting')
.action(lazy(() => import('./plugin/serve').then(m => m.default)));
program
.command('plugin:export')
.description('Exports the dev/ folder of a plugin')
.option('--stats', 'Write bundle stats to output directory')
.action(lazy(() => import('./plugin/export').then(m => m.default)));
program
.command('plugin:diff')
.option('--check', 'Fail if changes are required')
.option('--yes', 'Apply all changes')
.description('Diff an existing plugin with the creation template')
.action(lazy(() => import('./plugin/diff').then(m => m.default)));
program
.command('build')
.description('Build a package for publishing')
.option('--outputs <formats>', 'List of formats to output [types,cjs,esm]')
.action(lazy(() => import('./build').then(m => m.default)));
program
.command('lint')
.option(
'--format <format>',
'Lint report output format',
'eslint-formatter-friendly',
)
.option('--fix', 'Attempt to automatically fix violations')
.description('Lint a package')
.action(lazy(() => import('./lint').then(m => m.default)));
program
.command('test')
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
.helpOption(', --backstage-cli-help') // Let Jest handle help
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazy(() => import('./testCommand').then(m => m.default)));
program
.command('config:print')
.option('--with-secrets', 'Include secrets in the printed configuration')
.option(
'--env <env>',
'The environment to print configuration for [NODE_ENV or development]',
)
.option(
'--format <format>',
'Format to print the configuration in, either json or yaml [yaml]',
)
.description('Print the app configuration for the current package')
.action(lazy(() => import('./config/print').then(m => m.default)));
program
.command('prepack')
.description('Prepares a package for packaging before publishing')
.action(lazy(() => import('./pack').then(m => m.pre)));
program
.command('postpack')
.description('Restores the changes made by the prepack command')
.action(lazy(() => import('./pack').then(m => m.post)));
program
.command('clean')
.description('Delete cache directories')
.action(lazy(() => import('./clean/clean').then(m => m.default)));
program
.command('build-workspace <workspace-dir> ...<packages>')
.description('Builds a temporary dist workspace from the provided packages')
.action(lazy(() => import('./buildWorkspace').then(m => m.default)));
}
// Wraps an action function so that it always exits and handles errors
function lazy(
getActionFunc: () => Promise<(...args: any[]) => Promise<void>>,
): (...args: any[]) => Promise<never> {
return async (...args: any[]) => {
try {
const actionFunc = await getActionFunc();
await actionFunc(...args);
process.exit(0);
} catch (error) {
exitWithError(error);
}
};
}
@@ -39,7 +39,7 @@ const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`;
const tempDir = path.join(os.tmpdir(), 'remove-plugin-test');
const removeEmptyLines = (file: string): string =>
file.split('\n').filter(Boolean).join('\n');
file.split(/\r?\n/).filter(Boolean).join('\n');
const createTestPackageFile = async (
testFilePath: string,
@@ -66,7 +66,7 @@ const createTestPluginFile = async (
fse.copyFileSync(pluginsFilePath, testFilePath);
const pluginNameCapitalized = testPluginName
.split('-')
.map((name) => capitalize(name))
.map(name => capitalize(name))
.join('');
const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`;
addExportStatement(testFilePath, exportStatement);
@@ -100,7 +100,7 @@ describe('removePlugin', () => {
const packageFileContent = removeEmptyLines(
fse.readFileSync(packageFilePath, 'utf8'),
);
expect(testFileContent === packageFileContent).toBe(true);
expect(testFileContent).toBe(packageFileContent);
} finally {
fse.removeSync(testFilePath);
}
@@ -117,7 +117,7 @@ describe('removePlugin', () => {
const pluginsFileContent = removeEmptyLines(
fse.readFileSync(pluginsFilePaths, 'utf8'),
);
expect(testFileContent === pluginsFileContent).toBe(true);
expect(testFileContent).toBe(pluginsFileContent);
} finally {
fse.removeSync(testFilePath);
}
@@ -138,7 +138,7 @@ describe('removePlugin', () => {
'test@gmail.com',
]);
await removePluginFromCodeOwners(testFilePath, testPluginName);
expect(testFileContent === codeOwnersFileContent).toBeTruthy();
expect(testFileContent).toBe(codeOwnersFileContent);
} finally {
if (fse.existsSync(testFilePath)) fse.removeSync(testFilePath);
}
@@ -84,7 +84,7 @@ const removeAllStatementsContainingID = async (file: string, ID: string) => {
const originalContent = await fse.readFile(file, 'utf8');
const contentAfterRemoval = originalContent
.split('\n')
.filter((statement) => !statement.includes(`${ID}`)) // get rid of lines with pluginName
.filter(statement => !statement.includes(`${ID}`)) // get rid of lines with pluginName
.join('\n');
if (originalContent !== contentAfterRemoval) {
await fse.writeFile(file, contentAfterRemoval, 'utf8');
@@ -100,7 +100,7 @@ export const removeReferencesFromPluginsFile = async (
) => {
const pluginNameCapitalized = pluginName
.split('-')
.map((name) => capitalize(name))
.map(name => capitalize(name))
.join('');
await Task.forItem('removing', 'export references', async () => {
+2 -160
View File
@@ -18,151 +18,12 @@ import program from 'commander';
import chalk from 'chalk';
import { exitWithError } from './lib/errors';
import { version } from './lib/version';
import { registerCommands } from './commands';
const main = (argv: string[]) => {
program.name('backstage-cli').version(version);
program
.command('app:build')
.description('Build an app for a production release')
.option('--stats', 'Write bundle stats to output directory')
.action(lazyAction(() => import('./commands/app/build'), 'default'));
program
.command('app:serve')
.description('Serve an app for local development')
.option('--check', 'Enable type checking and linting')
.action(lazyAction(() => import('./commands/app/serve'), 'default'));
program
.command('backend:build')
.description('Build a backend plugin')
.action(lazyAction(() => import('./commands/backend/build'), 'default'));
program
.command('backend:build-image <image-tag>')
.description(
'Builds a docker image from the package, with all local deps included',
)
.action(
lazyAction(() => import('./commands/backend/buildImage'), 'default'),
);
program
.command('backend:dev')
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.option('--inspect', 'Enable debugger')
.action(lazyAction(() => import('./commands/backend/dev'), 'default'));
program
.command('app:diff')
.option('--check', 'Fail if changes are required')
.option('--yes', 'Apply all changes')
.description('Diff an existing app with the creation template')
.action(lazyAction(() => import('./commands/app/diff'), 'default'));
program
.command('create-plugin')
.description('Creates a new plugin in the current repository')
.action(
lazyAction(
() => import('./commands/create-plugin/createPlugin'),
'default',
),
);
program
.command('remove-plugin')
.description('Removes plugin in the current repository')
.action(
lazyAction(
() => import('./commands/remove-plugin/removePlugin'),
'default',
),
);
program
.command('plugin:build')
.description('Build a plugin')
.action(lazyAction(() => import('./commands/plugin/build'), 'default'));
program
.command('plugin:serve')
.description('Serves the dev/ folder of a plugin')
.option('--check', 'Enable type checking and linting')
.action(lazyAction(() => import('./commands/plugin/serve'), 'default'));
program
.command('plugin:export')
.description('Exports the dev/ folder of a plugin')
.option('--stats', 'Write bundle stats to output directory')
.action(lazyAction(() => import('./commands/plugin/export'), 'default'));
program
.command('plugin:diff')
.option('--check', 'Fail if changes are required')
.option('--yes', 'Apply all changes')
.description('Diff an existing plugin with the creation template')
.action(lazyAction(() => import('./commands/plugin/diff'), 'default'));
program
.command('build')
.description('Build a package for publishing')
.option('--outputs <formats>', 'List of formats to output [types,cjs,esm]')
.action(lazyAction(() => import('./commands/build'), 'default'));
program
.command('lint')
.option(
'--format <format>',
'Lint report output format',
'eslint-formatter-friendly',
)
.option('--fix', 'Attempt to automatically fix violations')
.description('Lint a package')
.action(lazyAction(() => import('./commands/lint'), 'default'));
program
.command('test')
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
.helpOption(', --backstage-cli-help') // Let Jest handle help
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazyAction(() => import('./commands/testCommand'), 'default'));
program
.command('config:print')
.option('--with-secrets', 'Include secrets in the printed configuration')
.option(
'--env <env>',
'The environment to print configuration for [NODE_ENV or development]',
)
.option(
'--format <format>',
'Format to print the configuration in, either json or yaml [yaml]',
)
.description('Print the app configuration for the current package')
.action(lazyAction(() => import('./commands/config/print'), 'default'));
program
.command('prepack')
.description('Prepares a package for packaging before publishing')
.action(lazyAction(() => import('./commands/pack'), 'pre'));
program
.command('postpack')
.description('Restores the changes made by the prepack command')
.action(lazyAction(() => import('./commands/pack'), 'post'));
program
.command('clean')
.description('Delete cache directories')
.action(lazyAction(() => import('./commands/clean/clean'), 'default'));
program
.command('build-workspace <workspace-dir> ...<packages>')
.description('Builds a temporary dist workspace from the provided packages')
.action(lazyAction(() => import('./commands/buildWorkspace'), 'default'));
registerCommands(program);
program.on('command:*', () => {
console.log();
@@ -181,25 +42,6 @@ const main = (argv: string[]) => {
program.parse(argv);
};
// Wraps an action function so that it always exits and handles errors
function lazyAction<T extends readonly any[], Export extends string>(
actionRequireFunc: () => Promise<
{ [name in Export]: (...args: T) => Promise<any> }
>,
exportName: Export,
): (...args: T) => Promise<never> {
return async (...args: T) => {
try {
const module = await actionRequireFunc();
const actionFunc = module[exportName];
await actionFunc(...args);
process.exit(0);
} catch (error) {
exitWithError(error);
}
};
}
process.on('unhandledRejection', rejection => {
if (rejection instanceof Error) {
exitWithError(rejection);
+1 -1
View File
@@ -37,7 +37,7 @@ export const makeConfigs = async (
options: BuildOptions,
): Promise<RollupOptions[]> => {
const typesInput = paths.resolveTargetRoot(
'dist',
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
'src/index.d.ts',
);
+42 -2
View File
@@ -15,10 +15,14 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath, relative as relativePath } from 'path';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { paths } from '../paths';
import { run } from '../run';
import tar from 'tar';
import tar, { CreateOptions } from 'tar';
import { tmpdir } from 'os';
type LernaPackage = {
@@ -48,6 +52,17 @@ type Options = {
* Defaults to ['yarn.lock', 'package.json'].
*/
files?: FileEntry[];
/**
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar';
};
/**
@@ -68,6 +83,13 @@ export async function createDistWorkspace(
const targets = await findTargetPackages(packageNames);
if (options.buildDependencies) {
const scopeArgs = targets.flatMap(target => ['--scope', target.name]);
await run('yarn', ['lerna', 'run', ...scopeArgs, 'build'], {
cwd: paths.targetRoot,
});
}
await moveToDistWorkspace(targetDir, targets);
const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json'];
@@ -77,6 +99,24 @@ export async function createDistWorkspace(
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.location);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
+3 -1
View File
@@ -37,8 +37,10 @@
},
"devDependencies": {
"@types/jest": "^26.0.7",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.0.0",
"@types/yup": "^0.28.2"
"@types/yup": "^0.28.2",
"mock-fs": "^4.13.0"
},
"files": [
"dist"
+38 -35
View File
@@ -14,46 +14,48 @@
* limitations under the License.
*/
const pathExists = jest.fn();
jest.mock('fs-extra', () => ({ pathExists }));
import mockFs from 'mock-fs';
import { resolveStaticConfig } from './resolver';
function normalizePaths(paths: string[]) {
return paths.map(p =>
p
.replace(/^[a-z]:/i, '')
.split('\\')
.join('/'),
);
}
describe('resolveStaticConfig', () => {
afterEach(() => {
jest.resetAllMocks();
mockFs.restore();
});
it('should resolve no files for empty roots', async () => {
mockFs({});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [],
});
expect(resolved).toEqual([]);
expect(pathExists).not.toHaveBeenCalled();
expect(normalizePaths(resolved)).toEqual([]);
});
it('should resolve a single app-config', async () => {
pathExists.mockImplementation(async (path: string) =>
['/repo/app-config.yaml'].includes(path),
);
mockFs({ '/repo/app-config.yaml': '' });
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo'],
});
expect(resolved).toEqual(['/repo/app-config.yaml']);
expect(pathExists).toHaveBeenCalledTimes(4);
expect(normalizePaths(resolved)).toEqual(['/repo/app-config.yaml']);
});
it('should resolve a app-configs in different directories', async () => {
pathExists.mockImplementation(async (path: string) =>
['/repo/app-config.yaml', '/repo/packages/a/app-config.yaml'].includes(
path,
),
);
mockFs({
'/repo/app-config.yaml': '',
'/repo/packages/a/app-config.yaml': '',
});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: [
@@ -64,53 +66,54 @@ describe('resolveStaticConfig', () => {
],
});
expect(resolved).toEqual([
expect(normalizePaths(resolved)).toEqual([
'/repo/app-config.yaml',
'/repo/packages/a/app-config.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(16);
});
it('should resolve env and local configs', async () => {
pathExists.mockImplementation(async (path: string) =>
[
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.production.yaml',
'/repo/app-config.production.local.yaml',
'/repo/app-config.development.local.yaml',
'/repo/packages/a/app-config.development.yaml',
'/repo/packages/a/app-config.local.yaml',
].includes(path),
);
mockFs({
'/repo/app-config.yaml': '',
'/repo/app-config.local.yaml': '',
'/repo/app-config.production.yaml': '',
'/repo/app-config.production.local.yaml': '',
'/repo/app-config.development.local.yaml': '',
'/repo/packages/a/app-config.development.yaml': '',
'/repo/packages/a/app-config.local.yaml': '',
});
const resolved = await resolveStaticConfig({
env: 'development',
rootPaths: ['/repo', '/repo/packages/a'],
});
expect(resolved).toEqual([
expect(normalizePaths(resolved)).toEqual([
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.development.local.yaml',
'/repo/packages/a/app-config.local.yaml',
'/repo/packages/a/app-config.development.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(8);
});
it('resolves suffixed configs in the correct order', async () => {
pathExists.mockImplementation(async () => true);
mockFs({
'/repo/app-config.yaml': '',
'/repo/app-config.local.yaml': '',
'/repo/app-config.production.yaml': '',
'/repo/app-config.production.local.yaml': '',
});
const resolved = await resolveStaticConfig({
env: 'production',
rootPaths: ['/repo'],
});
expect(resolved).toEqual([
expect(normalizePaths(resolved)).toEqual([
'/repo/app-config.yaml',
'/repo/app-config.local.yaml',
'/repo/app-config.production.yaml',
'/repo/app-config.production.local.yaml',
]);
expect(pathExists).toHaveBeenCalledTimes(4);
});
});
+22 -37
View File
@@ -15,45 +15,30 @@
*/
import { loadConfig } from './loader';
jest.mock('fs-extra', () => {
const mockFiles: { [path in string]: string } = {
'/root/app-config.yaml': `
app:
title: Example App
sessionKey:
$secret:
file: secrets/session-key.txt
`,
'/root/app-config.development.yaml': `
app:
sessionKey: development-key
`,
'/root/secrets/session-key.txt': 'abc123',
'/secret-port/app-config.yaml': `
backend:
listen:
port:
$secret:
file: secrets/port.txt
`,
'/secret-port/secrets/port.txt': '12345',
};
return {
async readFile(path: string) {
if (path in mockFiles) {
return mockFiles[path];
}
throw new Error(`File not found, ${path}`);
},
async pathExists(path: string) {
return path in mockFiles;
},
};
});
import mockFs from 'mock-fs';
describe('loadConfig', () => {
beforeAll(() => {
mockFs({
'/root/app-config.yaml': `
app:
title: Example App
sessionKey:
$secret:
file: secrets/session-key.txt
`,
'/root/app-config.development.yaml': `
app:
sessionKey: development-key
`,
'/root/secrets/session-key.txt': 'abc123',
});
});
afterAll(() => {
mockFs.restore();
});
it('loads config without secrets', async () => {
await expect(
loadConfig({
@@ -23,7 +23,7 @@ const PREFIX = 'okta.';
describe('OktaAuth', () => {
it('should get refreshed access token', async () => {
const getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oktaAuth = new OktaAuth({ getSession } as any);
@@ -116,7 +116,10 @@ describe('OktaAuth', () => {
['profile email', ['profile', 'email']],
[`${PREFIX}groups.manage`, [`${PREFIX}groups.manage`]],
['groups.read', [`${PREFIX}groups.read`]],
[`${PREFIX}groups.manage groups.read, openid`, [`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid']],
[
`${PREFIX}groups.manage groups.read, openid`,
[`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid'],
],
[`email\t ${PREFIX}groups.read`, ['email', `${PREFIX}groups.read`]],
// Some incorrect scopes that we don't try to fix
@@ -126,4 +129,4 @@ describe('OktaAuth', () => {
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
expect(OktaAuth.normalizeScopes(scope)).toEqual(new Set(scopes));
});
});
});
@@ -15,4 +15,4 @@
*/
export * from './types';
export { default as OktaAuth } from './OktaAuth';
export { default as OktaAuth } from './OktaAuth';
@@ -15,21 +15,21 @@
*/
import { defaultConfigLoader } from './createApp';
import { AppConfig } from '@backstage/config';
(process as any).env = { NODE_ENV: 'test' };
const anyEnv = process.env as any;
describe('defaultConfigLoader', () => {
afterEach(() => {
delete process.env.APP_CONFIG;
delete anyEnv.APP_CONFIG;
});
it('loads static config', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: [
{ data: { my: 'config' }, context: 'a' },
{ data: { my: 'override-config' }, context: 'b' },
] as AppConfig[],
});
anyEnv.APP_CONFIG = [
{ data: { my: 'config' }, context: 'a' },
{ data: { my: 'override-config' }, context: 'b' },
];
const configs = await defaultConfigLoader();
expect(configs).toEqual([
{ data: { my: 'config' }, context: 'a' },
@@ -38,13 +38,11 @@ describe('defaultConfigLoader', () => {
});
it('loads runtime config', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: [
{ data: { my: 'override-config' }, context: 'a' },
{ data: { my: 'config' }, context: 'b' },
] as AppConfig[],
});
anyEnv.APP_CONFIG = [
{ data: { my: 'override-config' }, context: 'a' },
{ data: { my: 'config' }, context: 'b' },
];
const configs = await (defaultConfigLoader as any)(
'{"my":"runtime-config"}',
);
@@ -62,20 +60,14 @@ describe('defaultConfigLoader', () => {
});
it('fails to load invalid static config', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: { my: 'invalid-config' } as any,
});
anyEnv.APP_CONFIG = { my: 'invalid-config' };
await expect(defaultConfigLoader()).rejects.toThrow(
'Static configuration has invalid format',
);
});
it('fails to load bad runtime config', async () => {
Object.defineProperty(process.env, 'APP_CONFIG', {
configurable: true,
value: [{ data: { my: 'config' }, context: 'a' }] as AppConfig[],
});
anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }];
await expect((defaultConfigLoader as any)('}')).rejects.toThrow(
'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
@@ -51,7 +51,7 @@ export const ConditionalButtons = () => {
<TextField
variant="outlined"
placeholder="Required*"
onChange={(e) => setRequired(!!e.target.value)}
onChange={e => setRequired(!!e.target.value)}
/>
</SimpleStepperStep>
<SimpleStepperStep title="Step 2">
@@ -0,0 +1,41 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Chip } from '@material-ui/core';
export default {
title: 'Chip',
component: Chip,
};
export const Default = () => <Chip label="Default" />;
export const LargeDeletable = () => (
<Chip label="Large deletable" size="medium" onDelete={() => ({})} />
);
export const LargeNotDeletable = () => (
<Chip label="Large not deletable" size="medium" />
);
export const SmallDeletable = () => (
<Chip label="Small deletable" size="small" onDelete={() => ({})} />
);
export const SmallNotDeletable = () => (
<Chip label="Small not deletable" size="small" />
);
@@ -15,9 +15,8 @@
*/
import React, { FC } from 'react';
import { InfoCard } from '.';
import { Grid } from '@material-ui/core';
import { Grid, Typography } from '@material-ui/core';
const cardContentStyle = { height: 200, width: 500 };
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export default {
@@ -25,24 +24,35 @@ export default {
component: InfoCard,
};
const text = (
<Typography paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</Typography>
);
const Wrapper: FC<{}> = ({ children }) => (
<Grid container spacing={4}>
<Grid item>{children}</Grid>
<Grid item xs={4}>
{children}
</Grid>
</Grid>
);
export const Default = () => (
<Wrapper>
<InfoCard title="Information Card">
<div style={cardContentStyle} />
</InfoCard>
<InfoCard title="Information Card">{text}</InfoCard>
</Wrapper>
);
export const Subhead = () => (
<Wrapper>
<InfoCard title="Information Card" subheader="Subhead">
<div style={cardContentStyle} />
<InfoCard title="Information Card" subheader="Subheader">
{text}
</InfoCard>
</Wrapper>
);
@@ -50,7 +60,7 @@ export const Subhead = () => (
export const LinkInFooter = () => (
<Wrapper>
<InfoCard title="Information Card" deepLink={linkInfo}>
<div style={cardContentStyle} />
{text}
</InfoCard>
</Wrapper>
);
+49 -45
View File
@@ -97,7 +97,7 @@ const columns: TableColumn[] = [
const tabs = [
{ label: 'Overview' },
{ label: 'CI/CD' },
{ label: 'Cost Efficency' },
{ label: 'Cost Efficiency' },
{ label: 'Code Coverage' },
{ label: 'Test' },
{ label: 'Compliance Advisor' },
@@ -137,38 +137,38 @@ const DataGrid = () => (
</Grid>
<Grid item xs>
<InfoCard
title="Information Card"
deepLink={{ title: 'LEARN MORE ABOUT RIGHTSIZING FOR GKE', link: '' }}
title="Additional Information"
deepLink={{ title: 'Learn more about GKE', link: '' }}
>
<b>Rightsize GKE deployment</b>
<p>
<Typography variant="h6">Rightsize GKE deployment</Typography>
<Typography paragraph>
Services are considered underutilized in GKE when the average usage of
requested cores is less than 80%.
</p>
<b>What can I do?</b>
<p>
</Typography>
<Typography variant="h6">What can I do?</Typography>
<Typography paragraph>
Review requested core and limit settings. Check HPA target scaling
settings in hpa.yaml. The recommended value for
targetCPUUtilizationPercentage is 80.
</p>
<p>
settings in <code>hpa.yaml</code>. The recommended value for&nbsp;
<code>targetCPUUtilizationPercentage</code> is <code>80</code>.
</Typography>
<Typography paragraph>
For single pods, there is of course no HPA. But it can also be useful
to think about a single pod out of a larger deployment, then modify
based on HPA requirements. Within a pod, each container has its own
CPU and memory requests and limits.
</p>
<b>Definitions</b>
<p>
</Typography>
<Typography variant="h6">Definitions</Typography>
<Typography paragraph>
A request is a minimum reserved value; a container will never have
less than this amount allocated to it, even if it doesn't actually use
it. Requests are used for determining what nodes to schedule pods on
(bin-packing). The tension here is between not allocating resources we
don't need, and having easy-enough access to enough resources to be
able to function.
</p>
<b>
</Typography>
<Typography paragraph>
Contact <Link>#cost-awareness</Link> for information and support.
</b>
</Typography>
</InfoCard>
</Grid>
</Grid>
@@ -195,37 +195,41 @@ const ExampleContentHeader = ({ selectedTab }: { selectedTab?: number }) => (
export const PluginWithData = () => {
const [selectedTab, setSelectedTab] = useState<number>(2);
return (
<Page theme={pageTheme.tool}>
<ExampleHeader />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
tabs={tabs.map(({ label }, index) => ({
id: index.toString(),
label,
}))}
/>
<Content>
<ExampleContentHeader selectedTab={selectedTab} />
<DataGrid />
</Content>
</Page>
<div style={{ border: '1px solid #ddd' }}>
<Page theme={pageTheme.tool}>
<ExampleHeader />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
tabs={tabs.map(({ label }, index) => ({
id: index.toString(),
label,
}))}
/>
<Content>
<ExampleContentHeader selectedTab={selectedTab} />
<DataGrid />
</Content>
</Page>
</div>
);
};
export const PluginWithTable = () => {
return (
<Page theme={pageTheme.tool}>
<ExampleHeader />
<Content>
<ExampleContentHeader />
<Table
options={{ paging: true, padding: 'dense' }}
data={generateTestData(10)}
columns={columns}
title="Example Content"
/>
</Content>
</Page>
<div style={{ border: '1px solid #ddd' }}>
<Page theme={pageTheme.tool}>
<ExampleHeader />
<Content>
<ExampleContentHeader />
<Table
options={{ paging: true, padding: 'dense' }}
data={generateTestData(10)}
columns={columns}
title="Example Content"
/>
</Content>
</Page>
</div>
);
};
@@ -29,9 +29,7 @@ import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
import Star from '@material-ui/icons/Star';
import { MemoryRouter } from 'react-router-dom';
import {
githubAuthApiRef,
} from '@backstage/core-api';
import { githubAuthApiRef } from '@backstage/core-api';
export default {
title: 'Sidebar',
@@ -60,12 +58,14 @@ export const SampleSidebar = () => (
<SidebarIntro />
<SidebarSpace />
<SidebarDivider />
<SidebarUserSettings providerSettings={
<OAuthProviderSettings
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
} />
<SidebarUserSettings
providerSettings={
<OAuthProviderSettings
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
}
/>
</Sidebar>
);
@@ -22,6 +22,7 @@ node_modules/
# Build output
dist
dist-types
# Temporary change files created by Vim
*.swp
@@ -0,0 +1,11 @@
app:
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
@@ -1,6 +1,6 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
baseUrl: http://localhost:7000
organization:
name: Acme Corporation
@@ -9,10 +9,6 @@ backend:
baseUrl: http://localhost:7000
listen:
port: 7000
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
{{#if dbTypeSqlite}}
database:
client: sqlite3
@@ -9,6 +9,7 @@
"start": "yarn workspace app start",
"build": "lerna run build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
"test": "lerna run test --since origin/master -- --coverage",
@@ -8,6 +8,7 @@
"@material-ui/icons": "^4.9.1",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
"@backstage/plugin-api-docs": "^{{version}}",
"@backstage/plugin-catalog": "^{{version}}",
"@backstage/plugin-register-component": "^{{version}}",
"@backstage/plugin-scaffolder": "^{{version}}",
@@ -10,6 +10,7 @@ import * as plugins from './plugins';
import { AppSidebar } from './sidebar';
import { Route, Routes, Navigate } from 'react-router';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { EntityPage } from './components/catalog/EntityPage';
const app = createApp({
@@ -33,6 +34,7 @@ const App: FC<{}> = () => (
path="/catalog/*"
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs/*" element={<DocsRouter />} />
<Navigate key="/" to="/catalog" />
{deprecatedAppRoutes}
</Routes>
@@ -21,6 +21,9 @@ import {
Router as CircleCIRouter,
isPluginApplicableToEntity as isCircleCIAvailable,
} from '@backstage/plugin-circleci';
import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import React from 'react';
import {
EntityPageLayout,
@@ -69,6 +72,16 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
title="API"
element={<ApiDocsRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -84,6 +97,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
title="CI/CD"
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -94,6 +112,11 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
title="Overview"
element={<OverviewContent entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
title="Docs"
element={<DocsRouter entity={entity} />}
/>
</EntityPageLayout>
);
@@ -9,7 +9,7 @@
},
"scripts": {
"build": "backstage-cli backend:build",
"build-image": "backstage-cli backend:build-image example-backend",
"build-image": "backstage-cli backend:build-image --build --tag example-backend",
"start": "backstage-cli backend:dev",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
@@ -8,8 +8,7 @@
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"skipLibCheck": true
"outDir": "dist-types",
"rootDir": "."
}
}
@@ -22,4 +22,24 @@ addParameters({
// Set the initial theme
current: 'light',
},
layout: 'fullscreen',
});
export const parameters = {
options: {
storySort: {
order: [
'Example Plugin',
'Header',
'Sidebar',
'Tabs',
'Information Card',
'Tabbed Card',
'Table',
'Status',
'Trendline',
'Progress Card',
],
},
},
};
+6 -6
View File
@@ -17,11 +17,11 @@
"@backstage/theme": "^0.1.1-alpha.21"
},
"devDependencies": {
"@storybook/addon-actions": "^5.3.17",
"@storybook/addon-links": "^5.3.17",
"@storybook/addon-storysource": "^5.3.18",
"@storybook/addons": "^6.0.4",
"@storybook/react": "^5.3.17",
"storybook-dark-mode": "^0.6.1"
"@storybook/addon-actions": "^6.0.21",
"@storybook/addon-links": "^6.0.21",
"@storybook/addon-storysource": "^6.0.21",
"@storybook/addons": "^6.0.21",
"@storybook/react": "^6.0.21",
"storybook-dark-mode": "^1.0.2"
}
}
@@ -1,7 +1,7 @@
site_name: 'mock-docs'
site_description: 'mock-docs site description'
nav:
nav:
- Home: index.md
- SubDocs: '!include ./sub-docs/mkdocs.yml'
@@ -1,4 +1,4 @@
site_name: subdocs
nav:
- Home 2: "index.md"
- Home 2: 'index.md'
+21
View File
@@ -192,10 +192,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
},
MuiChip: {
root: {
backgroundColor: '#D9D9D9',
// By default there's no margin, but it's usually wanted, so we add some trailing margin
marginRight: theme.spacing(1),
marginBottom: theme.spacing(1),
},
label: {
color: theme.palette.grey[900],
lineHeight: `${theme.spacing(2.5)}px`,
fontWeight: theme.typography.fontWeightMedium,
fontSize: `${theme.spacing(1.75)}px`,
},
labelSmall: {
fontSize: `${theme.spacing(1.5)}px`,
},
deleteIcon: {
color: theme.palette.grey[500],
width: `${theme.spacing(3)}px`,
height: `${theme.spacing(3)}px`,
margin: `0 ${theme.spacing(0.75)}px 0 -${theme.spacing(0.75)}px`,
},
deleteIconSmall: {
width: `${theme.spacing(2)}px`,
height: `${theme.spacing(2)}px`,
margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`,
},
},
MuiCardHeader: {
root: {
+1
View File
@@ -31,6 +31,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"swagger-ui-react": "^3.31.1"
@@ -0,0 +1,47 @@
/*
* 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 { ComponentEntity, Entity } from '@backstage/catalog-model';
import { Progress } from '@backstage/core';
import React, { FC } from 'react';
import { Grid } from '@material-ui/core';
import {
ApiDefinitionCard,
useComponentApiEntities,
useComponentApiNames,
} from '../../components';
export const EntityPageApi: FC<{ entity: Entity }> = ({ entity }) => {
const apiNames = useComponentApiNames(entity as ComponentEntity);
const { apiEntities, loading } = useComponentApiEntities({
entity: entity as ComponentEntity,
});
if (loading) {
return <Progress />;
}
return (
<Grid container spacing={3}>
{apiNames.map(api => (
<Grid item xs={12} key={api}>
<ApiDefinitionCard title={api} apiEntity={apiEntities!.get(api)} />
</Grid>
))}
</Grid>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { EntityPageApi } from './EntityPageApi';
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Route, Routes } from 'react-router';
import { WarningPanel } from '@backstage/core';
import { catalogRoute } from '../routes';
import { EntityPageApi } from './EntityPageApi';
const isPluginApplicableToEntity = (entity: Entity) => {
return ((entity.spec?.implementsApis as string[]) || []).length > 0;
};
export const Router = ({ entity }: { entity: Entity }) =>
// TODO(shmidt-i): move warning to a separate standardized component
!isPluginApplicableToEntity(entity) ? (
<WarningPanel title="API Docs plugin:">
The entity doesn't implement any APIs.
</WarningPanel>
) : (
<Routes>
<Route
path={`/${catalogRoute.path}`}
element={<EntityPageApi entity={entity} />}
/>
)
</Routes>
);
+17
View File
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { Router } from './Router';
@@ -16,7 +16,6 @@
import { Entity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
// TODO: Circular ref!
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
@@ -15,11 +15,10 @@
*/
import { Content, useApi } from '@backstage/core';
// TODO: Circular ref
import { catalogApiRef } from '@backstage/plugin-catalog';
import React from 'react';
import { useAsync } from 'react-use';
import { ApiCatalogTable } from '../ApiCatalogTable/ApiCatalogTable';
import { ApiCatalogTable } from '../ApiCatalogTable';
import ApiCatalogLayout from './ApiCatalogLayout';
const CatalogPageContents = () => {
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { ApiCatalogPage } from './ApiCatalogPage';
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { ApiCatalogTable } from './ApiCatalogTable';
@@ -14,23 +14,32 @@
* limitations under the License.
*/
import { ApiEntityV1alpha1 } from '@backstage/catalog-model';
import { ApiEntity } from '@backstage/catalog-model';
import { InfoCard } from '@backstage/core';
import React from 'react';
import { ApiDefinitionWidget } from '../ApiDefinitionWidget/ApiDefinitionWidget';
import { ApiDefinitionWidget } from '../ApiDefinitionWidget';
import { Alert } from '@material-ui/lab';
type Props = {
title?: string;
apiEntity: ApiEntityV1alpha1;
apiEntity?: ApiEntity;
};
export const ApiDefinitionCard = ({ title, apiEntity }: Props) => {
const type = apiEntity?.spec?.type || '';
const definition = apiEntity?.spec?.definition || '';
if (!apiEntity) {
return (
<InfoCard title={title}>
<Alert severity="error">Could not fetch the API</Alert>
</InfoCard>
);
}
return (
<InfoCard title={title} subheader={type}>
<ApiDefinitionWidget type={type} definition={definition} />
<InfoCard title={title} subheader={apiEntity.spec.type}>
<ApiDefinitionWidget
type={apiEntity.spec.type}
definition={apiEntity.spec.definition}
/>
</InfoCard>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { ApiDefinitionCard } from './ApiDefinitionCard';
@@ -15,9 +15,9 @@
*/
import React from 'react';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget/AsyncApiDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget/OpenApiDefinitionWidget';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget/PlainApiDefinitionWidget';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
type Props = {
type: string;

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