Fix validation file

This commit is contained in:
Marek Calus
2020-10-20 08:24:55 +02:00
232 changed files with 2709 additions and 1567 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
handle the case where no entities are available to show
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/core': patch
'@backstage/plugin-graphiql': patch
'@backstage/plugin-tech-radar': patch
'@backstage/plugin-techdocs': patch
---
add test case for Progress component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Update ItemCard headers to pass color contrast standards.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
update the test cases of CodeSnippet component
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-newrelic': minor
---
The New Relic plugin now uses the Backstage proxy to communicate with New Relic's API.
Please update your `app-config.yaml` as follows:
```yaml
# Old Config
newrelic:
api:
baseUrl: 'https://api.newrelic.com/v2'
key: NEW_RELIC_REST_API_KEY
```
```yaml
# New Config
proxy:
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
add test cases for Status components
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-catalog-backend': minor
---
Renamed the `LocationProcessor` class to `CatalogProcessor`.
Likewise, renamed `LocationProcessorResult`, `LocationProcessorLocationResult`,
`LocationProcessorDataResult`, `LocationProcessorEntityResult`,
`LocationProcessorErrorResult`, and `LocationProcessorEmit` to their `Catalog*`
counterparts.
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-cost-insights': minor
---
- getProjectDailyCost and getGroupDailyCost no longer accept a metric as a parameter
- getDailyMetricData added to API for fetching daily metric data for given interval
- dailyCost removed as configurable metric
- default field added to metric configuration for displaying comparison metric data in top panel
- Metric.kind can no longer be null
- MetricData type added
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
The new `scaffolder.github.baseUrl` config property allows to specify a custom base url for GitHub enterprise instances
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-radar': patch
---
fix the horizontal scrolling issue in the RadarPage component
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Better presentation of authentication errors
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
give aria-label attribute to Status Ok, Warning and Error
+2
View File
@@ -68,6 +68,7 @@ graphviz
Hackathons
haproxy
heroku
Heroku
horizontalpodautoscalers
Hostname
http
@@ -105,6 +106,7 @@ Monorepo
monorepos
msw
namespace
namespaces
Namespaces
neuro
newrelic
+7 -1
View File
@@ -1,5 +1,11 @@
.idea/
# macOS
.DS_Store
# IntelliJ
.idea/
*.iml
# Visual Studio Code
.vscode/
.vsls.json
+1
View File
@@ -6,3 +6,4 @@ coverage
templates
plugins/scaffolder-backend/sample-templates
.vscode
dist-types
-27
View File
@@ -1,27 +0,0 @@
# Deploying Backstage
## Heroku
Deploying to heroku is relatively easy following these steps.
First, make sure you have the [heroku CLI installed](https://devcenter.heroku.com/articles/heroku-cli) and log into it as well as login into Heroku's [container registry](https://devcenter.heroku.com/articles/container-registry-and-runtime).
```bash
$ heroku login
$ heroku container:login
```
You _might_ also need to set your Heroku app's stack to `container`
```bash
$ heroku stack:set container -a <your-app>
```
We can now build/push the Docker image to Heroku's container registry and release it to the `web` worker.
```bash
$ heroku container:push web -a <your-app>
$ heroku container:release web -a <your-app>
```
With that, you should have Backstage up and running!
+15 -11
View File
@@ -1,6 +1,7 @@
app:
title: Backstage Example App
baseUrl: http://localhost:7000
googleAnalyticsTrackingId: # UA-000000-0
backend:
baseUrl: http://localhost:7000
@@ -34,8 +35,14 @@ proxy:
$env: TRAVISCI_AUTH_TOKEN
travis-api-version: 3
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key:
$env: NEW_RELIC_REST_API_KEY
organization:
name: Spotify
name: My Company
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
@@ -44,18 +51,13 @@ techdocs:
techdocs: 'docker'
sentry:
organization: spotify
organization: my-company
rollbar:
organization: spotify
organization: my-company
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
newrelic:
api:
baseUrl: 'https://api.newrelic.com/v2'
key: NEW_RELIC_REST_API_KEY
lighthouse:
baseUrl: http://localhost:3003
@@ -147,6 +149,7 @@ catalog:
scaffolder:
github:
host: https://github.com
token:
$env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
@@ -238,10 +241,11 @@ costInsights:
name: Big Query
icon: search
metrics:
dailyCost:
name: Your Company's Daily Cost
DAU:
name: Cost Per DAU
name: Daily Active Users
default: true
MSC:
name: Monthly Subscribers
homepage:
clocks:
- label: UTC
@@ -13,6 +13,7 @@ metadata:
data:
APP_CONFIG_app_baseUrl: {{ .Values.appConfig.app.baseUrl | quote | quote }}
APP_CONFIG_app_title: {{ .Values.appConfig.app.title | quote | quote }}
APP_CONFIG_app_googleAnalyticsTrackingId: {{ .Values.appConfig.app.googleAnalyticsTrackingId | quote | quote }}
APP_CONFIG_backend_baseUrl: {{ .Values.appConfig.backend.baseUrl | quote | quote }}
APP_CONFIG_backend_cors_origin: {{ .Values.appConfig.backend.cors.origin | quote | quote }}
APP_CONFIG_techdocs_storageUrl: {{ .Values.appConfig.techdocs.storageUrl | quote | quote }}
+1
View File
@@ -86,6 +86,7 @@ appConfig:
app:
baseUrl: https://demo.example.com
title: Backstage
googleAnalyticsTrackingId:
backend:
baseUrl: https://demo.example.com
listen:
@@ -9,7 +9,6 @@ import {
InfoCard,
Header,
Page,
pageTheme,
Content,
ContentHeader,
HeaderLabel,
@@ -25,7 +24,7 @@ const ExampleComponent: FC<{}> = () => {
const profile = identityApi.getProfile();
return (
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<Header
title="Welcome to github-playground!"
subtitle="Optional subtitle"
@@ -0,0 +1,42 @@
# Plain Kubernetes Deployment
This directory contains an example of a simple Kubernetes deployment of Backstage. It is not intended to serve as a complete production deployment, but as a starting point for setting one up.
## Usage
You can try the deployment out as is. The easiest way is to use [Docker Desktop](https://www.docker.com/products/docker-desktop) with [Kubernetes](https://docs.docker.com/get-started/kube-deploy/).
From a fresh clone of this repo, run the following in the root:
```bash
yarn install
yarn docker-build
kubectl apply -f contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml
```
You can use the following commands to monitor the deployment:
```bash
# List all resources in the backstage namespace
kubectl -n backstage get all
# Inspect the status of the deployment resource
kubectl -n backstage describe deployment backstage-backend
# Inspect the status of the pod running the backstage backend
kubectl -n backstage describe pod -l app=backstage,component=backend
```
Once the deployment is up and running, you can use the following to set up a proxy to reach the backend locally:
```bash
kubectl proxy
```
With the proxy up and running, you should be able to navigate to [http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy](http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy) and see Backstage. Note that you'll end up on a 404 page, but hitting the home icon in the sidebar should take you to the catalog page where you can see a few example services.
## Caveats
This deployment is for demonstration purposes only, for a production deployment you will need to set up at least a persistent database and some form of ingress. If your organization doesn't already have established patterns for these, you could look at options of managed PostgreSQL instances from cloud providers, or something like Zalando's [postgres-operator](https://github.com/zalando/postgres-operator). For ingress there are also [plenty of options](https://ramitsurana.gitbook.io/awesome-kubernetes/docs/projects/projects#load-balancing), where `nginx` is a popular choice to get started.
@@ -0,0 +1,108 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: backstage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage-backend
namespace: backstage
spec:
replicas: 1
selector:
matchLabels:
app: backstage
component: backend
template:
metadata:
labels:
app: backstage
component: backend
spec:
containers:
- name: backend
# This image is built with `yarn docker-build` in the repo root.
# Replace this with your own image to deploy your own Backstage app.
image: example-backend:latest
imagePullPolicy: Never
env:
# We set this to development to make the backend start with incomplete configuration. In a production
# deployment you will want to make sure that you have a full configuration, and remove any plugins that
# you are not using.
- name: NODE_ENV
value: development
# This makes us load in `app-config.production.yaml` if there is one.
- name: APP_ENV
value: production
# This makes it possible for the app to reach the backend when serving through `kubectl proxy`
# If you expose the service using for example an ingress controller, you should
# switch this out or remove it.
#
# Note that we're not setting app.baseUrl here, as setting the base path is not working at the moment.
# Further work is needed around the routing in the frontend or react-router before we can support that.
- name: APP_CONFIG_backend_baseUrl
value: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy
ports:
- name: http
containerPort: 7000
volumeMounts:
- name: config-volume
mountPath: /usr/src/app/app-config.local.yaml
subPath: app-config.local.yaml
resources:
limits:
cpu: 1
memory: 0.5Gi
readinessProbe:
httpGet:
port: 7000
path: /healthcheck
livenessProbe:
httpGet:
port: 7000
path: /healthcheck
volumes:
- name: config-volume
configMap:
name: backstage-config
items:
- key: app-config
path: app-config.local.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: backstage-config
namespace: backstage
data:
# Note that the config here is only applied to the backend. The frontend config is applied at build time.
# To override frontend config in this deployment, use `APP_CONFIG_` env vars.
app-config: |
app:
baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy
backend:
baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy
---
apiVersion: v1
kind: Service
metadata:
name: backstage-backend
namespace: backstage
spec:
selector:
app: backstage
component: backend
ports:
- name: http
port: 80
targetPort: http
+2 -8
View File
@@ -178,8 +178,7 @@ Spotify will maintain the open source core, but we envision different parts of
the project being maintained by various companies and contributors. We also
envision a large, diverse ecosystem of open source plugins, which would be
maintained by their original authors/contributors or by the community. When it
comes to
[deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md),
comes to [deployment](https://backstage.io/docs/getting-started/deployment-k8s),
the system integrator (typically, the infrastructure team in your organization)
maintains Backstage in your own environment.
@@ -187,7 +186,7 @@ maintains Backstage in your own environment.
No, this is not a service offering. We build the piece of software, and someone
in your infrastructure team is responsible for
[deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and
[deploying](https://backstage.io/docs/getting-started/deployment-k8s) and
maintaining it.
### How secure is Backstage?
@@ -230,8 +229,3 @@ write an open source plugin for Backstage, like this
See all the ways you can
[contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md).
We'd love to have you as part of the community.
### Can I join the Backstage team?
If you're interested in being part of the Backstage team, reach out to
[fossopportunities@spotify.com](mailto:fossopportunities@spotify.com)
@@ -1,5 +1,5 @@
---
id: software-catalog-configuration
id: configuration
title: Catalog Configuration
description: Documentation on Software Catalog Configuration
---
+18
View File
@@ -124,3 +124,21 @@ the root directory:
```bash
yarn workspace backend start
```
### Troubleshooting
#### Cannot find module
You may encounter an error similar to below:
```
internal/modules/cjs/loader.js:968
throw err;
^
Error: Cannot find module '../build/Debug/nodegit.node'
```
This can occur if an npm dependency is not completely installed. Because some
dependencies run a post-install script, ensure that both your npm and yarn
configs have the `ignore-scripts` flag set to `false`.
+5 -10
View File
@@ -2,17 +2,18 @@
id: deployment-helm
title: Helm
description: Documentation on Kubernetes and Helm Deployment
sidebar_label: Kubernetes and Helm
---
# Helm charts
An example Backstage app can be deployed in Kubernetes using the [Backstage Helm
charts][backstage-helm-charts]
An example Backstage app can be deployed in Kubernetes using the
[Backstage Helm charts](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage)
First, choose a DNS name where backstage will be hosted create a yaml file for
your custom configuration.
```
```yaml
appConfig:
app:
baseUrl: https://backstage.mydomain.com
@@ -26,7 +27,6 @@ appConfig:
techdocs:
storageUrl: https://backstage.mydomain.com/api/techdocs/static/docs
requestUrl: https://backstage.mydomain.com/api/techdocs
```
Then use it to run:
@@ -61,9 +61,4 @@ backstage-ingress * 123.1.2.3 80 17m
> **NOTE**: this is not a production ready deployment.
For more information on how to customize the deployment check the
[README][charts-readme].
[backstage-helm-charts]:
(https://github.com/spotify/backstage/tree/master/contrib/chart/backstage)
[charts-readme]:
(https://github.com/spotify/backstage/tree/master/contrib/chart/backstage/README.md)
[README](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage/README.md).
+7 -1
View File
@@ -4,4 +4,10 @@ title: Kubernetes
description: Documentation on Kubernetes and K8s Deployment
---
Coming soon!
Backstage itself provides tooling up to the point of building docker images.
Beyond that point we do not have an opinionated way to deploy Backstage within
Kubernetes, as each cluster has its own unique set of tooling and patterns.
We do provide examples to help you get started though. Check out
[this example](https://github.com/spotify/backstage/tree/master/contrib/kubernetes/plain_single_backend_deplyoment/)
for a basic single-deployment setup.
+30
View File
@@ -17,3 +17,33 @@ $ docker run --rm -it -p 7000:7000 -e APP_ENV=production -e NODE_ENV=development
```
Then open http://localhost/ on your browser.
## Heroku
Deploying to Heroku is relatively easy following these steps.
First, make sure you have the
[heroku CLI installed](https://devcenter.heroku.com/articles/heroku-cli) and log
into it as well as login into Heroku's
[container registry](https://devcenter.heroku.com/articles/container-registry-and-runtime).
```bash
$ heroku login
$ heroku container:login
```
You _might_ also need to set your Heroku app's stack to `container`
```bash
$ heroku stack:set container -a <your-app>
```
We can now build/push the Docker image to Heroku's container registry and
release it to the `web` worker.
```bash
$ heroku container:push web -a <your-app>
$ heroku container:release web -a <your-app>
```
With that, you should have Backstage up and running!
-7
View File
@@ -1,7 +0,0 @@
---
id: installation
title: Installation
description: Documentation on Installation
---
Coming soon!
+27 -22
View File
@@ -47,40 +47,29 @@ guidelines to get started.
### Ongoing work 🚧
- **[Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)** -
- **[Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)**
Out of the box Backstage will ship with a set of plugins (Overview, CI, API
and Docs) that will demonstrate how a user can manage a micro service and
follow a change all the way out in production. Completing this work will make
it much easier to see how a plugin can be built that integrates with the
Backstage Service Catalog.
- **[Kubernetes support](https://github.com/spotify/backstage/milestone/20)** -
Native support for Kubernetes, making it easier for developers to see and
manage their services running in k8s.
- **[Helm charts](https://github.com/spotify/backstage/issues/2540)** - Provide
Helm charts for easy deployments of Backstage and its subsystems on
Kubernetes.
- **[Users and teams](https://github.com/spotify/backstage/issues/1807)**
Ownership is a central concept in Backstage. It should be easy to import your
existing organizational data, such as users and groups/teams, into Backstage.
A user the logs into Backstage should see software components owned by the
team(s) they are in.
- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** -
The platform APIs and features are stable and can be depended on for
production use. After this plugins will require little to no maintenance.
- Further improvements to platform documentation
* **[Improved Kubernetes plugin](https://github.com/spotify/backstage/issues/2857)** -
Native support for Kubernetes, making it easier for developers to see and
manage their services running in k8s.
### Plugins
Building and maintaining [plugins](https://backstage.io/plugins) is the work of
the entire Backstage community.
A list of plugins that are in development is
[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc).
We strongly recommend to upvote 👍 plugins you are interested in. This helps us
and the community prioritize what plugins to build.
Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
- Further improvements to platform documentation. Examples include a Golden Path
for plugin development.
### Future work 🔮
@@ -107,8 +96,24 @@ Chances are that someone will jump in and help build it.
query. Plugins can easily query this API as well as extend the model where
needed.
### Plugins
Building and maintaining [plugins](https://backstage.io/plugins) is the work of
the entire Backstage community.
A list of plugins that are in development is
[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc).
We strongly recommend to upvote 👍 plugins you are interested in. This helps us
and the community prioritize what plugins to build.
Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
### Completed milestones ✅
- [Kubernetes plugin - v1](https://github.com/spotify/backstage/tree/master/plugins/kubernetes)
- [Helm charts](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage)
- [Backstage Design System 💅](https://backstage.io/blog/2020/09/30/backstage-design-system)
- [Cost Insights plugin 💸](https://engineering.atspotify.com/2020/09/29/managing-clouds-from-the-ground-up-cost-engineering-at-spotify/)
- [Donate Backstage to the CNCF 🎉](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox)
+1 -1
View File
@@ -15,7 +15,7 @@
},
"devDependencies": {
"@spotify/prettier-config": "^8.0.0",
"docusaurus": "^2.0.0-alpha.65",
"docusaurus": "^2.0.0-alpha.66",
"js-yaml": "^3.14.0",
"prettier": "^2.0.5"
},
+16 -17
View File
@@ -44,7 +44,7 @@ const Plugins = () => (
</span>
</div>
<BulletLine style={{ width: '100% ' }} />
<Container wrapped className="grid">
<Container wrapped className="PluginGrid">
{pluginMetadata.map(
({
iconUrl,
@@ -57,26 +57,25 @@ const Plugins = () => (
}) => (
<div className="PluginCard">
<div className="PluginCardHeader">
<img src={iconUrl || defaultIconUrl} alt={title} />
<h2 className="PluginCardTitle">{title}</h2>
<p className="Author">
by <a href={authorUrl}>{author}</a>
</p>
<span className="ChipOutlined">{category}</span>
<div className="PluginCardImage">
<img src={iconUrl || defaultIconUrl} alt={title} />
</div>
<div className="PluginCardInfo">
<h3 className="PluginCardTitle">{title}</h3>
<p className="PluginCardAuthor">
by <a href={authorUrl}>{author}</a>
</p>
<span className="PluginCardChipOutlined">{category}</span>
</div>
</div>
<div className="PluginCardBody">
<p>{truncate(description)}</p>
</div>
<Container className="PluginCardFooter">
<span>
<a
className="PluginCardLink ButtonFilled"
href={documentation}
>
Explore
</a>
</span>
</Container>
<div className="PluginCardFooter">
<a className="ButtonFilled" href={documentation}>
Explore
</a>
</div>
</div>
),
)}
+1
View File
@@ -28,6 +28,7 @@
"label": "Deployment",
"ids": [
"getting-started/deployment-k8s",
"getting-started/deployment-helm",
"getting-started/deployment-other"
]
}
+8 -1
View File
@@ -1088,6 +1088,13 @@ code {
.nav-footer .copyright {
width: 600px;
color: #fff;
margin: auto;
margin: 0 auto;
font-size: 10pt;
}
@media only screen and (max-width: 485px) {
.nav-footer .copyright {
width: auto;
margin: 0 1.5em;
}
}
+47 -18
View File
@@ -6,7 +6,7 @@
flex-direction: column;
}
.grid {
.PluginGrid {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(4, 1fr);
@@ -15,17 +15,23 @@
}
@media (max-width: 1200px) {
.grid {
.PluginGrid {
grid-template-columns: repeat(3, 1fr);
}
}
@media only screen and (max-width: 815px) {
.grid {
.PluginGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (max-width: 485px) {
.PluginGrid {
grid-template-columns: 1fr;
}
}
.PluginCard img {
float: left;
margin: 0px 16px 8px 0px;
@@ -34,14 +40,32 @@
}
.PluginCardHeader {
display: flex;
flex-direction: row;
align-items: center;
max-height: fit-content;
min-height: fit-content;
}
.PluginCardImage {
width: 80px;
height: 80px;
margin-right: 16px;
}
.PluginCardImage img {
width: 100%;
max-width: 100%;
}
.PluginCardTitle {
color: white;
vertical-align: top;
margin: 8px 0px 0px 16px;
margin: 8px 0 0;
}
.PluginCardInfo {
flex: 1;
}
.PluginAddNewButton {
@@ -50,11 +74,16 @@
right: 0px;
}
@media only screen and (max-width: 485px) {
.PluginAddNewButton {
bottom: -4px;
}
}
.ButtonFilled {
padding: 4px 8px;
border-radius: 4px;
color: #69ddc7;
margin-top: 36px;
}
.ButtonFilled:hover {
@@ -62,7 +91,7 @@
background-color: transparent;
}
.ChipOutlined {
.PluginCardChipOutlined {
font-size: small;
border-radius: 16px;
padding: 2px 8px;
@@ -70,11 +99,16 @@
color: #69ddc7;
}
.PluginCardLink {
.PluginCardFooter {
display: flex;
justify-content: flex-end;
align-items: flex-end;
margin-top: auto;
min-height: 2em;
}
.PluginCardFooter a {
padding: 2px 8px;
position: absolute;
bottom: 0;
right: 0;
}
.PluginPageLayout {
@@ -95,18 +129,13 @@
padding-top: 8px;
}
.PluginCardFooter {
position: relative;
min-height: 2em;
}
.Author,
.Author a {
.PluginCardAuthor,
.PluginCardAuthor a {
margin-bottom: 0.25em;
color: rgba(255, 255, 255, 0.6);
}
.Author a:hover {
.PluginCardAuthor a:hover {
color: white;
}
+4 -4
View File
@@ -2233,10 +2233,10 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
docusaurus@^2.0.0-alpha.65:
version "2.0.0-alpha.65"
resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.65.tgz#e2b84985529deb79797aff52aa39ffbe03cd8a20"
integrity sha512-OEnKbXLPy3EpbQA/Dj0kakB6fg/Qni1LORTIero+5Q5G0QT5OH4FAbBQOy3L1qQH0S5Bv18zESxX9Nvclg20ag==
docusaurus@^2.0.0-alpha.66:
version "2.0.0-alpha.66"
resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.66.tgz#4dece48b838f773c4c973d9fe2546dd0bf2637cf"
integrity sha512-/HmRt3hEqpvZRdR2JRYKpwNUgalO14exxuwQu2vXW5eYzcKbQ922+3SxG7mF+LE8f7KOmgY+A3eS3SEyq+S3XA==
dependencies:
"@babel/core" "^7.9.0"
"@babel/plugin-proposal-class-properties" "^7.8.3"
+1 -1
View File
@@ -13,7 +13,6 @@ nav:
- Getting started:
- 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
- Production deployments:
@@ -23,6 +22,7 @@ nav:
- Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md'
- Deployment scenarios:
- Kubernetes: 'getting-started/deployment-k8s.md'
- Kubernetes and Helm: 'getting-started/deployment-helm.md'
- Other: 'getting-started/deployment-other.md'
- Features:
- Software Catalog:
+2 -1
View File
@@ -41,6 +41,7 @@
"@changesets/cli": "^2.11.0",
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^8.0.0",
"command-exists": "^1.2.9",
"concurrently": "^5.2.0",
"fs-extra": "^9.0.0",
"husky": "^4.2.3",
@@ -65,7 +66,7 @@
"prettier --write"
],
"*.md": [
"vale"
"node ./scripts/check-docs-quality"
]
},
"jest": {
+1 -1
View File
@@ -34,8 +34,8 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-insights": "^0.2.7",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.5.1",
"@roadiehq/backstage-plugin-github-insights": "^0.2.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"dayjs": "^1.9.1",
"history": "^5.0.0",
+17
View File
@@ -48,6 +48,23 @@
}
</style>
<title><%= app.title %></title>
<% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId
=== 'string') { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= app.googleAnalyticsTrackingId %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '<%= app.googleAnalyticsTrackingId %>');
</script>
<% } %>
</head>
<body style="margin: 0;">
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -28,6 +28,7 @@ import {
Group,
inclusiveStartDateOf,
Maybe,
MetricData,
ProductCost,
Project,
ProjectGrowthAlert,
@@ -116,19 +117,33 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
return projects;
}
async getGroupDailyCost(
group: string,
metric: string | null,
async getDailyMetricData(
metric: string,
intervals: string,
): Promise<Cost> {
): Promise<MetricData> {
const aggregation = aggregationFor(
durationOf(intervals),
metric ? 0.3 : 8_000,
);
const groupDailyCost: Cost = await this.request(
{ group, metric, intervals },
100_000,
).map(entry => ({ ...entry, amount: Math.round(entry.amount) }));
const cost: MetricData = await this.request(
{ metric, intervals },
{
format: 'number',
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
},
);
return cost;
}
async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(durationOf(intervals), 8_000);
const groupDailyCost: Cost = await this.request(
{ group, intervals },
{
id: metric, // costs with null ids will appear as "All Projects" in Cost Overview panel
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
@@ -138,17 +153,10 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
return groupDailyCost;
}
async getProjectDailyCost(
project: string,
metric: string | null,
intervals: string,
): Promise<Cost> {
const aggregation = aggregationFor(
durationOf(intervals),
metric ? 0.1 : 1_500,
);
async getProjectDailyCost(project: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(durationOf(intervals), 1_500);
const projectDailyCost: Cost = await this.request(
{ project, metric, intervals },
{ project, intervals },
{
id: 'project-a',
aggregation: aggregation,
@@ -17,7 +17,7 @@
import { mergeDatabaseConfig } from './config';
describe('config', () => {
describe(mergeDatabaseConfig, () => {
describe('mergeDatabaseConfig', () => {
it('does not mutate the input object', () => {
const input = {
original: 'key',
@@ -26,7 +26,7 @@ describe('database connection', () => {
},
]);
describe(createDatabaseClient, () => {
describe('createDatabaseClient', () => {
it('returns a postgres connection', () => {
expect(
createDatabaseClient(
@@ -44,7 +44,7 @@ describe('postgres', () => {
},
]);
describe(buildPgDatabaseConfig, () => {
describe('buildPgDatabaseConfig', () => {
it('builds a postgres config', () => {
const mockConnection = createMockConnection();
@@ -125,7 +125,7 @@ describe('postgres', () => {
});
});
describe(getPgConnectionConfig, () => {
describe('getPgConnectionConfig', () => {
it('returns the connection object back', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
@@ -163,7 +163,7 @@ describe('postgres', () => {
});
});
describe(createPgDatabaseClient, () => {
describe('createPgDatabaseClient', () => {
it('creates a postgres knex instance', () => {
expect(
createPgDatabaseClient(
@@ -188,8 +188,8 @@ describe('postgres', () => {
});
});
describe(parsePgConnectionString, () => {
it('parses a connection string uri ', () => {
describe('parsePgConnectionString', () => {
it('parses a connection string uri', () => {
expect(
parsePgConnectionString(
'postgresql://postgres:pass@foobar:5432/dbname?ssl=true',
@@ -32,7 +32,7 @@ describe('sqlite3', () => {
},
]);
describe(buildSqliteDatabaseConfig, () => {
describe('buildSqliteDatabaseConfig', () => {
it('buidls a string connection', () => {
expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({
client: 'sqlite3',
@@ -72,7 +72,7 @@ describe('sqlite3', () => {
});
});
describe(createSqliteDatabaseClient, () => {
describe('createSqliteDatabaseClient', () => {
it('creates an in memory knex instance', () => {
expect(
createSqliteDatabaseClient(
-2
View File
@@ -14,8 +14,6 @@
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import { resolve as resolvePath } from 'path';
/**
+6 -1
View File
@@ -68,7 +68,12 @@ export default async function createPlugin({
) as RepoVisibilityOptions;
const githubToken = githubConfig.getString('token');
const githubClient = new Octokit({ auth: githubToken });
const githubHost =
githubConfig.getOptionalString('host') ?? 'https://github.com';
const githubClient = new Octokit({
auth: githubToken,
baseUrl: githubHost,
});
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
+2 -2
View File
@@ -22,11 +22,11 @@
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"@types/yup": "^0.29.8",
"json-schema": "^0.2.5",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.1"
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
@@ -18,24 +18,26 @@ import * as yup from 'yup';
import { EntityPolicy } from '../../types';
import { Entity } from '../Entity';
const DEFAULT_ENTITY_SCHEMA = yup.object({
apiVersion: yup.string().required(),
kind: yup.string().required(),
metadata: yup
.object({
uid: yup.string().notRequired().min(1),
etag: yup.string().notRequired().min(1),
generation: yup.number().notRequired().integer().min(1),
name: yup.string().required(),
namespace: yup.string().notRequired(),
description: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
tags: yup.array<string>().notRequired(),
})
.required(),
spec: yup.object({}).notRequired(),
});
const DEFAULT_ENTITY_SCHEMA = yup
.object({
apiVersion: yup.string().required(),
kind: yup.string().required(),
metadata: yup
.object({
uid: yup.string().notRequired().min(1),
etag: yup.string().notRequired().min(1),
generation: yup.number().notRequired().integer().min(1),
name: yup.string().required(),
namespace: yup.string().notRequired(),
description: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
tags: yup.array<string>().notRequired(),
})
.required(),
spec: yup.object({}).notRequired(),
})
.required();
/**
* Ensures that the entity spec is valid according to a schema.
@@ -101,4 +101,24 @@ describe('ComponentV1alpha1Policy', () => {
(entity as any).spec.owner = '';
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
});
it('accepts missing implementsApis', async () => {
delete (entity as any).spec.implementsApis;
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects empty implementsApis', async () => {
(entity as any).spec.implementsApis = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
});
it('rejects undefined implementsApis', async () => {
(entity as any).spec.implementsApis = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
});
it('accepts no implementsApis', async () => {
(entity as any).spec.implementsApis = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
});
@@ -29,7 +29,7 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
implementsApis: yup.array(yup.string()).notRequired(),
implementsApis: yup.array(yup.string().required()).notRequired(),
})
.required(),
});
@@ -90,9 +90,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('accepts empty ancestors', async () => {
it('rejects empty ancestors', async () => {
(entity as any).spec.ancestors = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('rejects undefined ancestors', async () => {
(entity as any).spec.ancestors = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('accepts no ancestors', async () => {
@@ -105,9 +110,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('accepts empty children', async () => {
it('rejects empty children', async () => {
(entity as any).spec.children = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('rejects undefined children', async () => {
(entity as any).spec.children = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('accepts no children', async () => {
@@ -120,9 +130,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('accepts empty descendants', async () => {
it('rejects empty descendants', async () => {
(entity as any).spec.descendants = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('rejects undefined descendants', async () => {
(entity as any).spec.descendants = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('accepts no descendants', async () => {
@@ -30,21 +30,23 @@ const schema = yup.object<Partial<GroupEntityV1alpha1>>({
parent: yup.string().notRequired().min(1),
// Use these manual tests because yup .required() requires at least
// one element and there is no simple workaround -_-
ancestors: yup.array(yup.string()).test({
// the cast is there to convince typescript that the array itself is
// required without using .required()
ancestors: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'ancestors must be defined',
test: v => Boolean(v),
}),
children: yup.array(yup.string()).test({
}) as yup.ArraySchema<string, object>,
children: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'children must be defined',
test: v => Boolean(v),
}),
descendants: yup.array(yup.string()).test({
}) as yup.ArraySchema<string, object>,
descendants: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'descendants must be defined',
test: v => Boolean(v),
}),
}) as yup.ArraySchema<string, object>,
})
.required(),
});
@@ -93,6 +93,11 @@ describe('LocationV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('accepts empty targets', async () => {
(entity as any).spec.targets = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects wrong targets', async () => {
(entity as any).spec.targets = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/targets/);
@@ -28,7 +28,7 @@ const schema = yup.object<Partial<LocationEntityV1alpha1>>({
.object({
type: yup.string().required().min(1),
target: yup.string().notRequired().min(1),
targets: yup.array(yup.string()).notRequired(),
targets: yup.array(yup.string().required()).notRequired(),
})
.required(),
});
@@ -35,11 +35,13 @@ const schema = yup.object<Partial<UserEntityV1alpha1>>({
.notRequired(),
// Use this manual test because yup .required() requires at least one
// element and there is no simple workaround -_-
memberOf: yup.array(yup.string()).test({
// the cast is there to convince typescript that the array itself is
// required without using .required()
memberOf: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'memberOf must be defined',
test: v => Boolean(v),
}),
}) as yup.ArraySchema<string, object>,
})
.required(),
});
@@ -21,9 +21,10 @@ export const locationSpecSchema = yup
.object<LocationSpec>({
type: yup.string().required(),
target: yup.string().required(),
pendingLocation: yup.bool().oneOf([true]),
pendingLocation: yup.bool(),
})
.noUnknown();
.noUnknown()
.required();
export const locationSchema = yup
.object<Location>({
@@ -31,4 +32,5 @@ export const locationSchema = yup
type: yup.string().required(),
target: yup.string().required(),
})
.noUnknown();
.noUnknown()
.required();
+13 -4
View File
@@ -38,7 +38,9 @@
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^8.1.0",
"@rollup/plugin-yaml": "^2.1.1",
"@spotify/eslint-config": "^7.0.1",
"@spotify/eslint-config-base": "^8.0.0",
"@spotify/eslint-config-react": "^8.0.0",
"@spotify/eslint-config-typescript": "^8.0.0",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "5.4.x",
"@svgr/plugin-svgo": "5.4.x",
@@ -47,6 +49,8 @@
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v3.10.1",
"@typescript-eslint/parser": "^v3.10.1",
"bfj": "^7.0.2",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
@@ -56,9 +60,14 @@
"diff": "^4.0.2",
"esbuild": "^0.7.7",
"eslint": "^7.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-monorepo": "^0.2.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^4.0.0",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
@@ -76,7 +85,7 @@
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
"rollup": "2.23.x",
"rollup-plugin-dts": "1.4.11",
"rollup-plugin-dts": "1.4.13",
"rollup-plugin-esbuild": "^2.0.0",
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^3.1.1",
@@ -84,12 +93,12 @@
"rollup-pluginutils": "^2.8.2",
"start-server-webpack-plugin": "^2.2.5",
"style-loader": "^1.2.1",
"sucrase": "^3.14.1",
"sucrase": "^3.16.0",
"tar": "^6.0.1",
"terser-webpack-plugin": "^1.4.3",
"ts-jest": "^26.0.0",
"ts-loader": "^7.0.4",
"typescript": "^3.9.3",
"typescript": "^4.0.3",
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
"webpack-dev-server": "^3.11.0",
@@ -32,7 +32,6 @@ import {
removePluginFromCodeOwners,
} from './removePlugin';
// Some constant variables
const BACKSTAGE = `@backstage`;
const testPluginName = 'yarn-test-package';
const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`;
@@ -68,8 +67,8 @@ const createTestPluginFile = async (
.split('-')
.map(name => capitalize(name))
.join('');
const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`;
addExportStatement(testFilePath, exportStatement);
const exportStatement = `export { plugin as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`;
await addExportStatement(testFilePath, exportStatement);
};
const mkTestPluginDir = (testDirPath: string) => {
@@ -78,15 +77,21 @@ const mkTestPluginDir = (testDirPath: string) => {
fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`));
};
beforeAll(() => {
// Create temporary directory for all tests
createTemporaryPluginFolder(tempDir);
});
describe('removePlugin', () => {
beforeAll(() => {
// Create temporary directory for all tests
createTemporaryPluginFolder(tempDir);
});
afterAll(() => {
// Remove temporary directory
fse.removeSync(tempDir);
});
describe('Remove Plugin Dependencies', () => {
const appPath = paths.resolveTargetRoot('packages', 'app');
const githubDir = paths.resolveTargetRoot('.github');
it('removes plugin references from /packages/app/package.json', async () => {
// Set up test
const packageFilePath = path.join(appPath, 'package.json');
@@ -105,7 +110,8 @@ describe('removePlugin', () => {
fse.removeSync(testFilePath);
}
});
it('removes plugin exports from /packages/app/src/packacge.json', async () => {
it('removes plugin exports from /packages/app/src/package.json', async () => {
const testFilePath = path.join(tempDir, 'test.ts');
const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts');
createTestPluginFile(testFilePath, pluginsFilePaths);
@@ -122,6 +128,7 @@ describe('removePlugin', () => {
fse.removeSync(testFilePath);
}
});
it('removes codeOwners references', async () => {
const testFilePath = path.join(tempDir, 'test');
const codeownersPath = path.join(githubDir, 'CODEOWNERS');
@@ -144,12 +151,14 @@ describe('removePlugin', () => {
}
});
});
describe('Remove files', () => {
const testDirPath = path.join(
paths.resolveTargetRoot(),
'plugins',
testPluginName,
);
describe('Removes Plugin Directory', () => {
it('removes plugin directory from /plugins', async () => {
try {
@@ -162,6 +171,7 @@ describe('removePlugin', () => {
}
});
});
describe('Removes System Link', () => {
it('removes system link from @backstage', async () => {
const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage');
@@ -183,8 +193,3 @@ describe('removePlugin', () => {
});
});
});
afterAll(() => {
// Remove temporary directory
fse.removeSync(tempDir);
});
@@ -20,7 +20,6 @@ import inquirer, { Answers, Question } from 'inquirer';
import { getCodeownersFilePath } from '../../lib/codeowners';
import { paths } from '../../lib/paths';
import { Task } from '../../lib/tasks';
// import os from 'os';
const BACKSTAGE = '@backstage';
+3
View File
@@ -111,6 +111,9 @@ export async function createConfig(
app: {
title: options.config.getString('app.title'),
baseUrl: validBaseUrl.href,
googleAnalyticsTrackingId: options.config.getOptionalString(
'app.googleAnalyticsTrackingId',
),
},
},
}),
+2 -2
View File
@@ -17,7 +17,7 @@
import { isParallelDefault, parseParallel } from './parallel';
describe('parallel', () => {
describe(parseParallel, () => {
describe('parseParallel', () => {
it('coerces "false" string to boolean', () => {
expect(parseParallel('false')).toBeFalsy();
});
@@ -44,7 +44,7 @@ describe('parallel', () => {
});
});
describe(isParallelDefault, () => {
describe('isParallelDefault', () => {
it('returns true if default value', () => {
expect(isParallelDefault(undefined)).toBeTruthy();
expect(isParallelDefault(true)).toBeTruthy();
@@ -4,7 +4,6 @@ import {
InfoCard,
Header,
Page,
pageTheme,
Content,
ContentHeader,
HeaderLabel,
@@ -13,7 +12,7 @@ import {
import ExampleFetchComponent from '../ExampleFetchComponent';
const ExampleComponent: FC<{}> = () => (
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<Header title="Welcome to {{ id }}!" subtitle="Optional subtitle">
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
@@ -37,5 +36,5 @@ const ExampleComponent: FC<{}> = () => (
</Content>
</Page>
);
export default ExampleComponent;
+2 -2
View File
@@ -33,13 +33,13 @@
"@backstage/config": "^0.1.1-alpha.25",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
"yup": "^0.29.3"
},
"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.29.8",
"mock-fs": "^4.13.0"
},
"files": [
+1 -1
View File
@@ -58,7 +58,7 @@ const secretLoaderSchemas = {
};
// The top-level secret schema, which figures out what type of secret it is.
const secretSchema = yup.lazy<object>(value => {
const secretSchema = yup.lazy<object | undefined>(value => {
if (typeof value !== 'object' || value === null) {
return yup.object().required().label('secret');
}
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { createApiRef, ApiRef } from '../system';
import { Observable } from '../../types';
export type AlertMessage = {
@@ -38,7 +38,7 @@ export type AlertApi = {
alert$(): Observable<AlertMessage>;
};
export const alertApiRef = createApiRef<AlertApi>({
export const alertApiRef: ApiRef<AlertApi> = createApiRef({
id: 'core.alert',
description: 'Used to report alerts and forward them to the app',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { BackstageTheme } from '@backstage/theme';
import { Observable } from '../../types';
import { SvgIconProps } from '@material-ui/core';
@@ -77,7 +77,7 @@ export type AppThemeApi = {
setActiveThemeId(themeId?: string): void;
};
export const appThemeApiRef = createApiRef<AppThemeApi>({
export const appThemeApiRef: ApiRef<AppThemeApi> = createApiRef({
id: 'core.apptheme',
description: 'API Used to configure the app theme, and enumerate options',
});
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Config } from '@backstage/config';
// Using interface to make the ConfigApi name show up in docs
export type ConfigApi = Config;
export const configApiRef = createApiRef<ConfigApi>({
export const configApiRef: ApiRef<ConfigApi> = createApiRef({
id: 'core.config',
description: 'Used to access runtime configuration',
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
/**
* The discovery API is used to provide a mechanism for plugins to
@@ -41,7 +41,7 @@ export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
};
export const discoveryApiRef = createApiRef<DiscoveryApi>({
export const discoveryApiRef: ApiRef<DiscoveryApi> = createApiRef({
id: 'core.discovery',
description: 'Provides service discovery of backend plugins',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
/**
@@ -62,7 +62,7 @@ export type ErrorApi = {
error$(): Observable<{ error: Error; context?: ErrorContext }>;
};
export const errorApiRef = createApiRef<ErrorApi>({
export const errorApiRef: ApiRef<ErrorApi> = createApiRef({
id: 'core.error',
description: 'Used to report errors and forward them to the app',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags';
import { FeatureFlagName } from '../../plugin';
@@ -57,7 +57,7 @@ export interface FeatureFlagsRegistryItem {
name: FeatureFlagName;
}
export const featureFlagsApiRef = createApiRef<FeatureFlagsApi>({
export const featureFlagsApiRef: ApiRef<FeatureFlagsApi> = createApiRef({
id: 'core.featureflags',
description: 'Used to toggle functionality in features across Backstage',
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { ProfileInfo } from './auth';
/**
@@ -51,7 +51,7 @@ export type IdentityApi = {
signOut(): Promise<void>;
};
export const identityApiRef = createApiRef<IdentityApi>({
export const identityApiRef: ApiRef<IdentityApi> = createApiRef({
id: 'core.identity',
description: 'Provides access to the identity of the signed in user',
});
@@ -16,7 +16,7 @@
import { IconComponent } from '../../icons';
import { Observable } from '../../types';
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
/**
* Information about the auth provider that we're requesting a login towards.
@@ -127,7 +127,7 @@ export type OAuthRequestApi = {
authRequest$(): Observable<PendingAuthRequest[]>;
};
export const oauthRequestApiRef = createApiRef<OAuthRequestApi>({
export const oauthRequestApiRef: ApiRef<OAuthRequestApi> = createApiRef({
id: 'core.oauthrequest',
description: 'An API for implementing unified OAuth flows in Backstage',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
import { ErrorApi } from './ErrorApi';
@@ -65,7 +65,7 @@ export interface StorageApi {
observe$<T>(key: string): Observable<StorageValueChange<T>>;
}
export const storageApiRef = createApiRef<StorageApi>({
export const storageApiRef: ApiRef<StorageApi> = createApiRef({
id: 'core.storage',
description: 'Provides the ability to store data which is unique to the user',
});
+18 -18
View File
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { Observable } from '../..';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
/**
* This file contains declarations for common interfaces of auth-related APIs.
@@ -212,13 +212,13 @@ export type SessionApi = {
* Note that the ID token payload is only guaranteed to contain the user's numerical Google ID,
* email and expiration information. Do not rely on any other fields, as they might not be present.
*/
export const googleAuthApiRef = createApiRef<
export const googleAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.google',
description: 'Provides authentication towards Google APIs and identities',
});
@@ -229,9 +229,9 @@ export const googleAuthApiRef = createApiRef<
* See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
* for a full list of supported scopes.
*/
export const githubAuthApiRef = createApiRef<
export const githubAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.github',
description: 'Provides authentication towards GitHub APIs',
});
@@ -242,13 +242,13 @@ export const githubAuthApiRef = createApiRef<
* See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/
* for a full list of supported scopes.
*/
export const oktaAuthApiRef = createApiRef<
export const oktaAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.okta',
description: 'Provides authentication towards Okta APIs',
});
@@ -259,9 +259,9 @@ export const oktaAuthApiRef = createApiRef<
* See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token
* for a full list of supported scopes.
*/
export const gitlabAuthApiRef = createApiRef<
export const gitlabAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.gitlab',
description: 'Provides authentication towards GitLab APIs',
});
@@ -272,9 +272,9 @@ export const gitlabAuthApiRef = createApiRef<
* See https://auth0.com/docs/scopes/current/oidc-scopes
* for a full list of supported scopes.
*/
export const auth0AuthApiRef = createApiRef<
export const auth0AuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.auth0',
description: 'Provides authentication towards Auth0 APIs',
});
@@ -286,13 +286,13 @@ export const auth0AuthApiRef = createApiRef<
* - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
* - https://docs.microsoft.com/en-us/graph/permissions-reference
*/
export const microsoftAuthApiRef = createApiRef<
export const microsoftAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.microsoft',
description: 'Provides authentication towards Microsoft APIs and identities',
});
@@ -300,13 +300,13 @@ export const microsoftAuthApiRef = createApiRef<
/**
* Provides authentication for custom identity providers.
*/
export const oauth2ApiRef = createApiRef<
export const oauth2ApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.oauth2',
description: 'Example of how to use oauth2 custom provider',
});
@@ -314,9 +314,9 @@ export const oauth2ApiRef = createApiRef<
/**
* Provides authentication for saml based identity providers
*/
export const samlAuthApiRef = createApiRef<
export const samlAuthApiRef: ApiRef<
ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.saml',
description: 'Example of how to use SAML custom provider',
});
+1 -5
View File
@@ -14,10 +14,6 @@
* limitations under the License.
*/
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export * from './ApiRef';
export * from './types';
export * from './helpers';
export * from './system';
export * from './definitions';
export * from './implementations';
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import { ApiHolder } from './types';
import { ApiRef, ApiHolder } from './types';
/**
* An ApiHolder that queries multiple other holders from for
@@ -15,12 +15,12 @@
*/
import {
ApiRef,
ApiFactoryHolder,
ApiFactory,
AnyApiRef,
AnyApiFactory,
} from './types';
import { ApiRef } from './ApiRef';
type ApiFactoryScope =
| 'default' // Default factories registered by core and plugins
@@ -16,8 +16,7 @@
import React, { FC, createContext, useContext, ReactNode } from 'react';
import PropTypes from 'prop-types';
import { ApiRef } from './ApiRef';
import { ApiHolder, TypesToApiRefs } from './types';
import { ApiRef, ApiHolder, TypesToApiRefs } from './types';
import { ApiAggregator } from './ApiAggregator';
type ApiProviderProps = {
@@ -14,17 +14,13 @@
* limitations under the License.
*/
import type { ApiRef } from './types';
export type ApiRefConfig = {
id: string;
description: string;
};
export type ApiRef<T> = {
id: string;
description: string;
T: T;
};
class ApiRefImpl<T> implements ApiRef<T> {
constructor(private readonly config: ApiRefConfig) {
const valid = config.id
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import { ApiHolder } from './types';
import { ApiRef, ApiHolder } from './types';
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import {
ApiRef,
ApiHolder,
ApiFactoryHolder,
AnyApiRef,
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiFactory, TypesToApiRefs } from './types';
import { ApiRef } from './ApiRef';
import { ApiRef, ApiFactory, TypesToApiRefs } from './types';
/**
* Used to infer types for a standalone ApiFactory that isn't immediately passed
@@ -0,0 +1,23 @@
/*
* 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 { ApiProvider, useApi, useApiHolder } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export { ApiResolver } from './ApiResolver';
export { ApiFactoryRegistry } from './ApiFactoryRegistry';
export { createApiRef } from './ApiRef';
export * from './types';
export * from './helpers';
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
export type ApiRef<T> = {
id: string;
description: string;
T: T;
};
export type AnyApiRef = ApiRef<unknown>;
+1 -2
View File
@@ -54,8 +54,7 @@ import {
} from '../apis';
import { useAsync } from 'react-use';
import { AppIdentity } from './AppIdentity';
import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry';
import { ApiResolver } from '../apis/ApiResolver';
import { ApiResolver, ApiFactoryRegistry } from '../apis/system';
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
+1 -1
View File
@@ -16,7 +16,7 @@
import { ComponentType } from 'react';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
import { AnyApiFactory } from '../apis/system';
export type RouteOptions = {
// Whether the route path must match exactly, defaults to true.
@@ -20,10 +20,10 @@ import { wrapInTestApp } from '@backstage/test-utils';
import { CodeSnippet } from './CodeSnippet';
const JAVASCRIPT = `const greeting = "Hello";
const world = "World";
const greet = person => gretting + " " + person + "!";
const JAVASCRIPT = `
const greeting = "Hello";
const world = "World";
const greet = person => gretting + " " + person + "!";
`;
const minProps = {
@@ -48,20 +48,20 @@ describe('<CodeSnippet />', () => {
});
it('renders with line numbers', () => {
const { queryByText } = render(
const { getByText } = render(
wrapInTestApp(<CodeSnippet {...minProps} showLineNumbers />),
);
expect(queryByText(/1/)).toBeInTheDocument();
expect(queryByText(/2/)).toBeInTheDocument();
expect(queryByText(/3/)).toBeInTheDocument();
expect(getByText('1')).toBeInTheDocument();
expect(getByText('2')).toBeInTheDocument();
expect(getByText('3')).toBeInTheDocument();
});
it('copy code using button', async () => {
document.execCommand = jest.fn();
const rendered = render(
const { getByTitle } = render(
wrapInTestApp(<CodeSnippet {...minProps} showCopyCodeButton />),
);
const button = rendered.getByTitle('Text copied to clipboard');
const button = getByTitle('Text copied to clipboard');
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
});
@@ -0,0 +1,34 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { act } from 'react-dom/test-utils';
import { Progress } from './Progress';
describe('<Progress />', () => {
it('renders without exploding', async () => {
jest.useFakeTimers();
const { getByTestId, queryByTestId } = await renderInTestApp(<Progress />);
expect(queryByTestId('progress')).not.toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(250);
});
expect(getByTestId('progress')).toBeInTheDocument();
jest.useRealTimers();
});
});
@@ -28,6 +28,6 @@ export const Progress: FC<LinearProgressProps> = props => {
return isVisible ? (
<LinearProgress {...props} data-testid="progress" />
) : (
<div style={{ display: 'none' }} data-testid="progress" />
<div style={{ display: 'none' }} />
);
};
@@ -0,0 +1,69 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import {
StatusOK,
StatusWarning,
StatusError,
StatusPending,
StatusRunning,
StatusAborted,
} from './Status';
describe('<StatusOK />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusOK />);
expect(getByLabelText('Status ok')).toBeInTheDocument();
});
});
describe('<StatusWarning />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusWarning />);
expect(getByLabelText('Status warning')).toBeInTheDocument();
});
});
describe('<StatusError />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusError />);
expect(getByLabelText('Status error')).toBeInTheDocument();
});
});
describe('<StatusPending />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusPending />);
expect(getByLabelText('Status pending')).toBeInTheDocument();
});
});
describe('<StatusRunning />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusRunning />);
expect(getByLabelText('Status running')).toBeInTheDocument();
});
});
describe('<StatusAborted />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusAborted />);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
});
});
+17 -3
View File
@@ -65,20 +65,34 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
export const StatusOK: FC<{}> = props => {
const classes = useStyles(props);
return <span className={classNames(classes.status, classes.ok)} {...props} />;
return (
<span
className={classNames(classes.status, classes.ok)}
aria-label="Status ok"
{...props}
/>
);
};
export const StatusWarning: FC<{}> = props => {
const classes = useStyles(props);
return (
<span className={classNames(classes.status, classes.warning)} {...props} />
<span
className={classNames(classes.status, classes.warning)}
aria-label="Status warning"
{...props}
/>
);
};
export const StatusError: FC<{}> = props => {
const classes = useStyles(props);
return (
<span className={classNames(classes.status, classes.error)} {...props} />
<span
className={classNames(classes.status, classes.error)}
aria-label="Status error"
{...props}
/>
);
};
@@ -67,7 +67,7 @@ const columns = [
export const Default = () => (
<div style={containerStyle}>
<InfoCard title="Trends over time">
<InfoCard title="Trends over time" noPadding>
<Table
options={{
search: false,
@@ -16,7 +16,7 @@
import React from 'react';
import { Header } from '.';
import { HeaderLabel } from '../HeaderLabel';
import { Page, pageTheme } from '../Page';
import { Page } from '../Page';
export default {
title: 'Layout/Header',
@@ -32,7 +32,7 @@ const labels = (
);
export const Home = () => (
<Page theme={pageTheme.home}>
<Page themeId="home">
<Header title="Start/Home Page" type="home">
{labels}
</Header>
@@ -46,7 +46,7 @@ export const HomeWithSubtitle = () => (
);
export const Tool = () => (
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<Header title="Stand-alone tool" type="tool">
{labels}
</Header>
@@ -54,7 +54,7 @@ export const Tool = () => (
);
export const Service = () => (
<Page theme={pageTheme.service}>
<Page themeId="service">
<Header title="Service component page" type="service">
{labels}
</Header>
@@ -62,7 +62,7 @@ export const Service = () => (
);
export const Website = () => (
<Page theme={pageTheme.website}>
<Page themeId="website">
<Header title="Website component page" type="website">
{labels}
</Header>
@@ -70,7 +70,7 @@ export const Website = () => (
);
export const Library = () => (
<Page theme={pageTheme.library}>
<Page themeId="library">
<Header title="Library component page" type="library">
{labels}
</Header>
@@ -78,7 +78,7 @@ export const Library = () => (
);
export const App = () => (
<Page theme={pageTheme.app}>
<Page themeId="app">
<Header title="App component page" type="app">
{labels}
</Header>
@@ -86,7 +86,7 @@ export const App = () => (
);
export const Documentation = () => (
<Page theme={pageTheme.documentation}>
<Page themeId="documentation">
<Header title="Documentation component page" type="documentation">
{labels}
</Header>
@@ -94,7 +94,7 @@ export const Documentation = () => (
);
export const Other = () => (
<Page theme={pageTheme.other}>
<Page themeId="other">
<Header title="Other/generic component page" type="other">
{labels}
</Header>

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