Fix validation file
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
handle the case where no entities are available to show
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Update ItemCard headers to pass color contrast standards.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
update the test cases of CodeSnippet component
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
add test cases for Status components
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
---
|
||||
|
||||
fix the horizontal scrolling issue in the RadarPage component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Better presentation of authentication errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
give aria-label attribute to Status Ok, Warning and Error
|
||||
@@ -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
@@ -1,5 +1,11 @@
|
||||
.idea/
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
.vsls.json
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ coverage
|
||||
templates
|
||||
plugins/scaffolder-backend/sample-templates
|
||||
.vscode
|
||||
dist-types
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
---
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
id: installation
|
||||
title: Installation
|
||||
description: Documentation on Installation
|
||||
---
|
||||
|
||||
Coming soon!
|
||||
+27
-22
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"label": "Deployment",
|
||||
"ids": [
|
||||
"getting-started/deployment-k8s",
|
||||
"getting-started/deployment-helm",
|
||||
"getting-started/deployment-other"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -111,6 +111,9 @@ export async function createConfig(
|
||||
app: {
|
||||
title: options.config.getString('app.title'),
|
||||
baseUrl: validBaseUrl.href,
|
||||
googleAnalyticsTrackingId: options.config.getOptionalString(
|
||||
'app.googleAnalyticsTrackingId',
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-2
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-2
@@ -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
|
||||
+1
-2
@@ -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];
|
||||
|
||||
+1
-1
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiRef } from './ApiRef';
|
||||
import {
|
||||
ApiRef,
|
||||
ApiHolder,
|
||||
ApiFactoryHolder,
|
||||
AnyApiRef,
|
||||
+1
-2
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user