This commit is contained in:
Tim Hansen
2020-08-21 11:27:39 -06:00
234 changed files with 3120 additions and 1944 deletions
@@ -9,6 +9,7 @@ on:
- 'packages/storybook/**'
- 'packages/core/src/**'
- 'microsite/**'
- 'docs/**'
jobs:
deploy-microsite-and-storybook:
+2
View File
@@ -8,6 +8,8 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
> Collect changes for the next release below
## v0.1.1-alpha.19
### @backstage/create-app
- Many plugins have been added to the catalog and will for now be required to be added to separate apps as well. This will be solved as [#1536](https://github.com/spotify/backstage/issues/1536) gets sorted out, but for now you may need to install some plugins just to get pages to work.
+2 -14
View File
@@ -54,8 +54,6 @@ auth:
providers:
google:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_GOOGLE_CLIENT_ID
@@ -64,8 +62,6 @@ auth:
env: AUTH_GOOGLE_CLIENT_SECRET
github:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_GITHUB_CLIENT_ID
@@ -77,8 +73,6 @@ auth:
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_GITLAB_CLIENT_ID
@@ -94,8 +88,6 @@ auth:
# issuer: "passport-saml"
okta:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_OKTA_CLIENT_ID
@@ -107,24 +99,20 @@ auth:
env: AUTH_OKTA_AUDIENCE
oauth2:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$secret:
env: AUTH_OAUTH2_CLIENT_SECRET
authorizationURL:
authorizationUrl:
$secret:
env: AUTH_OAUTH2_AUTH_URL
tokenURL:
tokenUrl:
$secret:
env: AUTH_OAUTH2_TOKEN_URL
auth0:
development:
appOrigin: 'http://localhost:3000/'
secure: false
clientId:
$secret:
env: AUTH_AUTH0_CLIENT_ID
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+2 -2
View File
@@ -3,7 +3,7 @@ id: design
title: Design
---
![header](../assets/dls/designheader.png)
![header](../assets/dls/designheader-updated.png)
Much like Backstage Open Source, this is a _living_ document! We'll keep this
updated as we evolve our practices!
@@ -116,7 +116,7 @@ components. If youd like to help build up our design system, you can also add
components weve designed to the Storybook as well.
**[Figma](https://www.figma.com/@backstage)** - we're stoked to be using Figma
Community to share our design assets. You can duplicate our component library
Community to share our design assets. You can duplicate our UI Kit
and design your own plugin for Backstage.
**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be
@@ -42,6 +42,7 @@ software catalog API.
"labels": {
"system": "public-websites"
},
"tags": ["java"],
"name": "artist-web",
"uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2"
},
@@ -66,6 +67,8 @@ metadata:
annotations:
example.com/service-discovery: artistweb
circleci.com/project-slug: gh/example-org/artist-website
tags:
- java
spec:
type: website
lifecycle: production
@@ -232,6 +235,20 @@ The `backstage.io/` prefix is reserved for use by Backstage core components.
Values can be of any length, but are limited to being strings.
### `tags` [optional]
A list of single-valued strings, for example to classify catalog entities in
various ways. This is different to the labels in metadata, as labels are
key-value pairs.
The values are user defined, for example the programming language used for the
component, like `java` or `go`.
This field is optional, and currently has no special semantics.
Each tag must be sequences of `[a-zA-Z0-9]` separated by `-`, at most 63
characters in total.
## Kind: Component
Describes the following entity kind:
@@ -54,7 +54,7 @@ contains more information about the required fields.
Once we have a `template.yaml` ready, we can then add it to the service catalog
for use by the scaffolder.
Currently the catalog supports loading definitions from Github + Local Files. To
Currently the catalog supports loading definitions from GitHub + Local Files. To
load from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
@@ -54,7 +54,7 @@ The `protocol` is set on the
when added to the service catalog. You can see more about this `PreparerKey`
here in [Register your own template](../adding-templates.md)
**note:** Currently the catalog supports loading definitions from Github + Local
**note:** Currently the catalog supports loading definitions from GitHub + Local
Files, which translate into the two `PreparerKeys` `file` and `github`. To load
from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
+1 -1
View File
@@ -34,7 +34,7 @@ internally.
After filling in these variables, you'll get some more fields to fill out which
are required for backstage usage. The owner, which is a `user` in the backstage
system, and the `storePath` which right now must be a Github Organisation and a
system, and the `storePath` which right now must be a GitHub Organisation and a
non-existing github repository name in the format `organisation/reponame`.
![Enter backstage vars](../../assets/software-templates/template-picked-2.png)
@@ -109,7 +109,7 @@ export default async function createPlugin({ logger }: PluginEnvironment) {
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
// Create Github client with your access token from environment variables
// Create GitHub client with your access token from environment variables
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
+75 -18
View File
@@ -5,17 +5,15 @@ title: Project roadmap
## Current status
Backstage is currently in Alpha. This means that you can expect APIs and
features to change until we reach our
[Backstage Beta milestone](https://github.com/spotify/backstage/milestone/19).
Before we have API stability, plugins will need ongoing maintenance. It is also
recommended that teams who adopt Backstage today upgrades their installation as
new [releases](https://github.com/spotify/backstage/releases) are available.
> Backstage is currently under rapid development. This means that you can expect
> APIs and features to evolve. It is also recommended that teams who adopt
> Backstage today upgrade their installation as new
> [releases](https://github.com/spotify/backstage/releases) become available, as
> Backwards compatibility is not yet guaranteed.
## Phases
We have divided the project into three high-level _phases_ and we have already
begun work on various aspects of these phases:
We have divided the project into three high-level _phases_:
- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to
easily create a single consistent UI layer for your internal infrastructure
@@ -35,22 +33,81 @@ begun work on various aspects of these phases:
## Detailed roadmap
If you have questions about the roadmap or want to provide feedback, we would
love to hear from you! Please create an
[Issue](https://github.com/spotify/backstage/issues/new/choose), ping us on
[Discord](https://discord.gg/EBHEGzX) or reach out directly at
[alund@spotify.com](mailto:alund@spotify.com).
Want to help out? Awesome ❤️ Head over to
[CONTRIBUTING](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md)
guidelines to get started.
### Ongoing work 🚧
- [Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)
- [TechDocs v1](https://github.com/spotify/backstage/milestone/16)
- [Initial GraphQL API](https://github.com/spotify/backstage/milestone/13)
- Backstage Design Language System (DLS)
- Cloud Cost Insights plugin (from Spotify)
- **[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.
- **Backstage Design System** - By providing design guidelines for common plugin
layouts together, rich set of reusable UI components
([Storybook](https://backstage.io/storybook)) and Figma design resources. The
Design System will make it easy to design and build plugins that are
consistent across the platform -- supporting both developers and designers.
- **[TechDocs v1](https://github.com/spotify/backstage/milestone/16)** - Our
docs-like-code feature TechDocs working end to end.
- **[Initial GraphQL API](https://github.com/spotify/backstage/milestone/13)** -
A GraphQL API will open up the rich metadata provided by Backstage in a single
query. Plugins can easily query this API as well as extend the model where
needed.
- **Production deployments** - Provide instructions and default configurations
(e.g. through Helm charts) for easy deployments of Backstage and its
subsystems on Kubernetes.
- **Cloud Cost Insights plugin (from Spotify)** - Spotify teams are fully
responsible for their own software, including the cost of the cloud resources
they use. By making our internal cost insights plugin available as open source
you will also be able to treat cost as an engineering problem, and make it
easy for your engineers to see their spend and where there's opportunity to
reduce waste.
- Further improvements to platform documentation
### Future work 🔮
- [Backstage Beta](https://github.com/spotify/backstage/milestone/19)
- [Global search](https://github.com/spotify/backstage/issues/1499)
- [[TechDocs V.2] Stabilization release](https://github.com/spotify/backstage/milestone/17)
- Deploy a product demo at `demo.backstage.io`
- Plugin marketplace
- **[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.
- **[Plugin marketplace](https://github.com/spotify/backstage/issues/2009)** -
As the ecosystem of Backstage plugins continues to grow it is becoming
increasingly hard to keep track of what plugins are available. To solve this
we imagine a "Plugin marketplace" that helps with discovery and installation
of plugins.
- **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage
deployment available publicly so that people can click around and get a feel
for the product without having to install anything.
- **[Global search](https://github.com/spotify/backstage/issues/1499)** - Extend
the basic search available in the Backstage Service Catalog with a global
search experience. Long term this search solution should be extensible, making
it possible for you add custom search results.
- **[[TechDocs V.2] Stabilization release](https://github.com/spotify/backstage/milestone/17)** -
Platform stability and compatibility improvements.
- **Additional auth providers** - Backstage should work for most (all!) auth
solutions. Since Backstage can be used by companies regardless of what cloud
(or on prem) you are using we are especially keen to get auth support for
[AWS](https://github.com/spotify/backstage/issues/290),
[Azure](https://github.com/spotify/backstage/issues/348) and others.
### Completed milestones ✅
+173 -2
View File
@@ -1,6 +1,177 @@
---
id: call-existing-api
title: Call existing API
title: Call Existing API
---
## TODO
This article describes the various options that Backstage frontend plugins have,
in communicating with service APIs that already exist. Each section below
describes a possible choice, and the circumstances under which it fits.
In these examples, we will be ultimately requesting data from the fictional
FrobsCo API.
## Issuing Requests Directly
The most basic choice available is to issue requests directly from the plugin
frontend code to the FrobsCo API, using for example `fetch` or a support library
such as `axios`.
Example:
```ts
// Inside your component
fetch('https://api.frobsco.com/v1/list')
.then(response => response.json())
.then(payload => setFrobs(payload as Frob[]));
```
Internally at Spotify, this has not been a very common choice. Third party APIs
are sometimes accessed like this. Just a handful of internal APIs also went
through the trouble of exposing themselves in a way that is useful directly from
a browser, but even then, often not from the public internet but only supporting
users that are already on the company VPN.
This can be used when:
- The API already does/exposes exactly what you need.
- The request/response patterns of the API match real world usage needs in
Backstage frontend plugins. For example, if the end use case is to show a
small summary in Backstage, but the only available API endpoint gives a 30
megabyte blob with large amounts of redundant information, it would hurt the
end user experience. Particularly on mobile. The same goes for cases where you
want to show many individual pieces of information: if a common use case is to
show large tables where one API request per cell is necessary, the browser
will quickly become swamped and you may want to consider performing
aggregation elsewhere instead.
- The API can maintain interactive request/response times at your required peak
request rates. The end user experience will be degraded if they spend a lot of
time waiting for the data to arrive.
- The API endpoint is highly available. The browser does not have builtin
facilities for load balancing, service discovery, retries, health checks,
circuit breaking and similar. If the endpoint is occasionally down even for
short periods of time (e.g. during deploys), end users will quickly notice.
- The API is exposed over HTTPS (not just HTTP), and properly handles
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). These are
requirements that the user's browser will impose for security reasons, and the
requests will be rejected otherwise.
- The API endpoint is easily reachable, in terms of network conditions, by end
users. This may be particularly relevant if your end users are outside of your
perimeter.
- The requests do not require secrets to be passed. This limitation does not
apply to OAuth tokens, which the frontend can negotiate and make proper use
of.
## Using The Backstage Proxy
Backstage has an optional proxy plugin for the backend, that can be used to
easily add proxy routes to downstream APIs.
Example:
```yaml
# In app-config.yaml
proxy:
'/frobs':
target: 'http://api.frobsco.com/v1'
changeOrigin: true
pathRewrite:
'^/proxy/frobs/': '/'
```
```ts
// Inside your component
const backendUrl = config.getString('backend.baseUrl');
fetch(`${backendUrl}/proxy/frobs/list`)
.then(response => response.json())
.then(payload => setFrobs(payload as Frob[]));
```
The proxy is powered by the `http-proxy-middleware` package, and supports all of
its
[configuration options](https://github.com/chimurai/http-proxy-middleware#options).
Internally at Spotify, the proxy option has been the overwhelmingly most popular
choice for plugin makers. Since we have DNS based service discovery in place and
a microservices framework that made it trivial to expose plain HTTP, it has been
a matter of just adding a few lines of Backstage config to get the benefit of
being easily and robustly reachable from users' web browsers as well.
This may be used instead of direct requests, when:
- You need to perform HTTPS termination and/or CORS handling, because the API
itself is not supplying those.
- You need to inject a simple static secret into the requests, e.g. an
Authorization header that gets added to the request headers.
- You want to make use of other proxy facilities, such as retries, failover,
health checks, routing, request logging, rewrites, etc.
- You already have the Backstage backend itself exposed through your perimeter
and find it practical to have only one entry point to deal with, governing
ingress with just the Backstage config.
## Creating a Backstage Backend Plugin
Much like the Backstage frontend, the Backstage backend also has a plugin
system. The above mentioned proxy is actually one such plugin. If you were in
need of a more involved integration than just direct access to the FrobsCo API,
or if you needed to hold state, you may want to make such a plugin.
Example:
```ts
// Inside your component
const backendUrl = config.getString('backend.baseUrl');
fetch(`${backendUrl}/frobs-aggregator/summary`)
.then(response => response.json())
.then(payload => setSummary(payload as FrobSummary));
```
```ts
// Inside a new frobs-aggregator backend plugin
router.use('/summary', async (req, res) => {
const agg = await Promise.all([
fetch('https://api.frobsco.com/v1/list'),
fetch('http://flerps.partnercompany.com:8080/flerp-batch'),
database.currentThunk(),
]).then(async ([frobs, flerps, thunk]) => {
return computeAggregate(await frobs.json(), await flerps.json(), thunk);
});
res.status(200).send(agg);
});
```
For a more detailed example, see
[the lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse)
that stores some state in a database and adds new capabilities to the underlying
API.
Internally at Spotify, this has been a fairly popular choice for different
reasons. Commonly, the backend has been used as a caching and data massaging
layer for slow APIs or APIs whose request/response shapes or speeds were not
acceptable for direct use by frontends. For example, this has made it possible
to issue efficient batch queries from the frontend, e.g. in big lists or tables
that want to resolve a lot of sparse data from the larger list that an
underlying service supplies.
This may be used instead of the above, when:
- You need to perform complex model conversion, or protocol translation beyond
what the proxy handles.
- You want to perform aggregations or summaries on the backend instead of on the
frontend.
- You want to enable batching or caching of slower or more unreliable APIs.
- You need to maintain state for your plugin, perhaps using the builtin database
support in the backend.
- You need to inject secrets or in other ways negotiate with other parts of the
API or other services in order to perform your work.
- You want to enforce end user authentication / authorization for operations on
behalf of the API, have session handling, or similar.
There is a balance to strike regarding when to make an entirely separate backend
for a purpose, and when to make a Backstage backend plugin that adapts something
that already exists. General advice is not easy to give, but contact us on
Discord if you have any questions, and we may be able to offer guidance.
## Extending the GraphQL Model
The extensible GraphQL backend layer is not built yet. This section will be
expanded when that happens. Stay tuned!
+2 -2
View File
@@ -55,7 +55,7 @@ ApiRef:
## githubAuth
Provides authentication towards Github APIs
Provides authentication towards GitHub APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
@@ -67,7 +67,7 @@ ApiRef:
## gitlabAuth
Provides authentication towards Gitlab APIs
Provides authentication towards GitLab APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
@@ -13,7 +13,7 @@ Two days ago, we released the open source version of [Backstage](https://backsta
## Whats the big infrastructure problem?
As companies grow, their infrastructure systems get messier. Consider a team that wants to deploy something to the cloud. While Spotify has many awesome engineers, not every engineer is well-versed in our chosen cloud-provider tooling. Yet everyone is required to know and understand Terraform, GCP/AWS/Azure CLIs, Gitlab CI, Prometheus, Kubernetes, Docker, various monitoring and alerting tools, and much, much more. Once other resources come into play (databases, queueing, etc.), each engineer requires even more tools and domain-specific knowledge (or “disciplines”), from backend to machine learning, to mobile and data.
As companies grow, their infrastructure systems get messier. Consider a team that wants to deploy something to the cloud. While Spotify has many awesome engineers, not every engineer is well-versed in our chosen cloud-provider tooling. Yet everyone is required to know and understand Terraform, GCP/AWS/Azure CLIs, GitLab CI, Prometheus, Kubernetes, Docker, various monitoring and alerting tools, and much, much more. Once other resources come into play (databases, queueing, etc.), each engineer requires even more tools and domain-specific knowledge (or “disciplines”), from backend to machine learning, to mobile and data.
## Whats the fix?
@@ -35,11 +35,11 @@ Since the templates can be customized to integrate with your existing infrastruc
### Golden Paths pave the way
You can customize Backstage Software Templates to fit your organizations standards. Using Go instead of Java? CircleCI instead of Jenkins? Serverless instead of Kubernetes? GCP instead of AWS? [Make your own recipes for any software component](/docs/features/software-templates/adding-templates.md) and your best practices will be baked right in.
You can customize Backstage Software Templates to fit your organizations standards. Using Go instead of Java? CircleCI instead of Jenkins? Serverless instead of Kubernetes? GCP instead of AWS? [Make your own recipes for any software component](https://backstage.io/docs/features/software-templates/adding-templates) and your best practices will be baked right in.
## Getting started
The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](/docs/getting-started/index.md) and go to `http://localhost:3000/create`. If youve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first.
The sample Software Templates are available under `/create`. If you're setting up Backstage for the first time, follow [Getting Started with Backstage](https://backstage.io/docs/getting-started/) and go to `http://localhost:3000/create`. If youve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first.
![available-templates](assets/2020-08-05/templates.png)
@@ -69,7 +69,7 @@ New components, of course, get added automatically to the Backstage Service Cata
## Define your standards
Backstage ships with four example templates, but since these are likely not the (only) ones you want to promote inside your company, the next step is to add [your own templates](/docs/features/software-templates/adding-templates.md). Using Backstages Software Templates feature, its easy to help your engineers get started building software with your organizations best practices built-in.
Backstage ships with four example templates, but since these are likely not the (only) ones you want to promote inside your company, the next step is to add [your own templates](https://backstage.io/docs/features/software-templates/software-templates-index). Using Backstages Software Templates feature, its easy to help your engineers get started building software with your organizations best practices built-in.
We have learned that one of the keys to getting these standards adopted is to keep an open process. Templates are code. By making it clear to your engineers that you are open to pull requests, and that teams with different needs can add their own templates, you are on the path of striking a good balance between autonomy and standardization.
+3
View File
@@ -50,6 +50,9 @@
"auth/add-auth-provider": {
"title": "Adding authentication providers"
},
"auth/auth-backend-classes": {
"title": "auth/auth-backend-classes"
},
"auth/auth-backend": {
"title": "Auth backend"
},
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.0",
"version": "0.1.1-alpha.19",
"name": "backstage-microsite",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -112,7 +112,7 @@ const Background = props => {
left: 0,
right: 0,
bottom: 0,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg);`,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg)`,
}}
/>
</div>
+14 -4
View File
@@ -353,10 +353,20 @@ class Index extends React.Component {
</Block.TextBox>
<Breakpoint
wide={
<img
style={{ margin: 'auto' }}
src={`${baseUrl}img/techdocs-web.png`}
/>
<Block.Graphics padding={0}>
<Block.Graphic
x={-55}
y={-5}
width={210}
src={`${baseUrl}img/techdocs-web.png`}
/>
<Block.Graphic
x={-55}
y={-5}
width={210}
src={`${baseUrl}img/techdocs2.gif`}
/>
</Block.Graphics>
}
/>
</Block.Container>
+8
View File
@@ -7,6 +7,14 @@
/* your custom css */
/* override font color for new dark tech docs styling */
table {
color: white;
}
table tr:nth-child(2n) {
background-color: #2b2b2b;
}
@media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+1 -1
View File
@@ -5,7 +5,7 @@
"node": "12"
},
"scripts": {
"dev": "concurrently 'yarn start' 'yarn start-backend'",
"dev": "concurrently \"yarn start\" \"yarn start-backend\"",
"start": "yarn workspace example-app start",
"start-backend": "yarn workspace example-backend start",
"build": "lerna run build",
+24 -23
View File
@@ -1,33 +1,34 @@
{
"name": "example-app",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/plugin-api-docs": "^0.1.1-alpha.18",
"@backstage/plugin-catalog": "^0.1.1-alpha.18",
"@backstage/plugin-circleci": "^0.1.1-alpha.18",
"@backstage/plugin-explore": "^0.1.1-alpha.18",
"@backstage/plugin-github-actions": "^0.1.1-alpha.18",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.18",
"@backstage/plugin-graphiql": "^0.1.1-alpha.18",
"@backstage/plugin-jenkins": "^0.1.1-alpha.18",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.18",
"@backstage/plugin-newrelic": "^0.1.1-alpha.18",
"@backstage/plugin-register-component": "^0.1.1-alpha.18",
"@backstage/plugin-rollbar": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.18",
"@backstage/plugin-sentry": "^0.1.1-alpha.18",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.18",
"@backstage/plugin-techdocs": "^0.1.1-alpha.18",
"@backstage/plugin-welcome": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/plugin-api-docs": "^0.1.1-alpha.19",
"@backstage/plugin-catalog": "^0.1.1-alpha.19",
"@backstage/plugin-circleci": "^0.1.1-alpha.19",
"@backstage/plugin-explore": "^0.1.1-alpha.19",
"@backstage/plugin-github-actions": "^0.1.1-alpha.19",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.19",
"@backstage/plugin-graphiql": "^0.1.1-alpha.19",
"@backstage/plugin-jenkins": "^0.1.1-alpha.19",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.19",
"@backstage/plugin-newrelic": "^0.1.1-alpha.19",
"@backstage/plugin-register-component": "^0.1.1-alpha.19",
"@backstage/plugin-rollbar": "^0.1.1-alpha.19",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.19",
"@backstage/plugin-sentry": "^0.1.1-alpha.19",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.19",
"@backstage/plugin-techdocs": "^0.1.1-alpha.19",
"@backstage/plugin-welcome": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^0.1.3",
"@roadiehq/backstage-plugin-github-pull-requests": "0.3.0",
"@roadiehq/backstage-plugin-travis-ci": "^0.1.4",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
+3
View File
@@ -27,6 +27,9 @@ describe('App', () => {
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
techdocs: {
storageUrl: 'http://localhost:7000/techdocs/static/docs',
},
},
context: 'test',
},
+25 -8
View File
@@ -58,6 +58,10 @@ import {
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
import {
techdocsStorageApiRef,
TechDocsStorageApi,
} from '@backstage/plugin-techdocs';
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
import {
@@ -66,13 +70,18 @@ import {
} from '@backstage/plugin-github-actions';
import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins';
import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci';
import {
TravisCIApi,
travisCIApiRef,
} from '@roadiehq/backstage-plugin-travis-ci';
import { GithubPullRequestsClient, githubPullRequestsApiRef } from '@roadiehq/backstage-plugin-github-pull-requests';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
const backendUrl = config.getString('backend.baseUrl');
const techdocsUrl = config.getString('techdocs.storageUrl');
const builder = ApiRegistry.builder();
@@ -97,6 +106,7 @@ export const apis = (config: ConfigApi) => {
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
builder.add(travisCIApiRef, new TravisCIApi());
builder.add(githubPullRequestsApiRef, new GithubPullRequestsClient());
const oauthRequestApi = builder.add(
oauthRequestApiRef,
@@ -106,7 +116,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
googleAuthApiRef,
GoogleAuth.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -115,7 +125,7 @@ export const apis = (config: ConfigApi) => {
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -124,7 +134,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
oktaAuthApiRef,
OktaAuth.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -133,16 +143,16 @@ export const apis = (config: ConfigApi) => {
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
auth0AuthApiRef,
Auth0Auth.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -151,7 +161,7 @@ export const apis = (config: ConfigApi) => {
builder.add(
oauth2ApiRef,
OAuth2.create({
apiOrigin: backendUrl,
backendUrl,
basePath: '/auth/',
oauthRequestApi,
}),
@@ -208,5 +218,12 @@ export const apis = (config: ConfigApi) => {
}),
);
builder.add(
techdocsStorageApiRef,
new TechDocsStorageApi({
apiOrigin: techdocsUrl,
}),
);
return builder.build();
};
+4 -4
View File
@@ -30,14 +30,14 @@ export const providers = [
},
{
id: 'gitlab-auth-provider',
title: 'Gitlab',
message: 'Sign In using Gitlab',
title: 'GitLab',
message: 'Sign In using GitLab',
apiRef: gitlabAuthApiRef,
},
{
id: 'github-auth-provider',
title: 'Github',
message: 'Sign In using Github',
title: 'GitHub',
message: 'Sign In using GitHub',
apiRef: githubAuthApiRef,
},
{
+1
View File
@@ -31,3 +31,4 @@ export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests';
+7 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config-loader": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/config-loader": "^0.1.1-alpha.19",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -43,7 +43,8 @@
"lodash": "^4.17.15",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"selfsigned": "^1.10.7"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -54,7 +55,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
@@ -19,7 +19,7 @@ import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { useHotCleanup } from '../../hot';
@@ -30,7 +30,13 @@ import {
requestLoggingHandler,
} from '../../middleware';
import { ServiceBuilder } from '../types';
import { readBaseOptions, readCorsOptions } from './config';
import {
readBaseOptions,
readCorsOptions,
readHttpsSettings,
HttpsSettings,
} from './config';
import { createHttpServer, createHttpsServer } from './hostFactory';
const DEFAULT_PORT = 7000;
// '' is express default, which listens to all interfaces
@@ -41,6 +47,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private httpsSettings: HttpsSettings | undefined;
private routers: [string, Router][];
// Reference to the module where builder is created - needed for hot module
// reloading
@@ -70,6 +77,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
this.corsOptions = corsOptions;
}
const httpsSettings = readHttpsSettings(backendConfig);
if (httpsSettings) {
this.httpsSettings = httpsSettings;
}
return this;
}
@@ -88,6 +100,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
setHttpsSettings(settings: HttpsSettings): ServiceBuilder {
this.httpsSettings = settings;
return this;
}
enableCors(options: cors.CorsOptions): ServiceBuilder {
this.corsOptions = options;
return this;
@@ -98,9 +115,15 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
start(): Promise<Server> {
start(): Promise<http.Server> {
const app = express();
const { port, host, logger, corsOptions } = this.getOptions();
const {
port,
host,
logger,
corsOptions,
httpsSettings,
} = this.getOptions();
app.use(helmet());
if (corsOptions) {
@@ -121,20 +144,24 @@ export class ServiceBuilderImpl implements ServiceBuilder {
reject(e);
});
const server = stoppable(
app.listen(port, host, () => {
const server: http.Server = httpsSettings
? createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
server.stop((e: any) => {
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(server);
resolve(stoppableServer);
});
}
@@ -143,12 +170,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
httpsSettings?: HttpsSettings;
} {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
httpsSettings: this.httpsSettings,
};
}
}
@@ -22,6 +22,41 @@ export type BaseOptions = {
listenHost?: string;
};
export type CertificateOptions = {
key?: CertificateKeyOptions;
attributes?: CertificateAttributeOptions;
};
export type CertificateKeyOptions = {
size?: number;
algorithm?: string;
days?: number;
};
export type CertificateAttributeOptions = {
commonName?: string;
};
export type HttpsSettings = {
certificate: CertificateSigningOptions | CertificateReferenceOptions;
};
export type CertificateReferenceOptions = {
key: string;
cert: string;
};
export type CertificateSigningOptions = {
algorithm: string;
size?: number;
days?: number;
attributes?: CertificateAttributes;
};
export type CertificateAttributes = {
commonName?: string;
};
/**
* Reads some base options out of a config object.
*
@@ -40,6 +75,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
if (typeof config.get('listen') === 'string') {
// TODO(freben): Expand this to support more addresses and perhaps optional
const { host, port } = parseListenAddress(config.getString('listen'));
return removeUnknown({
listenPort: port,
listenHost: host,
@@ -49,6 +85,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
return removeUnknown({
listenPort: config.getOptionalNumber('listen.port'),
listenHost: config.getOptionalString('listen.host'),
baseUrl: config.getOptionalString('baseUrl'),
});
}
@@ -86,6 +123,39 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
});
}
/**
* Attempts to read a https settings object from the root of a config object.
*
* @param config The root of a backend config object
* @returns A https settings object, or undefined if not specified
*
* @example
* ```json
* {
* https: {
* certificate: ...
* }
* }
* ```
*/
export function readHttpsSettings(
config: ConfigReader,
): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
if (!cc) {
return undefined;
}
const certificateConfig = cc.get('certificate');
const cfg = {
certificate: certificateConfig,
};
return removeUnknown(cfg as HttpsSettings);
}
function getOptionalStringOrStrings(
config: ConfigReader,
key: string,
@@ -0,0 +1,92 @@
/*
* 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 express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Logger } from 'winston';
import { HttpsSettings } from './config';
/**
* Creates a Http server instance based on an Express application.
*
* @param app The Express application object
* @param logger Optional Winston logger object
* @returns A Http server instance
*
*/
export function createHttpServer(
app: express.Express,
logger?: Logger,
): http.Server {
logger?.info('Initializing http server');
return http.createServer(app);
}
/**
* Creates a Https server instance based on an Express application.
*
* @param app The Express application object
* @param httpsSettings HttpsSettings for self-signed certificate generation
* @param logger Optional Winston logger object
* @returns A Https server instance
*
*/
export function createHttpsServer(
app: express.Express,
httpsSettings: HttpsSettings,
logger?: Logger,
): http.Server {
logger?.info('Initializing https server');
const credentials: { key: string; cert: string } = {
key: '',
cert: '',
};
const signingOptions: any = httpsSettings?.certificate;
if (signingOptions?.algorithm !== undefined) {
logger?.info('Generating self-signed certificate with attributes');
const certificateAttributes: Array<any> = Object.entries(
signingOptions.attributes,
).map(([name, value]) => ({ name, value }));
// TODO: Create a type def for selfsigned.
const signatures = require('selfsigned').generate(certificateAttributes, {
algorithm: signingOptions?.algorithm,
keySize: signingOptions?.size || 2048,
days: signingOptions?.days || 30,
});
logger?.info('Bootstrapping self-signed certificate');
credentials.key = signatures.private;
credentials.cert = signatures.cert;
} else {
logger?.info('Bootstrapping cert from config');
credentials.key = signingOptions?.key;
credentials.cert = signingOptions?.cert;
}
if (credentials.key === '' || credentials.cert === '') {
throw new Error('Invalid credentials');
}
return https.createServer(credentials, app) as http.Server;
}
@@ -19,6 +19,7 @@ import cors from 'cors';
import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
import { HttpsSettings } from './lib/config';
export type ServiceBuilder = {
/**
@@ -39,6 +40,15 @@ export type ServiceBuilder = {
*/
setPort(port: number): ServiceBuilder;
/**
* Sets the host to listen on.
*
* '' is express default, which listens to all interfaces.
*
* @param host The host to listen on
*/
setHost(host: string): ServiceBuilder;
/**
* Sets the logger to use for service-specific logging.
*
@@ -58,6 +68,15 @@ export type ServiceBuilder = {
*/
enableCors(options: cors.CorsOptions): ServiceBuilder;
/**
* Configure self-signed certificate generation options.
*
* If this method is not called, the resulting service will use sensible defaults
*
* @param options Standard certificate options
*/
setHttpsSettings(settings: HttpsSettings): ServiceBuilder;
/**
* Adds a router (similar to the express .use call) to the service.
*
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,18 +18,18 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.18",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.18",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.18",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.18",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.18",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.18",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.18",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.18",
"@backstage/backend-common": "^0.1.1-alpha.19",
"@backstage/catalog-model": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.19",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.19",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.19",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.19",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.19",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.19",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.19",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.19",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.19",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
@@ -40,7 +40,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+11 -6
View File
@@ -20,22 +20,27 @@ import {
Preparers,
Generators,
LocalPublish,
TechdocsGenerator
TechdocsGenerator,
GithubPreparer,
} from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger, config }: PluginEnvironment) {
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator();
const techdocsGenerator = new TechdocsGenerator(logger);
generators.register('techdocs', techdocsGenerator);
const directoryPreparer = new DirectoryPreparer();
const preparers = new Preparers();
const githubPreparer = new GithubPreparer(logger);
const directoryPreparer = new DirectoryPreparer(logger);
preparers.register('dir', directoryPreparer);
preparers.register('github', githubPreparer);
const publisher = new LocalPublish();
const publisher = new LocalPublish(logger);
const dockerClient = new Docker();
@@ -3,6 +3,9 @@ kind: Component
metadata:
name: artist-lookup
description: Artist Lookup
tags:
- java
- data
spec:
type: service
lifecycle: experimental
@@ -7,7 +7,7 @@ spec:
type: service
lifecycle: experimental
owner: pets@example.com
implementedApis:
implementsApis:
- petstore
- streetlights
- hello-world
@@ -3,6 +3,8 @@ kind: Component
metadata:
name: podcast-api
description: Podcast API
tags:
- java
spec:
type: service
lifecycle: experimental
@@ -3,6 +3,9 @@ kind: Component
metadata:
name: queue-proxy
description: Queue Proxy
tags:
- go
- website
spec:
type: website
lifecycle: production
@@ -3,6 +3,8 @@ kind: Component
metadata:
name: searcher
description: Searcher
tags:
- go
spec:
type: service
lifecycle: production
@@ -3,6 +3,8 @@ kind: Component
metadata:
name: shuffle-api
description: Shuffle API
tags:
- go
spec:
type: service
lifecycle: production
@@ -3,6 +3,8 @@ kind: API
metadata:
name: streetlights
description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
tags:
- unstable
spec:
type: asyncapi
definition: |
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -113,6 +113,12 @@ export type EntityMeta = JsonObject & {
* entity.
*/
annotations?: Record<string, string>;
/**
* A list of single-valued strings, to for example classify catalog entities in
* various ways.
*/
tags?: string[];
};
/**
@@ -35,6 +35,9 @@ describe('FieldFormatEntityPolicy', () => {
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
tags:
- java
- data-service
spec:
custom: stuff
`);
@@ -102,4 +105,9 @@ describe('FieldFormatEntityPolicy', () => {
data.metadata.annotations.a = 7;
await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i);
});
it('rejects bad tag value', async () => {
data.metadata.tags.push('Hello World');
await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i);
});
});
@@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
require(`annotations.${k}`, v, this.validators.isValidAnnotationValue);
}
const tags = entity.metadata.tags ?? [];
for (let i = 0; i < tags.length; ++i) {
require(`tags.${i}`, tags[i], this.validators.isValidTag);
}
return entity;
}
}
@@ -35,6 +35,9 @@ describe('ReservedFieldsEntityPolicy', () => {
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
tags:
- java
- data
spec:
custom: stuff
`);
@@ -29,6 +29,7 @@ const DEFAULT_RESERVED_ENTITY_FIELDS: string[] = [
'metadata.description',
'metadata.labels',
'metadata.annotations',
'metadata.tags',
// The below items are known to appear in core kinds, and therefore should
// not be appearing in metadata (which would indicate that the user made a
// mistake in where to place them).
@@ -36,6 +36,9 @@ describe('SchemaValidEntityPolicy', () => {
backstage.io/custom: ValueStuff
annotations:
example.com/bindings: are-secret
tags:
- java
- data
spec:
custom: stuff
`);
@@ -190,6 +193,11 @@ describe('SchemaValidEntityPolicy', () => {
await expect(policy.enforce(data)).rejects.toThrow(/annotations/);
});
it('rejects bad tags type', async () => {
data.metadata.tags = 7;
await expect(policy.enforce(data)).rejects.toThrow(/tags/);
});
//
// spec
//
@@ -31,6 +31,7 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({
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(),
@@ -28,6 +28,7 @@ const defaultValidators: Validators = {
isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue,
isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey,
isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue,
isValidTag: CommonValidatorFunctions.isValidDnsLabel,
};
export function makeValidator(overrides: Partial<Validators> = {}): Validators {
@@ -24,4 +24,5 @@ export type Validators = {
isValidLabelValue(value: any): boolean;
isValidAnnotationKey(value: any): boolean;
isValidAnnotationValue(value: any): boolean;
isValidTag(value: any): boolean;
};
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
+2 -2
View File
@@ -37,8 +37,8 @@ async function getConfig() {
// TODO: jest is working on module support, it's possible that we can remove this in the future
transform: {
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
'\\.(js|jsx|ts|tsx)': require.resolve('ts-jest'),
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)': require.resolve(
'\\.(js|jsx|ts|tsx)$': require.resolve('ts-jest'),
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve(
'./jestFileTransform.js',
),
},
+33 -3
View File
@@ -17,6 +17,7 @@
const os = require('os');
const fs = require('fs-extra');
const fetch = require('node-fetch');
const handlebars = require('handlebars');
const killTree = require('tree-kill');
const { resolve: resolvePath, join: joinPath } = require('path');
const Browser = require('zombie');
@@ -62,6 +63,35 @@ async function buildDistWorkspace(workspaceName, rootDir) {
const workspaceDir = resolvePath(rootDir, workspaceName);
await fs.ensureDir(workspaceDir);
// We grab the needed dependencies from the template packages
const appPkgTemplate = await fs.readFile(
resolvePath(
__dirname,
'../../create-app/templates/default-app/packages/app/package.json.hbs',
),
'utf8',
);
const appPkg = JSON.parse(
handlebars.compile(appPkgTemplate)({ version: '0.0.0' }),
);
const appDeps = Object.keys(appPkg.dependencies).filter(name =>
name.startsWith('@backstage/'),
);
const backendPkgTemplate = await fs.readFile(
resolvePath(
__dirname,
'../../create-app/templates/default-app/packages/backend/package.json.hbs',
),
'utf8',
);
const backendPkg = JSON.parse(
handlebars.compile(backendPkgTemplate)({ version: '0.0.0' }),
);
const backendDeps = Object.keys(backendPkg.dependencies).filter(name =>
name.startsWith('@backstage/'),
);
print(`Preparing workspace`);
await runPlain([
'yarn',
@@ -73,8 +103,8 @@ async function buildDistWorkspace(workspaceName, rootDir) {
'@backstage/core',
'@backstage/dev-utils',
'@backstage/test-utils',
// We don't use the backend itself, but want all of its dependencies
'example-backend',
...appDeps,
...backendDeps,
]);
print('Pinning yarn version in workspace');
@@ -246,7 +276,7 @@ async function testAppServe(pluginName, appDir) {
try {
const browser = new Browser();
await waitForPageWithText(browser, '/', 'Welcome to Backstage');
await waitForPageWithText(browser, '/', 'Backstage Service Catalog');
await waitForPageWithText(
browser,
`/${pluginName}`,
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public"
@@ -29,9 +29,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config-loader": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/config-loader": "^0.1.1-alpha.19",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -41,8 +41,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/test-utils-core": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/test-utils-core": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -222,7 +222,7 @@ export const googleAuthApiRef = createApiRef<
});
/**
* Provides authentication towards Github APIs.
* Provides authentication towards GitHub APIs.
*
* See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
* for a full list of supported scopes.
@@ -231,7 +231,7 @@ export const githubAuthApiRef = createApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.github',
description: 'Provides authentication towards Github APIs',
description: 'Provides authentication towards GitHub APIs',
});
/**
@@ -252,7 +252,7 @@ export const oktaAuthApiRef = createApiRef<
});
/**
* Provides authentication towards Gitlab APIs.
* Provides authentication towards GitLab APIs.
*
* 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.
@@ -261,7 +261,7 @@ export const gitlabAuthApiRef = createApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.gitlab',
description: 'Provides authentication towards Gitlab APIs',
description: 'Provides authentication towards GitLab APIs',
});
/**
@@ -270,7 +270,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<OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi>({
export const auth0AuthApiRef = createApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.auth0',
description: 'Provides authentication towards Auth0 APIs',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { wait } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import { OAuthPendingRequests } from './OAuthPendingRequests';
describe('OAuthPendingRequests', () => {
@@ -27,7 +27,7 @@ describe('OAuthPendingRequests', () => {
target.pending().subscribe({ next, error });
target.request(input);
await wait(() => expect(next).toBeCalledTimes(2));
await waitFor(() => expect(next).toBeCalledTimes(2));
expect(next.mock.calls[0][0].scopes).toBeUndefined();
expect(next.mock.calls[1][0].scopes.toString()).toBe(input.toString());
expect(error.mock.calls.length).toBe(0);
@@ -34,7 +34,7 @@ import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Following the words of Rugvip): These two should be grabbed from global config when available, they're not unique to Auth0Auth
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -67,14 +67,14 @@ class Auth0Auth
BackstageIdentityApi,
SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -96,11 +96,7 @@ class Auth0Auth
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
`email`,
`profile`,
]),
defaultScopes: new Set(['openid', `email`, `profile`]),
sessionScopes: (session: Auth0Session) => session.providerInfo.scopes,
sessionShouldRefresh: (session: Auth0Session) => {
const expiresInSec =
@@ -35,7 +35,7 @@ import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -62,14 +62,14 @@ const DEFAULT_PROVIDER = {
class GithubAuth implements OAuthApi, SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -31,7 +31,7 @@ import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -58,14 +58,14 @@ const DEFAULT_PROVIDER = {
class GitlabAuth implements OAuthApi, SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -35,7 +35,7 @@ import { Observable } from '../../../../types';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -71,14 +71,14 @@ class GoogleAuth
BackstageIdentityApi,
SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -33,7 +33,7 @@ import {
import { OAuth2Session } from './types';
type CreateOptions = {
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -64,14 +64,14 @@ const SCOPE_PREFIX = '';
class OAuth2
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -34,7 +34,7 @@ import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager
import { Observable } from '../../../../types';
type CreateOptions = {
apiOrigin: string;
backendUrl: string;
basePath: string;
oauthRequestApi: OAuthRequestApi;
@@ -60,28 +60,34 @@ const DEFAULT_PROVIDER = {
icon: OktaIcon,
};
const OKTA_OIDC_SCOPES: Set<String> = new Set(
['openid', 'profile', 'email', 'phone', 'address', 'groups', 'offline_access']
)
const OKTA_OIDC_SCOPES: Set<String> = new Set([
'openid',
'profile',
'email',
'phone',
'address',
'groups',
'offline_access',
]);
const OKTA_SCOPE_PREFIX: string = 'okta.'
const OKTA_SCOPE_PREFIX: string = 'okta.';
class OktaAuth implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi
{
class OktaAuth
implements
OAuthApi,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionStateApi {
static create({
apiOrigin,
backendUrl,
basePath,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
const connector = new DefaultAuthConnector({
apiOrigin,
backendUrl,
basePath,
environment,
provider,
@@ -103,12 +109,7 @@ class OktaAuth implements
const sessionManager = new RefreshingAuthSessionManager({
connector,
defaultScopes: new Set([
'openid',
'email',
'profile',
'offline_access',
]),
defaultScopes: new Set(['openid', 'email', 'profile', 'offline_access']),
sessionScopes: session => session.scopes,
sessionShouldRefresh: session => {
const expiresInSec =
@@ -126,10 +127,7 @@ class OktaAuth implements
constructor(private readonly sessionManager: SessionManager<OktaSession>) {}
async getAccessToken(
scope?: string,
options?: AuthRequestOptions
) {
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
const session = await this.sessionManager.getSession({
...options,
scopes: OktaAuth.normalizeScopes(scope),
@@ -175,12 +173,12 @@ class OktaAuth implements
if (scope.startsWith(OKTA_SCOPE_PREFIX)) {
return scope;
}
return `${OKTA_SCOPE_PREFIX}${scope}`
return `${OKTA_SCOPE_PREFIX}${scope}`;
});
return new Set(normalizedScopes);
}
}
export default OktaAuth;
export default OktaAuth;
@@ -22,7 +22,7 @@ import * as loginPopup from '../loginPopup';
const anyFetch = fetch as any;
const defaultOptions = {
apiOrigin: 'my-origin',
backendUrl: 'http://my-origin',
environment: 'production',
provider: {
id: 'my-provider',
@@ -114,7 +114,8 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url: 'my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
url:
'http://my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
});
await expect(sessionPromise).resolves.toEqual({
@@ -162,7 +163,8 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url: 'my-origin/api/auth/my-provider/start?scope=-ab-&env=production',
url:
'http://my-origin/api/auth/my-provider/start?scope=-ab-&env=production',
});
});
});
@@ -23,9 +23,9 @@ const DEFAULT_BASE_PATH = '/api/auth/';
type Options<AuthSession> = {
/**
* Origin of auth requests, defaults to location.origin
* The base URL of the auth backend.
*/
apiOrigin?: string;
backendUrl?: string;
/**
* Base path of the auth requests, defaults to /api/auth/
*/
@@ -64,7 +64,7 @@ function defaultJoinScopes(scopes: Set<string>) {
*/
export class DefaultAuthConnector<AuthSession>
implements AuthConnector<AuthSession> {
private readonly apiOrigin: string;
private readonly backendUrl: string;
private readonly basePath: string;
private readonly environment: string;
private readonly provider: AuthProvider & { id: string };
@@ -74,7 +74,7 @@ export class DefaultAuthConnector<AuthSession>
constructor(options: Options<AuthSession>) {
const {
apiOrigin = window.location.origin,
backendUrl = window.location.origin,
basePath = DEFAULT_BASE_PATH,
environment,
provider,
@@ -88,7 +88,7 @@ export class DefaultAuthConnector<AuthSession>
onAuthRequest: scopes => this.showPopup(scopes),
});
this.apiOrigin = apiOrigin;
this.backendUrl = backendUrl;
this.basePath = basePath;
this.environment = environment;
this.provider = provider;
@@ -154,11 +154,12 @@ export class DefaultAuthConnector<AuthSession>
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
const scope = this.joinScopesFunc(scopes);
const popupUrl = this.buildUrl('/start', { scope });
const { origin } = new URL(this.backendUrl);
const payload = await showLoginPopup({
url: popupUrl,
name: `${this.provider.title} Login`,
origin: this.apiOrigin,
origin,
width: 450,
height: 730,
});
@@ -175,7 +176,7 @@ export class DefaultAuthConnector<AuthSession>
env: this.environment,
});
return `${this.apiOrigin}${this.basePath}${this.provider.id}${path}${queryString}`;
return `${this.backendUrl}${this.basePath}${this.provider.id}${path}${queryString}`;
}
private buildQueryString(query?: {
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/core-api": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/core-api": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -50,12 +50,12 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^13.2.1",
"react-syntax-highlighter": "^13.5.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+2 -1
View File
@@ -127,7 +127,8 @@ function convertColumns<T extends object>(
): TableColumn<T>[] {
return columns.map(column => {
const headerStyle: React.CSSProperties = {};
const cellStyle: React.CSSProperties = {};
const cellStyle: React.CSSProperties =
typeof column.cellStyle === 'object' ? column.cellStyle : {};
if (column.highlight) {
headerStyle.color = theme.palette.textContrast;
+4 -4
View File
@@ -48,7 +48,7 @@ export interface TabsProps {
tabs: TabProps[];
}
const useStyles = makeStyles<BackstageTheme>((theme: BackstageTheme) => ({
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
flexGrow: 1,
width: '100%',
@@ -66,7 +66,7 @@ const useStyles = makeStyles<BackstageTheme>((theme: BackstageTheme) => ({
export const Tabs: FC<TabsProps> = ({ tabs }) => {
const classes = useStyles();
const [value, setValue] = useState([0, 0]); // [selectedChunckedNavIndex, selectedIndex]
const [value, setValue] = useState([0, 0]); // [selectedChunkedNavIndex, selectedIndex]
const [navIndex, setNavIndex] = useState(0);
const [numberOfChunkedElement, setNumberOfChunkedElement] = useState(0);
const [chunkedTabs, setChunkedTabs] = useState<TabProps[][]>([[]]);
@@ -89,7 +89,7 @@ export const Tabs: FC<TabsProps> = ({ tabs }) => {
const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length;
useEffect(() => {
// Each time the window is resized we calculate how many tabs wwe can render given the window width
// Each time the window is resized we calculate how many tabs we can render given the window width
const padding = 20; // The AppBar padding
const numberOfTabIcons = navIndex === 0 ? 1 : 2;
@@ -99,7 +99,7 @@ export const Tabs: FC<TabsProps> = ({ tabs }) => {
const newChunkedElementSize = Math.floor(wrapperWidth / 170);
setNumberOfChunkedElement(newChunkedElementSize);
setChunkedTabs(chunkArray([...tabs], newChunkedElementSize));
setChunkedTabs(chunkArray(tabs, newChunkedElementSize));
setValue([
Math.floor(flattenIndex / newChunkedElementSize),
flattenIndex % newChunkedElementSize,
+11 -10
View File
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TabProps } from './Tabs';
export const chunkArray = (
myArray: TabProps[],
chunkSize: number,
): TabProps[][] => {
const results = [];
while (myArray.length) {
results.push(myArray.splice(0, chunkSize));
export function chunkArray<T>(array: T[], chunkSize: number): T[][] {
if (chunkSize <= 0) {
return [array];
}
return results;
};
const result: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
+68 -68
View File
@@ -14,59 +14,63 @@
* limitations under the License.
*/
import React, { Fragment, ReactNode, CSSProperties, FC } from 'react';
import React, { ReactNode, CSSProperties, FC, useContext } from 'react';
import { Helmet } from 'react-helmet';
import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { Theme } from '../Page/Page';
import { Waves } from './Waves';
import { PageThemeContext } from '../Page/Page';
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
},
leftItemsBox: {
flex: '1 1 auto',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: theme.spacing(1),
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
},
subtitle: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '1.0em',
},
type: {
textTransform: 'uppercase',
fontSize: 11,
opacity: 0.8,
marginBottom: theme.spacing(1),
color: theme.palette.bursts.fontColor,
},
}));
const useStyles = makeStyles<BackstageTheme, { backgroundImage: string }>(
theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
backgroundImage: props => props.backgroundImage,
backgroundPosition: 'center',
backgroundSize: '100% 400px',
},
leftItemsBox: {
flex: '1 1 auto',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: theme.spacing(1),
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
},
subtitle: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '1.0em',
},
type: {
textTransform: 'uppercase',
fontSize: 11,
opacity: 0.8,
marginBottom: theme.spacing(1),
color: theme.palette.bursts.fontColor,
},
}),
);
type HeaderStyles = ReturnType<typeof useStyles>;
@@ -159,32 +163,28 @@ export const Header: FC<Props> = ({
type,
typeLink,
}) => {
const classes = useStyles();
const theme = useContext(PageThemeContext);
const classes = useStyles({ backgroundImage: theme.backgroundImage });
const documentTitle = pageTitleOverride || title;
const pageTitle = title || pageTitleOverride;
const titleTemplate = `${documentTitle} | %s | Backstage`;
const defaultTitle = `${documentTitle} | Backstage`;
return (
<Fragment>
<>
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle} />
<Theme.Consumer>
{theme => (
<header style={style} className={classes.header}>
<Waves theme={theme} />
<div className={classes.leftItemsBox}>
<TypeFragment classes={classes} type={type} typeLink={typeLink} />
<TitleFragment
classes={classes}
pageTitle={pageTitle}
tooltip={tooltip}
/>
<SubtitleFragment classes={classes} subtitle={subtitle} />
</div>
<div className={classes.rightItemsBox}>{children}</div>
</header>
)}
</Theme.Consumer>
</Fragment>
<header style={style} className={classes.header}>
<div className={classes.leftItemsBox}>
<TypeFragment classes={classes} type={type} typeLink={typeLink} />
<TitleFragment
classes={classes}
pageTitle={pageTitle}
tooltip={tooltip}
/>
<SubtitleFragment classes={classes} subtitle={subtitle} />
</div>
<div className={classes.rightItemsBox}>{children}</div>
</header>
</>
);
};
-148
View File
@@ -1,148 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import { PageTheme } from '../Page';
const useStyles = makeStyles<PageTheme>({
wave: {
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: -1,
},
});
type Props = {
theme: PageTheme;
};
export const Waves: FC<Props> = ({ theme }) => {
const classes = useStyles();
const [backgroundColor1, backgroundColor2] = theme.gradient.colors;
const waveColor = theme.gradient.waveColor;
const [opacityStart, opacityStop] = theme.gradient.opacity;
return (
<svg
viewBox="0 0 1440 94"
preserveAspectRatio="xMinYMin slice"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={classes.wave}
data-testid="wave-svg"
>
<rect width="1440" height="94" fill="url(#headerGradient)" />
<g opacity="0.8">
<mask
id="mask0"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="1440"
height="94"
>
<rect width="1440" height="94" fill="#1F8A77" />
</mask>
<g mask="url(#mask0)">
<path
d="M710.947 14.3685C605.39 154.457 195.464 165.759 92 154.301V210.649L2330 287V222.126C1516.52 222.126 1394.17 80.1091 1161.6 -40.9544C926.122 -163.528 768.278 -61.7182 710.947 14.3685Z"
fill="url(#paint1_linear)"
/>
<path
d="M1163.25 124.34C1058.49 -16.113 651.678 -27.4441 549 -15.9565V-72.4504L2770 -149V-83.9579C1962.69 -83.9579 1841.28 58.4283 1610.47 179.806C1376.79 302.698 1220.14 200.624 1163.25 124.34Z"
fill="url(#paint2_linear)"
/>
<path
d="M-104.701 -5.00477C-224.644 62.3106 -690.435 67.7413 -808 62.2356V89.3117L1735 126V94.827C810.651 94.827 671.63 26.5849 407.363 -31.5885C139.798 -90.4876 -39.557 -41.5659 -104.701 -5.00477Z"
fill="url(#paint3_linear)"
/>
<path
d="M-247.511 -32.6315C-338.493 107.457 -691.821 118.759 -781 107.301V163.649L1148 240V175.126C446.833 175.126 341.378 33.1091 140.917 -87.9544C-62.0447 -210.528 -198.095 -108.718 -247.511 -32.6315Z"
fill="url(#paint4_linear)"
/>
</g>
</g>
<defs>
<linearGradient id="headerGradient">
<stop offset="0%" stopColor={backgroundColor1} />
<stop offset="100" stopColor={backgroundColor2} />
</linearGradient>
<linearGradient
id="paint0_linear"
x1="7.21415e-07"
y1="61"
x2="1649"
y2="59.5001"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={waveColor} stopOpacity={opacityStart} />
<stop offset="1" stopColor={waveColor} stopOpacity={opacityStop} />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="1318.25"
y1="-285.884"
x2="1311.42"
y2="241.925"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={waveColor} stopOpacity={opacityStart} />
<stop offset="1" stopColor={waveColor} stopOpacity={opacityStop} />
</linearGradient>
<linearGradient
id="paint2_linear"
x1="1765.93"
y1="425.372"
x2="1759.02"
y2="-103.806"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={waveColor} stopOpacity={opacityStart} />
<stop offset="1" stopColor={waveColor} stopOpacity={opacityStop} />
</linearGradient>
<linearGradient
id="paint3_linear"
x1="585.365"
y1="-149.282"
x2="583.977"
y2="104.375"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={waveColor} stopOpacity={opacityStart} />
<stop offset="1" stopColor={waveColor} stopOpacity={opacityStop} />
</linearGradient>
<linearGradient
id="paint4_linear"
x1="275.941"
y1="-332.884"
x2="268.016"
y2="194.894"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={waveColor} stopOpacity={opacityStart} />
<stop offset="1" stopColor={waveColor} stopOpacity={opacityStop} />
</linearGradient>
</defs>
</svg>
);
};
@@ -0,0 +1,50 @@
/*
* 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 { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { HeaderTabs } from './';
const mockTabs = [
{ id: 'overview', label: 'Overview' },
{ id: 'docs', label: 'Docs' },
];
describe('<HeaderTabs />', () => {
it('should render tabs', () => {
const rendered = render(wrapInTestApp(<HeaderTabs tabs={mockTabs} />));
expect(rendered.getByText('Overview')).toBeInTheDocument();
expect(rendered.getByText('Docs')).toBeInTheDocument();
});
it('should render correct selected tab', () => {
const rendered = render(wrapInTestApp(<HeaderTabs tabs={mockTabs} />));
expect(rendered.getByText('Docs').parentElement).toHaveAttribute(
'aria-selected',
'false',
);
rendered.getByText('Docs').click();
expect(rendered.getByText('Docs').parentElement).toHaveAttribute(
'aria-selected',
'true',
);
});
});
+13 -4
View File
@@ -17,7 +17,7 @@
// TODO(blam): Remove this implementation when the Tabs are ready
// This is just a temporary solution to implementing tabs for now
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { makeStyles, Tabs, Tab } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
@@ -46,15 +46,24 @@ export type Tab = {
export const HeaderTabs: React.FC<{
tabs: Tab[];
onChange?: (index: number) => void;
}> = ({ tabs, onChange }) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
selectedIndex?: number;
}> = ({ tabs, onChange, selectedIndex }) => {
const [selectedTab, setSelectedTab] = useState<number>(selectedIndex ?? 0);
const styles = useStyles();
const handleChange = (_: React.ChangeEvent<{}>, index: number) => {
setSelectedTab(index);
if (selectedIndex === undefined) {
setSelectedTab(index);
}
if (onChange) onChange(index);
};
useEffect(() => {
if (selectedIndex !== undefined) {
setSelectedTab(selectedIndex);
}
}, [selectedIndex]);
return (
<div className={styles.tabsWrapper}>
<Tabs
@@ -38,7 +38,7 @@ const useStyles = makeStyles(theme => ({
}));
type ItemCardProps = {
description: string;
description?: string;
tags?: string[];
title: string;
type?: string;
+3 -3
View File
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
import { PageTheme, pageTheme } from './PageThemeProvider';
import { makeStyles } from '@material-ui/core';
export const Theme = React.createContext<PageTheme>(pageTheme.home);
export const PageThemeContext = React.createContext<PageTheme>(pageTheme.home);
const useStyles = makeStyles(() => ({
root: {
@@ -38,8 +38,8 @@ type Props = {
export const Page: FC<Props> = ({ theme = pageTheme.home, children }) => {
const classes = useStyles();
return (
<Theme.Provider value={theme}>
<PageThemeContext.Provider value={theme}>
<div className={classes.root}>{children}</div>
</Theme.Provider>
</PageThemeContext.Provider>
);
};
@@ -14,87 +14,59 @@
* limitations under the License.
*/
export type Gradient = {
colors: string[];
waveColor: string;
opacity: string[];
};
export type PageTheme = {
gradient: Gradient;
colors: string[];
shape: string;
backgroundImage: string;
};
export const gradients: Record<string, Gradient> = {
darkGrey: {
colors: ['#171717', '#383838'],
waveColor: '#757575',
opacity: ['1.0', '0.0'],
},
marineBlue: {
colors: ['#00759A', '#004EAC'],
waveColor: '#BDDBFF',
opacity: ['0.72', '0.0'],
},
veryBlue: {
colors: ['#0B2B9C', '#311288'],
waveColor: '#8960FD',
opacity: ['0.72', '0.0'],
},
rubyRed: {
colors: ['#A4284B', '#8D1134'],
waveColor: '#FFBFF5',
opacity: ['0.28', '0.10'],
},
toastyOrange: {
colors: ['#CC3707', '#9A2500'],
waveColor: '#FF784E',
opacity: ['0.72', '0.0'],
},
purpleSky: {
colors: ['#AF29F8', '#4100F4'],
waveColor: '#AF29F8',
opacity: ['0.72', '0.0'],
},
eveningSea: {
colors: ['#00FFF2', '#035355'],
waveColor: '',
opacity: ['0.72', '0.0'],
},
teal: {
colors: ['#005E4D', '#004E40'],
waveColor: '#9BF0E1',
opacity: ['0.72', '0.0'],
},
pinkSea: {
colors: ['#C8077A', '#C2297D'],
waveColor: '#ea93c3',
opacity: ['0.8', '0.0'],
},
/*
# How to add a shape
1. Get the svg shape from figma, should be ~1400 wide, ~400 high
and only the white->transparent mask, no colors.
2. Run it through https://jakearchibald.github.io/svgomg/
3. Run that through https://github.com/tigt/mini-svg-data-uri
with something like https://npm.runkit.com/mini-svg-data-uri
4. Wrap the output in `url("")`
5. Give it a name and paste it into the `shapes` object below.
*/
export const shapes: Record<string, string> = {
wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`,
wave2: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1764' height='479' x='-229' y='-6' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M0 400h1350C1321 336 525 33 179-2c-345-34-395 236-408 402H0z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1378 177v223H0V217s219 75 327 52C436 246 717-35 965 45s254 144 413 132z'/%3e%3cpath fill='url(%23paint2_linear)' d='M26 400l-78-16c-170 205-44-6-137-30l-4-1 4 1 137 30c37-45 89-110 159-201 399-514-45 238 1176-50 275-65 354-39 91 267H26z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='431' x2='397.3' y1='-599' y2='372.8' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='236.5' x2='446.6' y1='-586' y2='381.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='851.8' x2='640.4' y1='-867.2' y2='363.7' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`,
round: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='2269' height='1408' x='-610' y='-509' maskUnits='userSpaceOnUse'%3e%3ccircle cx='1212.8' cy='74.8' r='317.5' fill='url(%23paint0_linear)' transform='rotate(-52 1213 75)'/%3e%3ccircle cx='737.8' cy='445.8' r='317.5' fill='url(%23paint1_linear)' transform='rotate(-116 738 446)'/%3e%3ccircle cx='601.8' cy='52.8' r='418.6' fill='url(%23paint2_linear)' transform='rotate(-117 602 53)'/%3e%3ccircle cx='999.8' cy='364' r='389.1' fill='url(%23paint3_linear)' transform='rotate(31 1000 364)'/%3e%3cellipse cx='-109.2' cy='263.5' fill='url(%23paint4_linear)' rx='429.2' ry='465.8' transform='rotate(-85 -109 264)'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='1301.2' x2='161.4' y1='-1879.7' y2='-969.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='826.2' x2='-313.6' y1='-1508.7' y2='-598.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='718.4' x2='-784.3' y1='-2524' y2='-1324.2' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='1108.2' x2='-288.6' y1='-2031.1' y2='-915.9' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint4_linear' x1='10.4' x2='-1626.5' y1='-2603.8' y2='-1399.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`,
};
export const colorVariants: Record<string, string[]> = {
darkGrey: ['#171717', '#383838'],
marineBlue: ['#006D8F', '#0049A1'],
veryBlue: ['#0027AF', '#270094'],
rubyRed: ['#98002B', '#8D1134'],
toastyOrange: ['#BE2200', '#A41D00'],
purpleSky: ['#8912CA', '#3E00EA'],
eveningSea: ['#00FFF2', '#035355'],
teal: ['#005B4B'],
pinkSea: ['#C8077A', '#C2297D'],
};
export const pageTheme: Record<string, PageTheme> = {
home: {
gradient: gradients.teal,
},
documentation: {
gradient: gradients.pinkSea,
},
tool: {
gradient: gradients.purpleSky,
},
service: {
gradient: gradients.marineBlue,
},
website: {
gradient: gradients.veryBlue,
},
library: {
gradient: gradients.rubyRed,
},
other: {
gradient: gradients.darkGrey,
},
app: {
gradient: gradients.toastyOrange,
},
home: genTheme(colorVariants.teal, shapes.wave),
documentation: genTheme(colorVariants.pinkSea, shapes.wave2),
tool: genTheme(colorVariants.purpleSky, shapes.round),
service: genTheme(colorVariants.marineBlue, shapes.wave),
website: genTheme(colorVariants.veryBlue, shapes.wave),
library: genTheme(colorVariants.rubyRed, shapes.wave),
other: genTheme(colorVariants.darkGrey, shapes.wave),
app: genTheme(colorVariants.toastyOrange, shapes.wave),
};
// As the background shapes and colors are decorative, we place them onto
// the page as a css background-image instead of an html element of its own.
// Utility to not have to write colors and shapes twice.
function genTheme(colors: string[], shape: string) {
const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors;
const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`;
const backgroundImage = `${shape}, ${gradient}`;
return { colors, shape, backgroundImage };
}
+1 -1
View File
@@ -15,5 +15,5 @@
*/
export { Page } from './Page';
export { gradients, pageTheme } from './PageThemeProvider';
export { pageTheme } from './PageThemeProvider';
export type { PageTheme } from './PageThemeProvider';
@@ -62,14 +62,14 @@ export function SidebarUserSettings() {
)}
{providers.includes('github') && (
<OAuthProviderSettings
title="Github"
title="GitHub"
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{providers.includes('gitlab') && (
<OAuthProviderSettings
title="Gitlab"
title="GitLab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
@@ -78,8 +78,7 @@ const loader: ProviderLoader = async apis => {
return {
userId: identity.id,
profile: profile!,
getIdToken: () =>
auth0AuthApi.getBackstageIdentity().then(i => i!.idToken),
getIdToken: () => auth0AuthApi.getBackstageIdentity().then(i => i!.idToken),
logout: async () => {
await auth0AuthApi.logout();
},
@@ -29,7 +29,8 @@ import { InfoCard } from '../InfoCard/InfoCard';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { GridItem } from './styles';
const ID_TOKEN_REGEX = /^[a-z0-9+/]+\.[a-z0-9+/]+\.[a-z0-9+/]+$/i;
// accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3)
const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i;
const useFormStyles = makeStyles(theme => ({
form: {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"chalk": "^4.0.0",
"commander": "^4.1.1",
"fs-extra": "^9.0.0",
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Coverage directory generated when running tests with coverage
coverage
# Dependencies
node_modules/
# Node version directives
.nvmrc
# dotenv environment variables file
.env
.env.test
# Build output
dist
# Temporary change files created by Vim
*.swp
# MkDocs build output
site
# Local configuration files
*.local.yaml
@@ -24,3 +24,35 @@ techdocs:
auth:
providers: {}
catalog:
locations:
# Backstage Example Component
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
# Backstage Example Templates
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
- type: github
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
@@ -1,8 +1,6 @@
describe('App', () => {
it('should render the welcome page', () => {
it('should render the catalog', () => {
cy.visit('/');
cy.contains('Welcome to Backstage');
cy.contains('Getting Started');
cy.contains('Quick Links');
cy.contains('Backstage Service Catalog');
});
});
@@ -5,13 +5,14 @@
"dependencies": {
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
"@backstage/plugin-catalog": "^{{version}}",
"@backstage/plugin-register-component": "^{{version}}",
"@backstage/plugin-scaffolder": "^{{version}}",
"@backstage/test-utils": "^{{version}}",
"@backstage/theme": "^{{version}}",
"history": "^5.0.0",
"plugin-welcome": "0.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
@@ -10,6 +10,7 @@ describe('App', () => {
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
},
context: 'test',
},
@@ -1,8 +1,16 @@
import { createApp } from '@backstage/core';
import React, { FC } from 'react';
import {
createApp,
AlertDisplay,
OAuthRequestDialog,
SidebarPage,
} from '@backstage/core';
import { apis } from './apis';
import * as plugins from './plugins';
import { AppSidebar } from './sidebar';
const app = createApp({
apis,
plugins: Object.values(plugins),
});
@@ -12,8 +20,13 @@ const AppRoutes = app.getRoutes();
const App: FC<{}> = () => (
<AppProvider>
<AlertDisplay />
<OAuthRequestDialog />
<AppRouter>
<AppRoutes />
<SidebarPage>
<AppSidebar />
<AppRoutes />
</SidebarPage>
</AppRouter>
</AppProvider>
);
@@ -0,0 +1,53 @@
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ConfigApi,
ErrorApiForwarder,
ErrorAlerter,
oauthRequestApiRef,
OAuthRequestManager,
storageApiRef,
WebStorage,
} from '@backstage/core';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
const backendUrl = config.getString('backend.baseUrl');
const builder = ApiRegistry.builder();
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
);
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(oauthRequestApiRef, new OAuthRequestManager());
builder.add(
catalogApiRef,
new CatalogClient({
apiOrigin: backendUrl,
basePath: '/catalog',
}),
);
builder.add(
scaffolderApiRef,
new ScaffolderApi({
apiOrigin: backendUrl,
basePath: '/scaffolder/v1',
}),
);
return builder.build();
};
@@ -1 +1,3 @@
export { plugin as WelcomePlugin } from 'plugin-welcome';
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
@@ -0,0 +1,28 @@
import React from 'react';
import HomeIcon from '@material-ui/icons/Home';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import {
Sidebar,
SidebarItem,
SidebarDivider,
SidebarSpace,
SidebarUserSettings,
SidebarThemeToggle,
SidebarPinButton,
} from '@backstage/core';
export const AppSidebar = () => (
<Sidebar>
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="./" text="Home" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
{/* End global nav */}
<SidebarDivider />
<SidebarSpace />
<SidebarDivider />
<SidebarThemeToggle />
<SidebarUserSettings />
<SidebarPinButton />
</Sidebar>
);
@@ -14,15 +14,15 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator();
const techdocsGenerator = new TechdocsGenerator(logger);
generators.register('techdocs', techdocsGenerator);
const directoryPreparer = new DirectoryPreparer();
const directoryPreparer = new DirectoryPreparer(logger);
const preparers = new Preparers();
preparers.register('dir', directoryPreparer);
const publisher = new LocalPublish();
const publisher = new LocalPublish(logger);
const dockerClient = new Docker();
@@ -1,3 +0,0 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
@@ -1,3 +0,0 @@
# Title
Welcome to the welcome plugin!
@@ -1,4 +0,0 @@
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
@@ -1,41 +0,0 @@
{
"name": "plugin-welcome",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": true,
"publishConfig": {
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^{{version}}",
"@backstage/theme": "^{{version}}",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3",
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@backstage/dev-utils": "^{{version}}",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"jest-fetch-mock": "^3.0.3"
},
"files": [
"dist"
]
}
@@ -1,58 +0,0 @@
import React, { FC } from 'react';
import { HeaderLabel } from '@backstage/core';
const timeFormat = { hour: '2-digit', minute: '2-digit' };
const utcOptions = { timeZone: 'UTC', ...timeFormat };
const nycOptions = { timeZone: 'America/New_York', ...timeFormat };
const tyoOptions = { timeZone: 'Asia/Tokyo', ...timeFormat };
const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat };
const defaultTimes = {
timeNY: '',
timeUTC: '',
timeTYO: '',
timeSTO: '',
};
function getTimes() {
const d = new Date();
const lang = window.navigator.language;
// Using the browser native toLocaleTimeString instead of huge moment-tz
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString
const timeNY = d.toLocaleTimeString(lang, nycOptions);
const timeUTC = d.toLocaleTimeString(lang, utcOptions);
const timeTYO = d.toLocaleTimeString(lang, tyoOptions);
const timeSTO = d.toLocaleTimeString(lang, stoOptions);
return { timeNY, timeUTC, timeTYO, timeSTO };
}
const HomePageTimer: FC<{}> = () => {
const [{ timeNY, timeUTC, timeTYO, timeSTO }, setTimes] = React.useState(
defaultTimes,
);
React.useEffect(() => {
setTimes(getTimes());
const intervalId = setInterval(() => {
setTimes(getTimes());
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<>
<HeaderLabel label="NYC" value={timeNY} />
<HeaderLabel label="UTC" value={timeUTC} />
<HeaderLabel label="STO" value={timeSTO} />
<HeaderLabel label="TYO" value={timeTYO} />
</>
);
};
export default HomePageTimer;
@@ -1 +0,0 @@
export { default } from './Timer';

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