Merge branch 'master' of https://github.com/spotify/backstage
This commit is contained in:
@@ -9,6 +9,7 @@ on:
|
||||
- 'packages/storybook/**'
|
||||
- 'packages/core/src/**'
|
||||
- 'microsite/**'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
deploy-microsite-and-storybook:
|
||||
|
||||
@@ -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
@@ -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
@@ -3,7 +3,7 @@ id: design
|
||||
title: Design
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||
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 you’d like to help build up our design system, you can also add
|
||||
components we’ve 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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||

|
||||
|
||||
@@ -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
@@ -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 ✅
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
## What’s 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.
|
||||
|
||||
## What’s 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 organization’s 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 organization’s 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 you’ve 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 you’ve already been running Backstage locally, run the command `yarn lerna run mock-data` to load the new sample templates into the Service Catalog first.
|
||||
|
||||

|
||||
|
||||
@@ -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 Backstage’s Software Templates feature, it’s easy to help your engineers get started building software with your organization’s 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 Backstage’s Software Templates feature, it’s easy to help your engineers get started building software with your organization’s 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.
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.1-alpha.19",
|
||||
"name": "backstage-microsite",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
+2
-2
@@ -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?: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
-58
@@ -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
@@ -1 +0,0 @@
|
||||
export { default } from './Timer';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user