diff --git a/.changeset/1724.md b/.changeset/1724.md
new file mode 100644
index 0000000000..2ac03f94e9
--- /dev/null
+++ b/.changeset/1724.md
@@ -0,0 +1,27 @@
+---
+'@backstage/cli': minor
+'@backstage/plugin-api-docs': minor
+'@backstage/plugin-app-backend': minor
+'@backstage/plugin-auth-backend': minor
+'@backstage/plugin-catalog-graphql': minor
+'@backstage/plugin-catalog': minor
+'@backstage/plugin-circleci': minor
+'@backstage/plugin-explore': minor
+'@backstage/plugin-gcp-projects': minor
+'@backstage/plugin-github-actions': minor
+'@backstage/plugin-gitops-profiles': minor
+'@backstage/plugin-graphiql': minor
+'@backstage/plugin-jenkins': minor
+'@backstage/plugin-kubernetes': minor
+'@backstage/plugin-lighthouse': minor
+'@backstage/plugin-newrelic': minor
+'@backstage/plugin-register-component': minor
+'@backstage/plugin-rollbar': minor
+'@backstage/plugin-scaffolder': minor
+'@backstage/plugin-sentry': minor
+'@backstage/plugin-tech-radar': minor
+'@backstage/plugin-techdocs': minor
+'@backstage/plugin-welcome': minor
+---
+
+Create backend plugin through CLI
diff --git a/.changeset/2284.md b/.changeset/2284.md
new file mode 100644
index 0000000000..655cc5a3e8
--- /dev/null
+++ b/.changeset/2284.md
@@ -0,0 +1,11 @@
+---
+'@backstage/core-api': minor
+'@backstage/core': minor
+'@backstage/plugin-auth-backend': minor
+---
+
+Add SAML login to backstage
+
+
+
+
diff --git a/.changeset/2515.md b/.changeset/2515.md
new file mode 100644
index 0000000000..32f5edd6b7
--- /dev/null
+++ b/.changeset/2515.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-cloudbuild': minor
+---
+
+Releasing Google Cloud Build Plugin
diff --git a/.changeset/2532.md b/.changeset/2532.md
new file mode 100644
index 0000000000..14307f6c08
--- /dev/null
+++ b/.changeset/2532.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-model': minor
+---
+
+Add handling and docs for entity references
diff --git a/.changeset/2535.md b/.changeset/2535.md
new file mode 100644
index 0000000000..bd28f3ce03
--- /dev/null
+++ b/.changeset/2535.md
@@ -0,0 +1,14 @@
+---
+'@backstage/core': patch
+'@backstage/theme': patch
+---
+
+Fix banner position and color
+
+This PR closes: #2245
+
+The "fixed" props added to control the position of the banner. When it is set to true the banner will be shown in bottom of that page and the width will be based on the content of the message.
+
+
+
+
diff --git a/.changeset/2541.md b/.changeset/2541.md
new file mode 100644
index 0000000000..1ba29da040
--- /dev/null
+++ b/.changeset/2541.md
@@ -0,0 +1,5 @@
+---
+'@backstage/theme': minor
+---
+
+Tweak dark mode colors
diff --git a/.changeset/2543.md b/.changeset/2543.md
new file mode 100644
index 0000000000..00311ade45
--- /dev/null
+++ b/.changeset/2543.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-scaffolder-backend': minor
+---
+
+Add Azure DevOps support to the scaffolder backend
+
+This adds support for Azure DevOps to the scaffolder (preparer & publisher). I thought I should get this in there now since #2426 has been merged. I had a previous PR with only the preparer but I closed that in favor of this one.
+
+I stayed with the 'azure/api' structure but I guess we should try and go the same way as with GitHub here #2501
diff --git a/.changeset/2562.md b/.changeset/2562.md
new file mode 100644
index 0000000000..716f370ad9
--- /dev/null
+++ b/.changeset/2562.md
@@ -0,0 +1,7 @@
+---
+'@backstage/create-app': minor
+'@backstage/plugin-auth-backend': minor
+'@backstage/plugin-techdocs-backend': minor
+---
+
+Change the default backend plugin mount point to /api
diff --git a/.changeset/2563.md b/.changeset/2563.md
new file mode 100644
index 0000000000..318d5cf5ef
--- /dev/null
+++ b/.changeset/2563.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-github-actions': minor
+---
+
+Adds a widget to show recent git workflow runs to the github actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable.
diff --git a/.changeset/2565.md b/.changeset/2565.md
new file mode 100644
index 0000000000..92253101ee
--- /dev/null
+++ b/.changeset/2565.md
@@ -0,0 +1,59 @@
+---
+'@backstage/core-api': patch
+---
+
+Add initial RouteRefRegistry
+
+Starting out some work to bring routing back and working as part of the work towards finalizing #1536
+
+This is some of the groundwork of an experiment we're working on to enable routing via RouteRefs, while letting the app itself look something like this:
+
+```jsx
+const App = () => (
+
+
+
+ {' '}
+ // catalogRouteRef
+
+
+
+
+
+
+ // statusRouteRef
+
+
+
+
+
+
+
+
+
+ // sentryRouteRef
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+As part of inverting the composition of the app, route refs and routing in general was somewhat broken, intentionally. Right now it's not really possible to easily route to different parts of the app from a plugin, or even different parts of the plugin that are not within the same router.
+
+The core part of the experiment is to construct a map of ApiRef[] -> path overrides. Each key in the map is the list of route refs to traversed to reach a leaf in the routing tree, and the value is the path override at that point. For example, the above tree would add entries like [techDocsRouteRef] -> '/docs', and [entityRouteRef, apiDocsRouteRef] -> '/api'. By mapping out the entire app in this structure, the idea is that we can navigate to any point in the app using RouteRefs.
+
+The RouteRefRegistry is an implementation of such a map, and the idea is to add it in master to make it a bit easier to experiment and iterate. This is not an exposed API at this point.
+
+We've explored a couple of alternatives for how to enable routing, but it's boiled down to either a solution centred around the route map mentioned above, or treating all routes as static and globally unique, with no room for flexibility, customization or conflicts between different plugins. We're starting out pursuing this options 😁. We also expect that a the app-wide routing table will make things like dynamic loading a lot cleaner, as there would be a much more clear handoff between the main chunk and dynamic chunks.
diff --git a/.changeset/2575.md b/.changeset/2575.md
new file mode 100644
index 0000000000..b08f50ed13
--- /dev/null
+++ b/.changeset/2575.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': minor
+---
+
+Fix dense in Structured Metadata Table
diff --git a/.changeset/2586.md b/.changeset/2586.md
new file mode 100644
index 0000000000..568adb8a3e
--- /dev/null
+++ b/.changeset/2586.md
@@ -0,0 +1,6 @@
+---
+'@backstage/catalog-model': minor
+'@backstage/plugin-catalog-backend': minor
+---
+
+Entirely case insensitive read path of entities
diff --git a/.changeset/2587.md b/.changeset/2587.md
new file mode 100644
index 0000000000..910de1ca04
--- /dev/null
+++ b/.changeset/2587.md
@@ -0,0 +1,9 @@
+---
+'@backstage/catalog-model': minor
+---
+
+Add the User & Group entities
+
+A user describes a person, such as an employee, a contractor, or similar. Users belong to Group entities in the catalog.
+
+A group describes an organizational entity, such as for example a team, a business unit, or a loose collection of people in an interest group. Members of these groups are modeled in the catalog as kind User.
diff --git a/.changeset/2597.md b/.changeset/2597.md
new file mode 100644
index 0000000000..9db25e8d0e
--- /dev/null
+++ b/.changeset/2597.md
@@ -0,0 +1,6 @@
+---
+'@backstage/catalog-model': minor
+'@backstage/plugin-catalog-backend': minor
+---
+
+Add ApiDefinitionAtLocationProcessor that allows to load a API definition from another location
diff --git a/.changeset/2598.md b/.changeset/2598.md
new file mode 100644
index 0000000000..03e10e7db8
--- /dev/null
+++ b/.changeset/2598.md
@@ -0,0 +1,11 @@
+---
+'@backstage/plugin-techdocs': minor
+---
+
+Add a message if techdocs takes long time to load
+
+Fixes #2416.
+
+The UI after the change should look like this:
+
+
diff --git a/.changeset/2600.md b/.changeset/2600.md
new file mode 100644
index 0000000000..6fc9775fa1
--- /dev/null
+++ b/.changeset/2600.md
@@ -0,0 +1,19 @@
+---
+'@backstage/plugin-techdocs-backend': minor
+'@backstage/plugin-proxy-backend': minor
+'@backstage/plugin-auth-backend': minor
+'@backstage/create-app': minor
+'@backstage/backend-common': minor
+---
+
+Add service discovery interface and implement for single host deployments
+
+Fixes #1847, #2596
+
+Went with an interface similar to the frontend DiscoveryApi, since it's dead simple but still provides a lot of flexibility in the implementation.
+
+Also ended up with two different methods, one for internal endpoint discovery and one for external. The two use-cases are explained a bit more in the docs, but basically it's service-to-service vs callback URLs.
+
+This did get me thinking about uniqueness and that we're heading towards a global namespace for backend plugin IDs. That's probably fine, but if we're happy with that we should leverage it a bit more to simplify the backend setup. For example we'd have each plugin provide its own ID and not manually mount on paths in the backend.
+
+Draft until we're happy with the implementation, then I can add more docs and changelog entry. Also didn't go on a thorough hunt for places where discovery can be used, but I don't think there are many since it's been pretty awkward to do service-to-service communication.
diff --git a/.changeset/2603.md b/.changeset/2603.md
new file mode 100644
index 0000000000..c24d2ea033
--- /dev/null
+++ b/.changeset/2603.md
@@ -0,0 +1,8 @@
+---
+'@backstage/backend-common': minor
+'@backstage/create-app': minor
+---
+
+Make CSP configurable to fix app-backend served app not being able to fetch
+
+See discussion [here on discord](https://discordapp.com/channels/687207715902193673/687235481154617364/758721460163575850)
diff --git a/.changeset/2606.md b/.changeset/2606.md
new file mode 100644
index 0000000000..f6059e7705
--- /dev/null
+++ b/.changeset/2606.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Move auth provider router creation to router
diff --git a/.changeset/2609.md b/.changeset/2609.md
new file mode 100644
index 0000000000..6bb8ab7a40
--- /dev/null
+++ b/.changeset/2609.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Sync scaffolded backend with example
diff --git a/.changeset/2610.md b/.changeset/2610.md
new file mode 100644
index 0000000000..92ec8dcc0c
--- /dev/null
+++ b/.changeset/2610.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-circleci': minor
+---
+
+Refactor to use DiscoveryApi
diff --git a/.changeset/2611.md b/.changeset/2611.md
new file mode 100644
index 0000000000..cbf3129038
--- /dev/null
+++ b/.changeset/2611.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Remove discovery api override
diff --git a/.changeset/2612.md b/.changeset/2612.md
new file mode 100644
index 0000000000..bf093d8f77
--- /dev/null
+++ b/.changeset/2612.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-jenkins': patch
+---
+
+Refactor to use DiscoveryApi
diff --git a/.changeset/2613.md b/.changeset/2613.md
new file mode 100644
index 0000000000..cf8c1a155a
--- /dev/null
+++ b/.changeset/2613.md
@@ -0,0 +1,12 @@
+---
+'@backstage/plugin-auth-backend': minor
+---
+
+Initial implementation of catalog user lookup
+
+This adds a basic catalog client + method for the Google provider to look up users in the catalog. It expects to find a single user entity in the catalog with a google.com/email annotation that matches the email of the Google profile.
+
+Right now it falls back to the old behavior of splitting the email, since I don't wanna break the sign-in flow for existing apps, not yet anyway x).
+
+- Added "@backstage/catalog-model@^0.1.1-alpha.23" as a dependency
+- Added "node-fetch@^2.6.1" as a dependency
diff --git a/.changeset/2614.md b/.changeset/2614.md
new file mode 100644
index 0000000000..7e2660bc80
--- /dev/null
+++ b/.changeset/2614.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': minor
+---
+
+Default to using internal scope for new plugins
diff --git a/.changeset/2615.md b/.changeset/2615.md
new file mode 100644
index 0000000000..412b8127c4
--- /dev/null
+++ b/.changeset/2615.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-common': minor
+---
+
+Use localhost to fall back to IPv4 if IPv6 isn't available
diff --git a/.changeset/2616.md b/.changeset/2616.md
new file mode 100644
index 0000000000..9a70c0e79f
--- /dev/null
+++ b/.changeset/2616.md
@@ -0,0 +1,7 @@
+---
+'@backstage/create-app': minor
+---
+
+Remove identity-backend
+
+Not used, and we're heading down the route of identities in the catalog
diff --git a/.changeset/2623.md b/.changeset/2623.md
new file mode 100644
index 0000000000..abd5ce7a4c
--- /dev/null
+++ b/.changeset/2623.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog': minor
+---
+
+Created EntityNotFound component for catalog which displays the 404 page when entity is not found.
+
+Fixes #2266
diff --git a/.changeset/2624.md b/.changeset/2624.md
new file mode 100644
index 0000000000..dcfc800713
--- /dev/null
+++ b/.changeset/2624.md
@@ -0,0 +1,17 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Make title meaningful after component creation
+
+Fixes #2458.
+
+After the change, the UX should look like this:
+
+### If the component creation was successful:
+
+
+
+### If the component creation failed:
+
+
diff --git a/.changeset/2625-catalog-backend.md b/.changeset/2625-catalog-backend.md
new file mode 100644
index 0000000000..bcd49dddb2
--- /dev/null
+++ b/.changeset/2625-catalog-backend.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Add codeowners processor
+
+- Add `codeowners-utils@^1.0.2` as a dependency
+- Add `core-js@^3.6.5` as a dependency
+- Added new CodeOwnersProcessor
diff --git a/.changeset/2625-cli.md b/.changeset/2625-cli.md
new file mode 100644
index 0000000000..c5a5437b69
--- /dev/null
+++ b/.changeset/2625-cli.md
@@ -0,0 +1,7 @@
+---
+'@backstage/cli': patch
+---
+
+Add codeowners processor
+
+- Include ESNext.Promise in TypeScript compilation
diff --git a/.changeset/2628.md b/.changeset/2628.md
new file mode 100644
index 0000000000..0ac36d456a
--- /dev/null
+++ b/.changeset/2628.md
@@ -0,0 +1,22 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+This feature works the same as \$secret does in config - it allows programmatic substitution of values into a document.
+
+This is particularly useful e.g. for API type entities where you do not want to repeat your entire API spec document inside the catalog-info.yaml file. For those cases, you can instead do something like
+
+```
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: my-federated-service
+spec:
+ type: graphql
+ definition:
+ $text: ./schema.graphql
+```
+
+The textual content of that file will be injected as the value of definition, during each refresh loop. Both relative and absolute paths are supported, as well as any HTTP/HTTPS URL pointing to a service that returns the relevant data.
+
+The initial version supports injection of text file data, and structured data from JSON and YAML files. You can add any handler of your own in addition to these.
diff --git a/.changeset/2630.md b/.changeset/2630.md
new file mode 100644
index 0000000000..933204e292
--- /dev/null
+++ b/.changeset/2630.md
@@ -0,0 +1,9 @@
+---
+'@backstage/create-app': minor
+'@backstage/plugin-catalog-backend': minor
+---
+
+Allow node v14 and add to master build matrix
+
+- Upgrade sqlite3@^5.0.0 in @backstage/plugin-catalog-backend
+- Add Node 14 to engines in @backstage/create-app
diff --git a/.changeset/2637.md b/.changeset/2637.md
new file mode 100644
index 0000000000..9d368634b6
--- /dev/null
+++ b/.changeset/2637.md
@@ -0,0 +1,21 @@
+---
+'@backstage/plugin-cost-insights': minor
+'@backstage/plugin-explore': minor
+---
+
+This PR adds Spotify's Cost Insights Tool. Cost Insights explains costs from cloud services in an understandable way, using software terms familiar to your engineers. This tool helps you and your team make trade-offs between cost optimization efforts and your other priorities.
+
+Cost Insights features:
+
+Daily cost graph by team or billing account
+Cost comparison against configurable business metrics
+Insights panels for configurable cloud products your company uses
+Cost alerts and recommendations
+Selectable time periods for month over month, or quarter over quarter cost comparison
+Conversion of cost growth into average engineer cost (configurable) to help optimization trade-off decisions
+
+
+
+This PR adds the Cost Insights frontend React plugin with a defined CostInsightsApi. We include an example client with static data in the expected format. This API should talk with a cloud billing backend that aggregates billing data from your cloud provider.
+
+Fixes #688 💵
diff --git a/.changeset/2639.md b/.changeset/2639.md
new file mode 100644
index 0000000000..e6a623088e
--- /dev/null
+++ b/.changeset/2639.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': minor
+---
+
+Upgrade dependency `esbuild@0.7.7`
diff --git a/.changeset/2641.md b/.changeset/2641.md
new file mode 100644
index 0000000000..3b06358e81
--- /dev/null
+++ b/.changeset/2641.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Simplify the read function in processors
diff --git a/.changeset/2656.md b/.changeset/2656.md
new file mode 100644
index 0000000000..e253b6b1f6
--- /dev/null
+++ b/.changeset/2656.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': minor
+---
+
+Lookup user in Google Auth Provider
diff --git a/.changeset/2657.md b/.changeset/2657.md
new file mode 100644
index 0000000000..9aa645d65f
--- /dev/null
+++ b/.changeset/2657.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': minor
+---
+
+Added EmptyState component
diff --git a/.changeset/2660.md b/.changeset/2660.md
new file mode 100644
index 0000000000..758d523f07
--- /dev/null
+++ b/.changeset/2660.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-api-docs': patch
+---
+
+Resolve some dark mode styling issues in asyncAPI specs
diff --git a/.changeset/2661.md b/.changeset/2661.md
new file mode 100644
index 0000000000..e73b0abf26
--- /dev/null
+++ b/.changeset/2661.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': patch
+---
+
+Fixed banner component position in DismissableBanner component
diff --git a/.changeset/2669-catalog-backend.md b/.changeset/2669-catalog-backend.md
new file mode 100644
index 0000000000..32ee5c0f4d
--- /dev/null
+++ b/.changeset/2669-catalog-backend.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Add the ability to import users from GitHub Organization into the catalog.
+
+The token needs to have the scopes `user:email`, `read:user`, and `read:org`.
diff --git a/.changeset/2669-catalog-model.md b/.changeset/2669-catalog-model.md
new file mode 100644
index 0000000000..b9a2841a1b
--- /dev/null
+++ b/.changeset/2669-catalog-model.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-model': minor
+---
+
+Add the ability to import users from GitHub Organization into the catalog.
diff --git a/.changeset/2669-create-app.md b/.changeset/2669-create-app.md
new file mode 100644
index 0000000000..e7075ffeb8
--- /dev/null
+++ b/.changeset/2669-create-app.md
@@ -0,0 +1,7 @@
+---
+'@backstage/create-app': minor
+---
+
+Add the ability to import users from GitHub Organization into the catalog.
+
+The token needs to have the scopes `user:email`, `read:user`, and `read:org`.
diff --git a/.changeset/2674.md b/.changeset/2674.md
new file mode 100644
index 0000000000..15a7a0e849
--- /dev/null
+++ b/.changeset/2674.md
@@ -0,0 +1,12 @@
+---
+'@backstage/backend-common': minor
+'@backstage/create-app': minor
+---
+
+Auto-create plugin databases
+
+Relates to #1598.
+
+This creates databases for plugins before handing off control to plugins.
+
+The list of plugins currently need to be hard-coded depending on the installed plugins. A later PR will properly refactor the code to provide a factory pattern where plugins specify what they need, and Knex instances will be provided based on the input.
diff --git a/.changeset/2686.md b/.changeset/2686.md
new file mode 100644
index 0000000000..8831c1309b
--- /dev/null
+++ b/.changeset/2686.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Update SSR template to pass CI
diff --git a/.changeset/2689.md b/.changeset/2689.md
new file mode 100644
index 0000000000..be75dd261b
--- /dev/null
+++ b/.changeset/2689.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Upgrade dependency rollup-plugin-typescript2 to ^0.27.3
diff --git a/.changeset/2722.md b/.changeset/2722.md
new file mode 100644
index 0000000000..69642b5797
--- /dev/null
+++ b/.changeset/2722.md
@@ -0,0 +1,11 @@
+---
+'@backstage/plugin-api-docs': minor
+---
+
+There were some missing features and markdown was not rendered properly, but this is fixed now.
+
+Details:
+
+- [`asyncapi/asyncapi-react#149`](https://github.com/asyncapi/asyncapi-react/pull/149) - fix: improve markdown rendering of nested fields
+- [`asyncapi/asyncapi-react#150`](https://github.com/asyncapi/asyncapi-react/pull/150) - feat: display the description of channels and operations
+- [`asyncapi/asyncapi-react#153`](https://github.com/asyncapi/asyncapi-react/pull/153) - fix: let the list of `enums` break into multiple lines
diff --git a/.changeset/2798.md b/.changeset/2798.md
new file mode 100644
index 0000000000..2b202142ef
--- /dev/null
+++ b/.changeset/2798.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Use the new `UrlReader` in `PlaceholderProcessor`.
+This allows to use the placeholder processor to include API definitions in API entities.
+Previously it was only possible to do this if the definition comes from the same location type as the entity itself.
diff --git a/.changeset/2803.md b/.changeset/2803.md
new file mode 100644
index 0000000000..3a4773757f
--- /dev/null
+++ b/.changeset/2803.md
@@ -0,0 +1,26 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Remove the backstage.io/definition-at-location annotation.
+The annotation was superseded by the placeholder processor.
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: spotify
+ description: The Spotify web API
+ tags:
+ - spotify
+ - rest
+ annotations:
+ # Don't use this annotation, but the placeholder $text instead (see below).
+ backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml'
+spec:
+ type: openapi
+ lifecycle: production
+ owner: spotify@example.com
+ definition:
+ $text: https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml
+```
diff --git a/.changeset/2804.md b/.changeset/2804.md
new file mode 100644
index 0000000000..5223a24791
--- /dev/null
+++ b/.changeset/2804.md
@@ -0,0 +1,15 @@
+---
+'@backstage/plugin-proxy-backend': minor
+---
+
+Limit the http headers that are forwarded from the request to a safe set of defaults.
+A user can configure additional headers that should be forwarded if the specific applications needs that.
+
+```yaml
+proxy:
+ '/my-api':
+ target: 'https://my-api.com/get'
+ allowedHeaders:
+ # We need to forward the Authorization header that was provided by the caller
+ - Authorization
+```
diff --git a/.changeset/codeowner-processor-url-reader.md b/.changeset/codeowner-processor-url-reader.md
new file mode 100644
index 0000000000..af4eb0997f
--- /dev/null
+++ b/.changeset/codeowner-processor-url-reader.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Use the new `UrlReader` in the `CodeOwnersProcessor`.
diff --git a/.changeset/create-app-url-reader-update.md b/.changeset/create-app-url-reader-update.md
new file mode 100644
index 0000000000..eb2e538b77
--- /dev/null
+++ b/.changeset/create-app-url-reader-update.md
@@ -0,0 +1,6 @@
+---
+'example-backend': patch
+'@backstage/create-app': patch
+---
+
+Bump @backstage/catalog-backend and pass the now required UrlReader interface to the plugin
diff --git a/.changeset/cyan-plants-dress.md b/.changeset/cyan-plants-dress.md
new file mode 100644
index 0000000000..19ebc63e53
--- /dev/null
+++ b/.changeset/cyan-plants-dress.md
@@ -0,0 +1,5 @@
+---
+'example-backend': patch
+---
+
+Pass GitHub token into Scaffolder GitHub Preparer
diff --git a/.changeset/long-ways-count.md b/.changeset/long-ways-count.md
new file mode 100644
index 0000000000..e04c1225cc
--- /dev/null
+++ b/.changeset/long-ways-count.md
@@ -0,0 +1,57 @@
+---
+'@backstage/plugin-techdocs-backend': minor
+'@backstage/create-app': patch
+---
+
+Updated naming of environment variables. New pattern [NAME]\_TOKEN for Github, Gitlab, Azure & Github enterprise access tokens.
+
+### Detail:
+
+- Previously we have to export same token for both, catalog & scaffolder
+
+```bash
+export GITHUB_ACCESS_TOKEN=foo
+export GITHUB_PRIVATE_TOKEN=foo
+```
+
+with latest changes, only single export is sufficient.
+
+```bash
+export GITHUB_TOKEN=foo
+export GITLAB_TOKEN=foo
+export GHE_TOKEN=foo
+export AZURE_TOKEN=foo
+```
+
+### list:
+
+
+
+ Old name
+ New name
+
+
+ GITHUB_ACCESS_TOKEN
+ GITHUB_TOKEN
+
+
+ GITHUB_PRIVATE_TOKEN
+ GITHUB_TOKEN
+
+
+ GITLAB_ACCESS_TOKEN
+ GITLAB_TOKEN
+
+
+ GITLAB_PRIVATE_TOKEN
+ GITLAB_TOKEN
+
+
+ AZURE_PRIVATE_TOKEN
+ AZURE_TOKEN
+
+
+ GHE_PRIVATE_TOKEN
+ GHE_TOKEN
+
+
diff --git a/.changeset/loud-lamps-visit.md b/.changeset/loud-lamps-visit.md
new file mode 100644
index 0000000000..9023061643
--- /dev/null
+++ b/.changeset/loud-lamps-visit.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Including source maps with all packages
diff --git a/.changeset/new-url-reader.md b/.changeset/new-url-reader.md
new file mode 100644
index 0000000000..94758c5bc1
--- /dev/null
+++ b/.changeset/new-url-reader.md
@@ -0,0 +1,12 @@
+---
+'@backstage/backend-common': patch
+---
+
+Added new UrlReader interface for reading opaque data from URLs with different providers.
+
+This new URL reading system is intended as a replacement for the various integrations towards
+external systems in the catalog, scaffolder, and techdocs. It is configured via a new top-level
+config section called 'integrations'.
+
+Along with the UrlReader interface is a new UrlReaders class, which exposes static factory
+methods for instantiating readers that can read from many different integrations simultaneously.
diff --git a/.changeset/ninety-pens-poke.md b/.changeset/ninety-pens-poke.md
new file mode 100644
index 0000000000..94bcb06749
--- /dev/null
+++ b/.changeset/ninety-pens-poke.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Fix for configured templates using 'url' locations even though it's not supported yet
diff --git a/.changeset/pretty-cups-joke.md b/.changeset/pretty-cups-joke.md
new file mode 100644
index 0000000000..49a1db2a75
--- /dev/null
+++ b/.changeset/pretty-cups-joke.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Add authentication token to Scaffolder GitHub Preparer
diff --git a/.changeset/remove-location-processor-read.md b/.changeset/remove-location-processor-read.md
new file mode 100644
index 0000000000..78aea6d820
--- /dev/null
+++ b/.changeset/remove-location-processor-read.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Remove the `read` argument of `LocationProcessor.processEntity`.
+Instead, pass the `UrlReader` into the constructor of your `LocationProcessor`.
diff --git a/.changeset/short-secrets.md b/.changeset/short-secrets.md
new file mode 100644
index 0000000000..72cb059979
--- /dev/null
+++ b/.changeset/short-secrets.md
@@ -0,0 +1,5 @@
+---
+'@backstage/config-loader': minor
+---
+
+Added support for new shorthand when defining secrets, where `$env: ENV` can be used instead of `$secret: { env: ENV }` etc.
diff --git a/.changeset/url-reader-processor.md b/.changeset/url-reader-processor.md
new file mode 100644
index 0000000000..40fef3c106
--- /dev/null
+++ b/.changeset/url-reader-processor.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+The catalog backend UrlReaderProcessor now uses a UrlReader from @backstage/backend-common, which must now be supplied to the constructor.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 0437698301..01fc8e70c3 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -13,3 +13,4 @@
- [ ] Prettier run on changed files
- [ ] Tests added for new functionality
- [ ] Regression tests added for bug fixes
+- [ ] Added a changeset ([more info](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md#creating-changesets))
diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 21045f04dd..c9774ae237 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -32,6 +32,7 @@ Codehilite
codeowners
config
Config
+configmaps
configs
const
cookiecutter
@@ -67,6 +68,7 @@ graphviz
Hackathons
haproxy
heroku
+horizontalpodautoscalers
Hostname
http
https
@@ -140,6 +142,7 @@ rankdir
readme
Readme
Redash
+replicasets
repo
Repo
repos
diff --git a/.github/workflows/microsite-build-check.yml b/.github/workflows/microsite-build-check.yml
index 0b614509f2..8a2fa99ed2 100644
--- a/.github/workflows/microsite-build-check.yml
+++ b/.github/workflows/microsite-build-check.yml
@@ -33,6 +33,10 @@ jobs:
run: yarn install --frozen-lockfile
working-directory: microsite
+ - name: prettier
+ run: yarn prettier:check
+ working-directory: microsite
+
- name: build microsite
run: yarn build
working-directory: microsite
diff --git a/.imgbotconfig b/.imgbotconfig
new file mode 100644
index 0000000000..f337e087dc
--- /dev/null
+++ b/.imgbotconfig
@@ -0,0 +1,5 @@
+{
+ "ignoredFiles": [
+ "docs/assets/**/*.svg"
+ ]
+}
diff --git a/.prettierignore b/.prettierignore
index 9e75b74eee..4b1acbb594 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,6 @@
.yarn
dist
-microsite/build
+microsite
coverage
*.hbs
templates
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf2638cfe9..1860a4de6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.24
+
### Backend (example-backend, or backends created with @backstage/create-app)
- The default mount point for backend plugins have been changed to `/api`. These changes are done in the backend package itself, so it is recommended that you sync up existing backend packages with this new pattern. [#2562](https://github.com/spotify/backstage/pull/2562)
diff --git a/app-config.yaml b/app-config.yaml
index 88df40813e..228eafc588 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -18,23 +18,20 @@ proxy:
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
- $secret:
- env: CIRCLECI_AUTH_TOKEN
+ $env: CIRCLECI_AUTH_TOKEN
'/jenkins/api':
target: http://localhost:8080
headers:
Authorization:
- $secret:
- env: JENKINS_BASIC_AUTH_HEADER
+ $env: JENKINS_BASIC_AUTH_HEADER
'/travisci/api':
target: https://api.travis-ci.com
changeOrigin: true
headers:
Authorization:
- $secret:
- env: TRAVISCI_AUTH_TOKEN
+ $env: TRAVISCI_AUTH_TOKEN
travis-api-version: 3
organization:
@@ -52,8 +49,7 @@ sentry:
rollbar:
organization: spotify
accountToken:
- $secret:
- env: ROLLBAR_ACCOUNT_TOKEN
+ $env: ROLLBAR_ACCOUNT_TOKEN
newrelic:
api:
@@ -67,174 +63,163 @@ kubernetes:
clusterLocatorMethod: 'configMultiTenant'
clusters: []
+integrations:
+ github:
+ - host: github.com
+ token:
+ $env: GITHUB_TOKEN
+ ### Example for how to add your GitHub Enterprise instance using the API:
+ # - host: ghe.example.net
+ # apiBaseUrl: https://ghe.example.net/api/v3
+ # token:
+ # $env: GHE_TOKEN
+ ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
+ # - host: ghe.example.net
+ # rawBaseUrl: https://ghe.example.net/raw
+ # token:
+ # $env: GHE_TOKEN
+ gitlab:
+ - host: gitlab.com
+ token:
+ $env: GITLAB_TOKEN
+ bitbucket:
+ - host: bitbucket.org
+ username:
+ $env: BITBUCKET_USERNAME
+ appPassword:
+ $env: BITBUCKET_APP_PASSWORD
+ azure:
+ - host: dev.azure.com
+ token:
+ $env: AZURE_TOKEN
+
catalog:
rules:
- allow: [Component, API, Group, User, Template, Location]
+
processors:
- github:
- providers:
- - target: https://github.com
- token:
- $secret:
- env: GITHUB_PRIVATE_TOKEN
- #### Example for how to add your GitHub Enterprise instance using the API:
- # - target: https://ghe.example.net
- # apiBaseUrl: https://ghe.example.net/api/v3
- # token:
- # $secret:
- # env: GHE_PRIVATE_TOKEN
- #### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
- # - target: https://ghe.example.net
- # rawBaseUrl: https://ghe.example.net/raw
- # token:
- # $secret:
- # env: GHE_PRIVATE_TOKEN
githubOrg:
providers:
- target: https://github.com
token:
- $secret:
- env: GITHUB_PRIVATE_TOKEN
+ $env: GITHUB_TOKEN
#### Example for how to add your GitHub Enterprise instance using the API:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
- # $secret:
- # env: GHE_PRIVATE_TOKEN
- bitbucketApi:
- username:
- $secret:
- env: BITBUCKET_USERNAME
- appPassword:
- $secret:
- env: BITBUCKET_APP_PASSWORD
- gitlabApi:
- privateToken:
- $secret:
- env: GITLAB_PRIVATE_TOKEN
- azureApi:
- privateToken:
- $secret:
- env: AZURE_PRIVATE_TOKEN
+ # $env: GHE_TOKEN
+ ldapOrg:
+ ### Example for how to add your enterprise LDAP server
+ # providers:
+ # - target: ldaps://ds.example.net
+ # bind:
+ # dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
+ # secret: { $secret: { env: LDAP_SECRET } }
+ # users:
+ # dn: ou=people,ou=example,dc=example,dc=net
+ # options:
+ # filter: (uid=*)
+ # map:
+ # description: l
+ # groups:
+ # dn: ou=access,ou=groups,ou=example,dc=example,dc=net
+ # options:
+ # filter: (&(objectClass=some-group-class)(!(groupType=email)))
locations:
# Backstage example components
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
# Example component for github-actions
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/github-actions/examples/sample.yaml
# Example component for techdocs
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/documented-component.yaml
# Backstage example APIs
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml
# Backstage example templates
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml
scaffolder:
github:
token:
- $secret:
- env: GITHUB_ACCESS_TOKEN
+ $env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
gitlab:
api:
baseUrl: https://gitlab.com
token:
- $secret:
- env: GITLAB_ACCESS_TOKEN
+ $env: GITLAB_TOKEN
azure:
baseUrl: https://dev.azure.com/{your-organization}
api:
token:
- $secret:
- env: AZURE_PRIVATE_TOKEN
+ $env: AZURE_TOKEN
auth:
providers:
google:
development:
clientId:
- $secret:
- env: AUTH_GOOGLE_CLIENT_ID
+ $env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GOOGLE_CLIENT_SECRET
+ $env: AUTH_GOOGLE_CLIENT_SECRET
github:
development:
clientId:
- $secret:
- env: AUTH_GITHUB_CLIENT_ID
+ $env: AUTH_GITHUB_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GITHUB_CLIENT_SECRET
+ $env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
- $secret:
- env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
+ $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
development:
clientId:
- $secret:
- env: AUTH_GITLAB_CLIENT_ID
+ $env: AUTH_GITLAB_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GITLAB_CLIENT_SECRET
+ $env: AUTH_GITLAB_CLIENT_SECRET
audience:
- $secret:
- env: GITLAB_BASE_URL
+ $env: GITLAB_BASE_URL
saml:
entryPoint: 'http://localhost:7001/'
issuer: 'passport-saml'
okta:
development:
clientId:
- $secret:
- env: AUTH_OKTA_CLIENT_ID
+ $env: AUTH_OKTA_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_OKTA_CLIENT_SECRET
+ $env: AUTH_OKTA_CLIENT_SECRET
audience:
- $secret:
- env: AUTH_OKTA_AUDIENCE
+ $env: AUTH_OKTA_AUDIENCE
oauth2:
development:
clientId:
- $secret:
- env: AUTH_OAUTH2_CLIENT_ID
+ $env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_OAUTH2_CLIENT_SECRET
+ $env: AUTH_OAUTH2_CLIENT_SECRET
authorizationUrl:
- $secret:
- env: AUTH_OAUTH2_AUTH_URL
+ $env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
- $secret:
- env: AUTH_OAUTH2_TOKEN_URL
+ $env: AUTH_OAUTH2_TOKEN_URL
auth0:
development:
clientId:
- $secret:
- env: AUTH_AUTH0_CLIENT_ID
+ $env: AUTH_AUTH0_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_AUTH0_CLIENT_SECRET
+ $env: AUTH_AUTH0_CLIENT_SECRET
domain:
- $secret:
- env: AUTH_AUTH0_DOMAIN
+ $env: AUTH_AUTH0_DOMAIN
microsoft:
development:
clientId:
- $secret:
- env: AUTH_MICROSOFT_CLIENT_ID
+ $env: AUTH_MICROSOFT_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_MICROSOFT_CLIENT_SECRET
+ $env: AUTH_MICROSOFT_CLIENT_SECRET
tenantId:
- $secret:
- env: AUTH_MICROSOFT_TENANT_ID
+ $env: AUTH_MICROSOFT_TENANT_ID
costInsights:
engineerCost: 200000
products:
diff --git a/backstage_overview.png b/backstage_overview.png
deleted file mode 100644
index 6c939c127c..0000000000
Binary files a/backstage_overview.png and /dev/null differ
diff --git a/contrib/docker/kubernetes-example-backend/Dockerfile b/contrib/docker/kubernetes-example-backend/Dockerfile
new file mode 100644
index 0000000000..df617decf5
--- /dev/null
+++ b/contrib/docker/kubernetes-example-backend/Dockerfile
@@ -0,0 +1,35 @@
+FROM node:12-buster
+
+WORKDIR /usr/src/app
+
+# (workaround) Install cookiecutter and mkdocs to avoid the need to run docker in docker
+RUN cd /tmp && curl -O https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz && \
+ tar -xvf Python-3.8.2.tar.xz && \
+ cd Python-3.8.2 && \
+ ./configure --enable-optimizations && \
+ make -j 4 && \
+ make altinstall
+
+RUN apt update
+RUN apt install -y mkdocs
+
+RUN pip3.8 install mkdocs-techdocs-core
+
+RUN pip3.8 install cookiecutter && \
+ apt remove -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev libbz2-dev g++ python-pip python-dev && \
+ rm -rf /var/cache/apt/* /tmp/Python-3.8.2
+
+# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
+# The skeleton contains the package.json of each package in the monorepo,
+# and along with yarn.lock and the root package.json, that's enough to run yarn install.
+ADD yarn.lock package.json skeleton.tar ./
+
+RUN yarn install --frozen-lockfile --production
+
+# This will copy the contents of the dist-workspace when running the build-image command.
+# Do not use this Dockerfile outside of that command, as it will copy in the source code instead.
+COPY . .
+
+CMD ["node", "packages/backend"]
+
+
diff --git a/contrib/docker/kubernetes-example-backend/README.md b/contrib/docker/kubernetes-example-backend/README.md
new file mode 100644
index 0000000000..d0f9d57022
--- /dev/null
+++ b/contrib/docker/kubernetes-example-backend/README.md
@@ -0,0 +1,13 @@
+# Example backend Dockerfile
+
+This Dockerfile will build the example backend with certain additional binaries needed to workaround
+the docker requirement in the scaffolder and techdocs.
+
+# Usage
+
+```bash
+yarn docker-build -f --tag
+```
+
+> The absolute path is necessary as this directory is not copied to the build workspace when building
+> the docker image.
diff --git a/docs/assets/architecture-decisions/catalog-core-entities.png b/docs/assets/architecture-decisions/catalog-core-entities.png
index b0c7cb4575..b7f238708f 100644
Binary files a/docs/assets/architecture-decisions/catalog-core-entities.png and b/docs/assets/architecture-decisions/catalog-core-entities.png differ
diff --git a/docs/assets/architecture-overview/circle-ci.png b/docs/assets/architecture-overview/circle-ci.png
index c5695196f1..48aebc9200 100644
Binary files a/docs/assets/architecture-overview/circle-ci.png and b/docs/assets/architecture-overview/circle-ci.png differ
diff --git a/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png b/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png
index 7d08fc5e36..f0a8e8871d 100644
Binary files a/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png and b/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png differ
diff --git a/docs/assets/architecture-overview/lighthouse-plugin-architecture.png b/docs/assets/architecture-overview/lighthouse-plugin-architecture.png
index 0da5d6f042..0d67bcf19b 100644
Binary files a/docs/assets/architecture-overview/lighthouse-plugin-architecture.png and b/docs/assets/architecture-overview/lighthouse-plugin-architecture.png differ
diff --git a/docs/assets/architecture-overview/lighthouse-plugin.png b/docs/assets/architecture-overview/lighthouse-plugin.png
index 9692fc0833..0b9d0ccc8a 100644
Binary files a/docs/assets/architecture-overview/lighthouse-plugin.png and b/docs/assets/architecture-overview/lighthouse-plugin.png differ
diff --git a/docs/assets/architecture-overview/tech-radar-plugin-architecture.png b/docs/assets/architecture-overview/tech-radar-plugin-architecture.png
index d55be5ee82..417a2764a1 100644
Binary files a/docs/assets/architecture-overview/tech-radar-plugin-architecture.png and b/docs/assets/architecture-overview/tech-radar-plugin-architecture.png differ
diff --git a/docs/assets/architecture-overview/tech-radar-plugin.png b/docs/assets/architecture-overview/tech-radar-plugin.png
index dbf39b6c63..062227ac80 100644
Binary files a/docs/assets/architecture-overview/tech-radar-plugin.png and b/docs/assets/architecture-overview/tech-radar-plugin.png differ
diff --git a/docs/assets/auth/oauth-popup-flow.svg b/docs/assets/auth/oauth-popup-flow.svg
index 2132903783..4a6e76ed21 100644
--- a/docs/assets/auth/oauth-popup-flow.svg
+++ b/docs/assets/auth/oauth-popup-flow.svg
@@ -1,50 +1,50 @@
OAuth Consent and Refresh Flow Browser Browser Popup Window Popup Window auth-backend plugin auth-backend plugin Consent Screen Consent Screen OAuth Provider OAuth Provider Components on page ask for an access token with greater scope than the existing session. Open popup GET /auth/<provider>/start?scope=some%20scopes Redirect to consent screen with random nonce in OAuth state and short-lived cookie with the same nonce. GET /consent_url?redirect_uri=<redirect_uri>?nonce=<n> where redirect_uri=<app-origin>/auth/<provider>/handler/frame User consents to access the new scope. Redirect to given redirect URL, with authorization code GET /auth/<provider>/handler/frame?code=<c>&nonce=<n> Request includes the previously set none cookie Verify that the nonce in the cookie matches the nonce in the OAuth state Authorization Code Client ID Client Secret Verify and generate tokens Access Token (ID Token) (Refresh Token) Scope Expire Time Small HTML page with inlined response payload Store Refresh Token in HTTP-only cookie postMessage() with tokens and info or error Close self A later point when a refresh is needed. Either because of a reload or an expiring session. GET /auth/<provider>/token Refresh Token cookie included Refresh Token Client ID Client Secret Access Token (ID Token) Scope Expire Time Tokens and info
\ No newline at end of file
+-->
diff --git a/docs/assets/contributorheader.png b/docs/assets/contributorheader.png
index 53d1027d85..64d0cb4cff 100644
Binary files a/docs/assets/contributorheader.png and b/docs/assets/contributorheader.png differ
diff --git a/docs/assets/dls/DLS.png b/docs/assets/dls/DLS.png
index 94214e4650..f84b94857d 100644
Binary files a/docs/assets/dls/DLS.png and b/docs/assets/dls/DLS.png differ
diff --git a/docs/assets/dls/designheader-updated.png b/docs/assets/dls/designheader-updated.png
index 56c5a56abb..39aded2252 100644
Binary files a/docs/assets/dls/designheader-updated.png and b/docs/assets/dls/designheader-updated.png differ
diff --git a/docs/assets/dls/designheader.png b/docs/assets/dls/designheader.png
index e9ace5c2e7..8f160039df 100644
Binary files a/docs/assets/dls/designheader.png and b/docs/assets/dls/designheader.png differ
diff --git a/docs/assets/dls/running-storybook.png b/docs/assets/dls/running-storybook.png
index 6cf1ded4b9..ce9422b99d 100644
Binary files a/docs/assets/dls/running-storybook.png and b/docs/assets/dls/running-storybook.png differ
diff --git a/docs/assets/dls/storybook-page.png b/docs/assets/dls/storybook-page.png
index 113f96e589..58470feaff 100644
Binary files a/docs/assets/dls/storybook-page.png and b/docs/assets/dls/storybook-page.png differ
diff --git a/docs/assets/getting-started/create-app_output.png b/docs/assets/getting-started/create-app_output.png
index caa39ec6d2..875ad05122 100644
Binary files a/docs/assets/getting-started/create-app_output.png and b/docs/assets/getting-started/create-app_output.png differ
diff --git a/docs/assets/getting-started/create-plugin_output.png b/docs/assets/getting-started/create-plugin_output.png
index f048a9f5fc..47231c2079 100644
Binary files a/docs/assets/getting-started/create-plugin_output.png and b/docs/assets/getting-started/create-plugin_output.png differ
diff --git a/docs/assets/headline.png b/docs/assets/headline.png
index 83d7b14f21..fef99a269f 100644
Binary files a/docs/assets/headline.png and b/docs/assets/headline.png differ
diff --git a/docs/assets/my-plugin_screenshot.png b/docs/assets/my-plugin_screenshot.png
index 0b2817fa1e..4f2849a691 100644
Binary files a/docs/assets/my-plugin_screenshot.png and b/docs/assets/my-plugin_screenshot.png differ
diff --git a/docs/assets/pop.png b/docs/assets/pop.png
index 441126f33c..614136b501 100644
Binary files a/docs/assets/pop.png and b/docs/assets/pop.png differ
diff --git a/docs/assets/software-catalog/bsc-edit.png b/docs/assets/software-catalog/bsc-edit.png
index d1ac1f52e5..f3a7daa1dd 100644
Binary files a/docs/assets/software-catalog/bsc-edit.png and b/docs/assets/software-catalog/bsc-edit.png differ
diff --git a/docs/assets/software-catalog/bsc-register-1.png b/docs/assets/software-catalog/bsc-register-1.png
index 117b2ea8ea..0e33c173fb 100644
Binary files a/docs/assets/software-catalog/bsc-register-1.png and b/docs/assets/software-catalog/bsc-register-1.png differ
diff --git a/docs/assets/software-catalog/bsc-register-2.png b/docs/assets/software-catalog/bsc-register-2.png
index fd1ea7b998..de71141ba0 100644
Binary files a/docs/assets/software-catalog/bsc-register-2.png and b/docs/assets/software-catalog/bsc-register-2.png differ
diff --git a/docs/assets/software-catalog/bsc-search.png b/docs/assets/software-catalog/bsc-search.png
index 8e417cb076..042e6055e0 100644
Binary files a/docs/assets/software-catalog/bsc-search.png and b/docs/assets/software-catalog/bsc-search.png differ
diff --git a/docs/assets/software-catalog/bsc-starred.png b/docs/assets/software-catalog/bsc-starred.png
index 27db19c842..c9721c27a2 100644
Binary files a/docs/assets/software-catalog/bsc-starred.png and b/docs/assets/software-catalog/bsc-starred.png differ
diff --git a/docs/assets/software-catalog/service-catalog-home.png b/docs/assets/software-catalog/service-catalog-home.png
index 742748632e..1d2e8ed317 100644
Binary files a/docs/assets/software-catalog/service-catalog-home.png and b/docs/assets/software-catalog/service-catalog-home.png differ
diff --git a/docs/assets/software-catalog/software-model-core-entities.png b/docs/assets/software-catalog/software-model-core-entities.png
index b718b7527c..60cb283802 100644
Binary files a/docs/assets/software-catalog/software-model-core-entities.png and b/docs/assets/software-catalog/software-model-core-entities.png differ
diff --git a/docs/assets/software-templates/added-to-the-catalog-list.png b/docs/assets/software-templates/added-to-the-catalog-list.png
index 4b544e51c1..c73c33ede3 100644
Binary files a/docs/assets/software-templates/added-to-the-catalog-list.png and b/docs/assets/software-templates/added-to-the-catalog-list.png differ
diff --git a/docs/assets/software-templates/complete.png b/docs/assets/software-templates/complete.png
index eee14fae0e..2a8de0ba1e 100644
Binary files a/docs/assets/software-templates/complete.png and b/docs/assets/software-templates/complete.png differ
diff --git a/docs/assets/software-templates/create.png b/docs/assets/software-templates/create.png
index 8a2e92b9f4..8123780cd3 100644
Binary files a/docs/assets/software-templates/create.png and b/docs/assets/software-templates/create.png differ
diff --git a/docs/assets/software-templates/failed.png b/docs/assets/software-templates/failed.png
index bca8c72d6a..4cb7ce84cc 100644
Binary files a/docs/assets/software-templates/failed.png and b/docs/assets/software-templates/failed.png differ
diff --git a/docs/assets/software-templates/go-to-catalog.png b/docs/assets/software-templates/go-to-catalog.png
index ae16230a02..ea03295846 100644
Binary files a/docs/assets/software-templates/go-to-catalog.png and b/docs/assets/software-templates/go-to-catalog.png differ
diff --git a/docs/assets/software-templates/running.png b/docs/assets/software-templates/running.png
index 208376e059..681c3702f7 100644
Binary files a/docs/assets/software-templates/running.png and b/docs/assets/software-templates/running.png differ
diff --git a/docs/assets/software-templates/template-picked-2.png b/docs/assets/software-templates/template-picked-2.png
index 685e356ee8..a1737c8e00 100644
Binary files a/docs/assets/software-templates/template-picked-2.png and b/docs/assets/software-templates/template-picked-2.png differ
diff --git a/docs/assets/software-templates/template-picked.png b/docs/assets/software-templates/template-picked.png
index 1094acec4a..79b1e260e0 100644
Binary files a/docs/assets/software-templates/template-picked.png and b/docs/assets/software-templates/template-picked.png differ
diff --git a/docs/assets/techdocs/documentation-template.png b/docs/assets/techdocs/documentation-template.png
index 1f44ad27c6..7539f1f6f4 100644
Binary files a/docs/assets/techdocs/documentation-template.png and b/docs/assets/techdocs/documentation-template.png differ
diff --git a/docs/assets/techdocs/techdocs_big_picture.png b/docs/assets/techdocs/techdocs_big_picture.png
index ffe437180f..8b8f7a2338 100644
Binary files a/docs/assets/techdocs/techdocs_big_picture.png and b/docs/assets/techdocs/techdocs_big_picture.png differ
diff --git a/docs/assets/utility-apis-fig1.svg b/docs/assets/utility-apis-fig1.svg
index 47123299c7..3261de88a8 100644
--- a/docs/assets/utility-apis-fig1.svg
+++ b/docs/assets/utility-apis-fig1.svg
@@ -1,3 +1,3 @@
-fooApiRef FooApi Plugin Component App A App B DefaultFooApi CustomFooApi App C Viewer does not support full SVG 1.1
\ No newline at end of file
+fooApiRef FooApi Plugin Component App A App B DefaultFooApi CustomFooApi App C Viewer does not support full SVG 1.1
diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md
index 4ccee8ede9..9dc677b439 100644
--- a/docs/auth/auth-backend-classes.md
+++ b/docs/auth/auth-backend-classes.md
@@ -67,26 +67,21 @@ auth:
google:
development:
clientId:
- $secret:
- env: AUTH_GOOGLE_CLIENT_ID
+ $env: AUTH_GOOGLE_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GOOGLE_CLIENT_SECRET
+ $env: AUTH_GOOGLE_CLIENT_SECRET
github:
development:
clientId:
- $secret:
- env: AUTH_GITHUB_CLIENT_ID
+ $env: AUTH_GITHUB_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GITHUB_CLIENT_SECRET
+ $env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
- $secret:
- env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
+ $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
development:
clientId:
- $secret:
+ $env:
...
```
diff --git a/docs/conf/index.md b/docs/conf/index.md
index 83eff50a7d..e1dfc30582 100644
--- a/docs/conf/index.md
+++ b/docs/conf/index.md
@@ -18,7 +18,8 @@ allowing for customization.
Configuration is stored in `app-config.yaml` files, with support for suffixes
such as `app-config.production.yaml` to override values for specific
environments. The configuration files themselves contain plain YAML, but with
-support for loading in secrets from various sources using a `$secret` key.
+support for loading in secrets from various sources using for example `$env` and
+`$file` keys.
It is also possible to supply configuration through environment variables, for
example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these
diff --git a/docs/conf/writing.md b/docs/conf/writing.md
index 6d338e586d..5564e780bb 100644
--- a/docs/conf/writing.md
+++ b/docs/conf/writing.md
@@ -90,18 +90,17 @@ order:
## Secrets
-Secrets are supported via a special `$secret` key, which in turn provides a
-number of different ways to read in secrets. To load a configuration value as a
-secret, supply an object with a single `$secret` key, and within that supply an
-object that describes how the secret is loaded. For example, the following will
-read the config key `backend.mySecretKey` from the environment variable
-`MY_SECRET_KEY`:
+Secrets are supported via a special secret keys that are prefixed with `$`,
+which in turn provides a number of different ways to read in secrets. To load a
+configuration value as a secret, supply an object with one of the special secret
+keys, for example `$env` or `$file`. A full list of supported secret keys can be
+found below. For example, the following will read the config key
+`backend.mySecretKey` from the environment variable `MY_SECRET_KEY`:
```yaml
backend:
mySecretKey:
- $secret:
- env: MY_SECRET_KEY
+ $env: MY_SECRET_KEY
```
With the above configuration, calling `config.getString('backend.mySecretKey')`
@@ -123,8 +122,7 @@ This reads a secret from an environment variable. For example, the following
config loads the secret from the `MY_SECRET` env var.
```yaml
-$secret:
- env: MY_SECRET
+$env: MY_SECRET
```
### File Secrets
@@ -135,22 +133,19 @@ following reads the contents of `my-secret.txt` relative to the config file
itself:
```yaml
-$secret:
- file: ./my-secret.txt
+$file: ./my-secret.txt
```
### Data File Secrets
This reads secrets from a path within a JSON-like data file. The file path
-behaves similar to file secrets, but in addition a `path` is used to point to a
-specific value inside the file. Supported file extensions are `.json`, `.yaml`,
-and `.yml`. For example, the following would read out `my-secret-key` from
-`my-secrets.json`:
+behaves similar to file secrets, but with the addition of a url fragment that is
+used to point to a specific value inside the file. Supported file extensions are
+`.json`, `.yaml`, and `.yml`. For example, the following would read out
+`my-secret-key` from `my-secrets.json`:
```yaml
-$secret:
- data: ./my-secrets.json
- path: deployment.key
+$data: ./my-secrets.json#deployment.key
# my-secrets.json
{
diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md
index c98bd73ca3..46de2d3d67 100644
--- a/docs/features/software-catalog/configuration.md
+++ b/docs/features/software-catalog/configuration.md
@@ -11,36 +11,50 @@ tasks, such as reading raw entity data from a remote source, parsing it,
transforming it, and validating it. These processors are configured under the
`catalog.processors` key.
-### Processor: github
+### Processor: url
-The `github` processor is responsible for fetching entity data from files on
-GitHub or GitHub Enterprise. The configuration for this processor lives under
-`catalog.processors.github`. Example:
+The `url` processor is responsible for fetching entity data from files in any
+external provider like GitHub, GitLab, Bitbucket, etc. The configuration of this
+processor lives under the top-level `integrations` key, as it is used by other
+parts of Backstage too.
```yaml
-catalog:
- processors:
- github:
- providers:
- - target: https://github.com
- token:
- $secret:
- env: GITHUB_PRIVATE_TOKEN
- - target: https://ghe.example.net
- apiBaseUrl: https://ghe.example.net/api/v3
- rawBaseUrl: https://ghe.example.net/raw
- token:
- $secret:
- env: GHE_PRIVATE_TOKEN
+integrations:
+ github:
+ - host: github.com
+ token:
+ $env: GITHUB_TOKEN
+ - host: ghe.example.net
+ apiBaseUrl: https://ghe.example.net/api/v3
+ rawBaseUrl: https://ghe.example.net/raw
+ token:
+ $env: GHE_TOKEN
+ gitlab:
+ - host: gitlab.com
+ token:
+ $env: GITLAB_TOKEN
+ bitbucket:
+ - host: bitbucket.org
+ username:
+ $env: BITBUCKET_USERNAME
+ appPassword:
+ $env: BITBUCKET_APP_PASSWORD
+ azure:
+ - host: dev.azure.com
+ token:
+ $env: AZURE_TOKEN
```
-The main subkey is `providers`, where you can list the various GitHub compatible
-providers you want to be able to fetch data from. Each entry is a structure with
-up to four elements:
+Each key under `integrations` is a separate configuration for each external
+provider. The providers each have their own configuration, so let's look at the
+GitHub section as an example.
-- `target` (required): The string prefix of the location target that you want to
- match on, with no trailing slash. For GitHub, it should be exactly
- `https://github.com`.
+Directly under the `github` key is a list of provider configurations, where you
+can list the various GitHub compatible providers you want to be able to fetch
+data from. Each entry is a structure with up to four elements:
+
+- `host` (optional): The host of the location target that you want to match on.
+ The default host is `github.com`.
- `token` (optional): An authentication token as expected by GitHub. If
supplied, it will be passed along with all calls to this provider, both API
and raw. If it is not supplied, anonymous access will be used.
@@ -74,7 +88,7 @@ the catalog under the `catalog.locations` key, for example:
```yaml
catalog:
locations:
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
```
@@ -97,7 +111,7 @@ catalog:
- allow: [Component, API, Location, Template]
locations:
- - type: github
+ - type: url
target: https://github.com/org/example/blob/master/org-data.yaml
rules:
- allow: [Group]
diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md
index 7e2a4c3867..b045660202 100644
--- a/docs/features/software-catalog/extending-the-model.md
+++ b/docs/features/software-catalog/extending-the-model.md
@@ -16,11 +16,9 @@ Backstage natively supports tracking of the following component

Since these types are likely not the only kind of software you will want to
-track in Backstage, it is possible to
-
-It is possible to add your own software types that fit your organization's data
-model. Inside Spotify our model has grown significantly over the years, and now
-includes ML models, Apps, data pipelines and many more.
+track in Backstage, it is possible to add your own software types that fit your
+organization's data model. Inside Spotify our model has grown significantly over
+the years, and now includes ML models, Apps, data pipelines and many more.
## Adding a new type
diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md
index 8be6a00579..4ba83146db 100644
--- a/docs/features/software-catalog/index.md
+++ b/docs/features/software-catalog/index.md
@@ -90,7 +90,7 @@ above example can be added using the following configuration:
```yaml
catalog:
locations:
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
```
diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md
index a825020eb6..26990033aa 100644
--- a/docs/features/software-catalog/installation.md
+++ b/docs/features/software-catalog/installation.md
@@ -166,21 +166,21 @@ our example templates through static configuration. Add the following to the
catalog:
locations:
# Backstage Example Component
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml
```
diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md
index 1bd5528f22..40a4ca3ea1 100644
--- a/docs/features/software-catalog/well-known-annotations.md
+++ b/docs/features/software-catalog/well-known-annotations.md
@@ -41,27 +41,6 @@ expecting a two-item array out of it. The format of the target part is
type-dependent and could conceivably even be an empty string, but the separator
colon is always present.
-### backstage.io/definition-at-location
-
-```yaml
-# Example
-apiVersion: backstage.io/v1alpha1
-kind: API
-metadata:
- name: petstore
- annotations:
- backstage.io/definition-at-location: 'url:https://petstore.swagger.io/v2/swagger.json'
-spec:
- type: openapi
-```
-
-This annotation allows to fetch an API definition from another location, instead
-of wrapping the API definition inside the definition field. This allows to
-easily consume existing API definition. The definition is fetched during
-ingestion by a processor and included in the entity. It is updated on every
-refresh. The annotation contains a location reference string that contains the
-location processor type and the target.
-
### backstage.io/techdocs-ref
```yaml
@@ -179,6 +158,21 @@ fallback (`rollbar.organization` followed by `organization.name`).
Specifying this annotation may enable Rollbar related features in Backstage for
that entity.
+### backstage.io/ldap-rdn, backstage.io/ldap-uuid, backstage.io/ldap-dn
+
+```yaml
+# Example:
+metadata:
+ annotations:
+ backstage.io/ldap-rdn: my-team
+ backstage.io/ldap-uuid: c57e8ba2-6cc4-1039-9ebc-d5f241a7ca21
+ backstage.io/ldap-dn: cn=my-team,ou=access,ou=groups,ou=spotify,dc=spotify,dc=net
+```
+
+The value of these annotations are the corresponding attributes that were found
+when ingestion the entity from LDAP. Not all of them may be present, depending
+on what attributes that the server presented at ingestion time.
+
## Deprecated Annotations
The following annotations are deprecated, and only listed here to aid in
@@ -190,6 +184,25 @@ This annotation was used for a while to enable the GitHub Actions feature. This
is now instead using the [github.com/project-slug](#github-com-project-slug)
annotation, with the same value format.
+### backstage.io/definition-at-location
+
+This annotation allowed to load the API definition from another location. Now
+placeholders can be used instead:
+
+```
+apiVersion: backstage.io/v1alpha1
+kind: API
+metadata:
+ name: petstore
+ description: The Petstore API
+spec:
+ type: openapi
+ lifecycle: production
+ owner: petstore@example.com
+ definition:
+ $text: https://petstore.swagger.io/v2/swagger.json
+```
+
## Links
- [Descriptor Format: annotations](descriptor-format.md#annotations-optional)
diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md
index 4283af7c72..5a4e0049ab 100644
--- a/docs/features/software-templates/adding-templates.md
+++ b/docs/features/software-templates/adding-templates.md
@@ -66,7 +66,7 @@ for example
```yaml
catalog:
locations:
- - type: github
+ - type: url
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
rules:
- allow: [Template]
diff --git a/docs/features/software-templates/extending/create-your-own-publisher.md b/docs/features/software-templates/extending/create-your-own-publisher.md
index ecb097f9d9..546980f033 100644
--- a/docs/features/software-templates/extending/create-your-own-publisher.md
+++ b/docs/features/software-templates/extending/create-your-own-publisher.md
@@ -34,7 +34,7 @@ import { Octokit } from '@octokit/rest';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
- const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
+ const githubClient = new Octokit({ auth: process.env.GITHUB_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
return await createRouter({
diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md
index eff7e53ad3..838c5e2d11 100644
--- a/docs/features/software-templates/installation.md
+++ b/docs/features/software-templates/installation.md
@@ -191,13 +191,13 @@ our example templates through static configuration. Add the following to the
catalog:
locations:
# Backstage Example Templates
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
- - type: github
+ - type: url
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
```
@@ -232,15 +232,13 @@ instance:
scaffolder:
github:
token:
- $secret:
- env: GITHUB_ACCESS_TOKEN
+ $env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
gitlab:
api:
baseUrl: https://gitlab.com
token:
- $secret:
- env: SCAFFOLDER_GITLAB_PRIVATE_TOKEN
+ $env: GITLAB_TOKEN
```
#### Azure DevOps
@@ -257,8 +255,7 @@ scaffolder:
baseUrl: https://dev.azure.com/{your-organization}
api:
token:
- $secret:
- env: AZURE_PRIVATE_TOKEN
+ $env: AZURE_TOKEN
```
### Running the Backend
@@ -268,7 +265,7 @@ backend with the new configuration:
```bash
cd packages/backend
-GITHUB_ACCESS_TOKEN= yarn start
+GITHUB_TOKEN= yarn start
```
If you've also set up the frontend plugin, so you should be ready to go browse
diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md
index 831e8af145..9be0e24452 100644
--- a/docs/getting-started/create-an-app.md
+++ b/docs/getting-started/create-an-app.md
@@ -74,6 +74,7 @@ app.
```
app
├── app-config.yaml
+├── catalog-info.yaml
├── lerna.json
├── package.json
└── packages
@@ -83,6 +84,9 @@ app
- **app-config.yaml**: Main configuration file for the app. See
[Configuration](https://backstage.io/docs/conf/) for more information.
+- **catalog-info.yaml**: Catalog Entities descriptors. See
+ [Descriptor Format of Catalog Entities](https://backstage.io/docs/features/software-catalog/descriptor-format)
+ to get started.
- **lerna.json**: Contains information about workspaces and other lerna
configuration needed for the monorepo setup.
- **package.json**: Root package.json for the project. _Note: Be sure that you
diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md
index 154a959f85..aa9aa0bde1 100644
--- a/docs/getting-started/development-environment.md
+++ b/docs/getting-started/development-environment.md
@@ -88,3 +88,17 @@ yarn create-plugin # Create a new plugin
> See
> [package.json](https://github.com/spotify/backstage/blob/master/package.json)
> for other yarn commands/options.
+
+## Local configuration
+
+Backstage allows you to specify the configuration used while running the
+application on your computer. Local configuration is read from
+`app-config.local.yaml`. This file is ignored by Git, which means that you can
+safely use it to reference secrets like GitHub tokens without worrying about
+these secrets, inadvertently ending up in the Git repository. You do not need to
+copy everything from the default config to the local config.
+`app-config.local.yaml` will be merged with `app-config.yaml` and overwrite the
+default app configs.
+
+You can learn more about the local configuration in
+[Static Configuration in Backstage](../conf/) section.
diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md
index c1fbda379a..3c1ccb6438 100644
--- a/docs/plugins/existing-plugins.md
+++ b/docs/plugins/existing-plugins.md
@@ -4,12 +4,15 @@ title: Existing plugins
description: Lists of existing open source plugins
---
-## Open source plugins
+## The Plugin Marketplace
-The full list of open source plugins can be found
-[here](https://github.com/spotify/backstage/tree/master/plugins).
+Open source plugins that you can add to your Backstage deployment can be found
+at:
-## Plugin gallery
+https://backstage.io/plugins
-TODO: In the future we would like to have something similar to
-https://grafana.com/grafana/plugins
+
+
+## Links
+
+- [[blog] The Plugin Marketplace is open](https://backstage.io/blog/2020/09/30/plugin-marketplace)
diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md
index a691c7f0bc..f55b4d9229 100644
--- a/docs/plugins/proxying.md
+++ b/docs/plugins/proxying.md
@@ -41,8 +41,7 @@ proxy:
target: http://larger.example.com:8080/svc.v1
headers:
Authorization:
- $secret:
- env: EXAMPLE_AUTH_HEADER
+ $env: EXAMPLE_AUTH_HEADER
```
Each key under the proxy configuration entry is a route to match, below the
@@ -58,6 +57,15 @@ is also possible to limit the forwarded HTTP methods with the configuration
`allowedMethods`, for example `allowedMethods: ['GET']` to enforce read-only
access.
+By default, the proxy will only forward safe HTTP request headers to the target.
+Those are based on the headers that are considered safe for CORS and includes
+headers like `content-type` or `last-modified`, as well as all headers that are
+set by the proxy. If the proxy should forward other headers like
+`authorization`, this must be enabled by the `allowedHeaders` config, for
+example `allowedHeaders: ['Authorization']`. This should help to not
+accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to
+third-parties.
+
If the value is a string, it is assumed to correspond to:
```yaml
diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md
index 7403f2c2dd..8f6b35617b 100644
--- a/docs/tutorials/quickstart-app-auth.md
+++ b/docs/tutorials/quickstart-app-auth.md
@@ -72,15 +72,12 @@ auth:
github:
development:
clientId:
- $secret:
- env: AUTH_GITHUB_CLIENT_ID
+ $env: AUTH_GITHUB_CLIENT_ID
clientSecret:
- $secret:
- env: AUTH_GITHUB_CLIENT_SECRET
+ $env: AUTH_GITHUB_CLIENT_SECRET
## uncomment the following three lines if using enterprise
# enterpriseInstanceUrl:
- # $secret:
- # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
+ # $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
```
2. Set environment variables in whatever fashion is easiest for you. I chose to
diff --git a/lerna.json b/lerna.json
index d43d4ff160..7d514abb97 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
- "version": "0.1.1-alpha.23"
+ "version": "0.1.1-alpha.24"
}
diff --git a/microsite/.prettierignore b/microsite/.prettierignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/microsite/.prettierignore
@@ -0,0 +1 @@
+build
diff --git a/microsite/blog/2020-09-30-plugin-marketplace.md b/microsite/blog/2020-09-30-plugin-marketplace.md
index a8b9a88c20..b7a799097f 100644
--- a/microsite/blog/2020-09-30-plugin-marketplace.md
+++ b/microsite/blog/2020-09-30-plugin-marketplace.md
@@ -6,7 +6,7 @@ authorURL: https://twitter.com/stalund
Backstage has an ambitious goal: to provide engineers with the best possible developer experience.
-A fantastic developer experience leads to happy, creative, and productive engineers. Our belief is that engineers should not have to be experts in various infrastructure tools to be productive. Infrastructure should be abstracted away, so that developers can spend more cycles building and testing, quickly and safely. Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end.
+A great developer experience leads to happy, creative, and productive engineers. Our belief is that engineers should not have to be experts in various infrastructure tools or disciplines (e.g., machine learning or backend) to be productive. Infrastructure should be abstracted away, so that developers can spend more cycles building and testing, quickly and safely. Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end.
Now you may be thinking, “Yeah, sure, that sounds nice and all, but how does Backstage actually abstract away infrastructure?” The short answer: [plugins](https://backstage.io/plugins).
@@ -16,11 +16,11 @@ Now you may be thinking, “Yeah, sure, that sounds nice and all, but how does B
Think of plugins as a mini version of an infrastructure tool or service — just the parts you need, made quick and easy. The advantage of using a Backstage plugin instead of a tool’s dedicated UI is that all your infrastructure is packaged into a “single pane of glass” instead of being spread out like independently built “islands”. Once you grow your infrastructure portfolio, the complexity really starts to add up. The Backstage plugin model allows you to add more tools without increasing the cognitive load for your users.
-Our goal for the plugin ecosystem is that anything can be a plugin. The definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://backstage.io/blog/2020/09/30/backstage-design-system) we ensure the overall user experience stays consistent between plugins. If we all do our job well, the end user of Backstage does not see the boundaries between plugins. They are interacting with one consistent product — with many features.
+Our goal for the plugin ecosystem is that anything can be a plugin. The definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following clear [design guidelines](https://backstage.io/blog/2020/09/30/backstage-design-system) we ensure the overall user experience stays consistent between plugins. If we all do our job well, the end user of Backstage does not see the boundaries between plugins. They are interacting with one consistent product — with many features.
## Building an ecosystem
-Imagine a not-so-distant future where you walk up to Backstage, install it in your environment, and then choose from a gallery of existing, open source plugins that match whatever infrastructure you use inside your company. That is our vision for the Backstage plugin ecosystem.
+Imagine a not-so-distant future where you walk up to Backstage, install it in your environment, and then choose from a gallery of existing, open source plugins that serve and match whatever infrastructure and software development needs you have inside your company. That means you can get started with Backstage and see the gains of improved productivity within days, not months. That is our vision for the Backstage plugin ecosystem.
Running services on Kubernetes? There’s a plugin for that. Using Snyk for security scanning? There’s a plugin for that. Grafana, DataDog, or Rollbar for monitoring? There’s a plugin for that. Using Jenkins, CircleCI, GitHub Actions, or Travis CI? Pick the CI plugin of your choice. You get the picture. Regardless of your stack, there’s a plugin that you can use.
@@ -32,7 +32,7 @@ This grand vision is actually not that far off. Already today there is a growing
## Creating and suggesting new plugins
-Not all plugins you need will be open source. Every company has their own homegrown tooling. Building internal plugins lets you tailor your version of Backstage to be a perfect fit for your infrastructure. If you end up [building plugins](https://backstage.io/docs/plugins/create-a-plugin) that could be useful for other companies, please consider releasing them as open source and [add them to the Marketplace](https://backstage.io/docs/plugins/add-to-marketplace).
+Not all plugins you need will be open source. Every company has their own homegrown tooling. Building internal plugins lets you tailor your version of Backstage to be a perfect fit for your infrastructure and software development needs. If you end up [building plugins](https://backstage.io/docs/plugins/create-a-plugin) that could be useful for other companies, please consider releasing them as open source and [add them to the Marketplace](https://backstage.io/docs/plugins/add-to-marketplace).
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new plugin Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development and opens up opportunities for collaboration. You can also use this process if you have an idea for a good plugin, but you need help building it.
@@ -40,4 +40,4 @@ We are really excited to see all the amazing plugins that have already been buil
What plugins would you like to see in the Plugin Marketplace? [Tell us](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME)!
-_Special shout-out to community member [Iain Billett](https://github.com/iain-b) from Roadie for helping build and contribute the [plugin marketplace page](https://backstage.io/plugins) (as his first PR no less!)._
+_Special shout-out to community member [Iain Billett](https://github.com/iain-b) from Roadie for helping build and contribute the [Plugin Marketplace page](https://backstage.io/plugins) (as his first PR no less!)._
diff --git a/microsite/blog/assets/2/screen.gif b/microsite/blog/assets/2/screen.gif
index a0d52a23b4..f790d639cc 100644
Binary files a/microsite/blog/assets/2/screen.gif and b/microsite/blog/assets/2/screen.gif differ
diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png
index 4a50f17d15..386b195ea9 100644
Binary files a/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png differ
diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png
index 8067f665c9..1a48b1900c 100644
Binary files a/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png differ
diff --git a/microsite/blog/assets/20-05-20/tabs.png b/microsite/blog/assets/20-05-20/tabs.png
index 4a5b11f6fe..71516b6846 100644
Binary files a/microsite/blog/assets/20-05-20/tabs.png and b/microsite/blog/assets/20-05-20/tabs.png differ
diff --git a/microsite/blog/assets/20-07-01/auth-landing.png b/microsite/blog/assets/20-07-01/auth-landing.png
index e00f4c2e43..064802c90f 100644
Binary files a/microsite/blog/assets/20-07-01/auth-landing.png and b/microsite/blog/assets/20-07-01/auth-landing.png differ
diff --git a/microsite/blog/assets/20-07-01/auth-sidebar.png b/microsite/blog/assets/20-07-01/auth-sidebar.png
index b089ab6c0a..7c72a96ab5 100644
Binary files a/microsite/blog/assets/20-07-01/auth-sidebar.png and b/microsite/blog/assets/20-07-01/auth-sidebar.png differ
diff --git a/microsite/blog/assets/2020-08-05/cards.png b/microsite/blog/assets/2020-08-05/cards.png
index 4779618b92..a3e038ce1a 100644
Binary files a/microsite/blog/assets/2020-08-05/cards.png and b/microsite/blog/assets/2020-08-05/cards.png differ
diff --git a/microsite/blog/assets/2020-08-05/catalog.png b/microsite/blog/assets/2020-08-05/catalog.png
index e9c0c65ade..b1f3680511 100644
Binary files a/microsite/blog/assets/2020-08-05/catalog.png and b/microsite/blog/assets/2020-08-05/catalog.png differ
diff --git a/microsite/blog/assets/2020-08-05/create-component.png b/microsite/blog/assets/2020-08-05/create-component.png
index 4d815393fc..7f0c630d58 100644
Binary files a/microsite/blog/assets/2020-08-05/create-component.png and b/microsite/blog/assets/2020-08-05/create-component.png differ
diff --git a/microsite/blog/assets/2020-08-05/template-form.png b/microsite/blog/assets/2020-08-05/template-form.png
index 5805243f59..0643a7f3e8 100644
Binary files a/microsite/blog/assets/2020-08-05/template-form.png and b/microsite/blog/assets/2020-08-05/template-form.png differ
diff --git a/microsite/blog/assets/2020-08-05/templates.png b/microsite/blog/assets/2020-08-05/templates.png
index e350d463f6..af76121a72 100644
Binary files a/microsite/blog/assets/2020-08-05/templates.png and b/microsite/blog/assets/2020-08-05/templates.png differ
diff --git a/microsite/blog/assets/3/audit-list.png b/microsite/blog/assets/3/audit-list.png
index 84b64f976a..df18b9923f 100644
Binary files a/microsite/blog/assets/3/audit-list.png and b/microsite/blog/assets/3/audit-list.png differ
diff --git a/microsite/blog/assets/3/audit-view.png b/microsite/blog/assets/3/audit-view.png
index a2e9716cf4..df5f5691b0 100644
Binary files a/microsite/blog/assets/3/audit-view.png and b/microsite/blog/assets/3/audit-view.png differ
diff --git a/microsite/blog/assets/3/create-audit.png b/microsite/blog/assets/3/create-audit.png
index cb28de73a4..2b22d66b18 100644
Binary files a/microsite/blog/assets/3/create-audit.png and b/microsite/blog/assets/3/create-audit.png differ
diff --git a/microsite/blog/assets/3/lead-copy.png b/microsite/blog/assets/3/lead-copy.png
index fbf247eec8..8476461909 100644
Binary files a/microsite/blog/assets/3/lead-copy.png and b/microsite/blog/assets/3/lead-copy.png differ
diff --git a/microsite/blog/assets/3/lead.png b/microsite/blog/assets/3/lead.png
index 4b60c4961c..639b5b4a9e 100644
Binary files a/microsite/blog/assets/3/lead.png and b/microsite/blog/assets/3/lead.png differ
diff --git a/microsite/blog/assets/4/create-app.png b/microsite/blog/assets/4/create-app.png
index 52dcc13097..5ff683e5d0 100644
Binary files a/microsite/blog/assets/4/create-app.png and b/microsite/blog/assets/4/create-app.png differ
diff --git a/microsite/blog/assets/4/welcome.png b/microsite/blog/assets/4/welcome.png
index 5de0d57098..e217fda699 100644
Binary files a/microsite/blog/assets/4/welcome.png and b/microsite/blog/assets/4/welcome.png differ
diff --git a/microsite/blog/assets/5/lead.png b/microsite/blog/assets/5/lead.png
index 657268fc09..615cf4d349 100644
Binary files a/microsite/blog/assets/5/lead.png and b/microsite/blog/assets/5/lead.png differ
diff --git a/microsite/blog/assets/6/header.png b/microsite/blog/assets/6/header.png
index 6908e40dbc..3c39971d36 100644
Binary files a/microsite/blog/assets/6/header.png and b/microsite/blog/assets/6/header.png differ
diff --git a/microsite/blog/assets/Backstage-mural-DS.png b/microsite/blog/assets/Backstage-mural-DS.png
index 2944874706..6e2021a37c 100644
Binary files a/microsite/blog/assets/Backstage-mural-DS.png and b/microsite/blog/assets/Backstage-mural-DS.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/discover1.png b/microsite/blog/assets/announcing-techdocs/discover1.png
index 5b23e64b43..33869b4d64 100644
Binary files a/microsite/blog/assets/announcing-techdocs/discover1.png and b/microsite/blog/assets/announcing-techdocs/discover1.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/discover2.png b/microsite/blog/assets/announcing-techdocs/discover2.png
index 3737ee68db..e7ed8d39fe 100644
Binary files a/microsite/blog/assets/announcing-techdocs/discover2.png and b/microsite/blog/assets/announcing-techdocs/discover2.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png b/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png
index f3b724ff4f..ee8b4dead0 100644
Binary files a/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png and b/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/feedback-loop1.png b/microsite/blog/assets/announcing-techdocs/feedback-loop1.png
index 4e2d0de5df..d78144bded 100644
Binary files a/microsite/blog/assets/announcing-techdocs/feedback-loop1.png and b/microsite/blog/assets/announcing-techdocs/feedback-loop1.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/feedback-loop2.png b/microsite/blog/assets/announcing-techdocs/feedback-loop2.png
index 516780e3ac..5177451c9e 100644
Binary files a/microsite/blog/assets/announcing-techdocs/feedback-loop2.png and b/microsite/blog/assets/announcing-techdocs/feedback-loop2.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/github-issues.png b/microsite/blog/assets/announcing-techdocs/github-issues.png
index 3d5f3465c0..dfe345bd0a 100644
Binary files a/microsite/blog/assets/announcing-techdocs/github-issues.png and b/microsite/blog/assets/announcing-techdocs/github-issues.png differ
diff --git a/microsite/blog/assets/announcing-techdocs/metrics.png b/microsite/blog/assets/announcing-techdocs/metrics.png
index b332a65b0d..58e63951bf 100644
Binary files a/microsite/blog/assets/announcing-techdocs/metrics.png and b/microsite/blog/assets/announcing-techdocs/metrics.png differ
diff --git a/microsite/blog/assets/backstage-DS-header.png b/microsite/blog/assets/backstage-DS-header.png
index 2160ef3229..2637bcc2a3 100644
Binary files a/microsite/blog/assets/backstage-DS-header.png and b/microsite/blog/assets/backstage-DS-header.png differ
diff --git a/microsite/blog/assets/backstage-discord-DS.png b/microsite/blog/assets/backstage-discord-DS.png
index a0e513d7d8..bfee45ff18 100644
Binary files a/microsite/blog/assets/backstage-discord-DS.png and b/microsite/blog/assets/backstage-discord-DS.png differ
diff --git a/microsite/blog/assets/backstage-figma1-DS.png b/microsite/blog/assets/backstage-figma1-DS.png
index 00268eac2a..c2c32a00cb 100644
Binary files a/microsite/blog/assets/backstage-figma1-DS.png and b/microsite/blog/assets/backstage-figma1-DS.png differ
diff --git a/microsite/blog/assets/backstage-github-ds.png b/microsite/blog/assets/backstage-github-ds.png
index 6bd6bd976e..297056de6b 100644
Binary files a/microsite/blog/assets/backstage-github-ds.png and b/microsite/blog/assets/backstage-github-ds.png differ
diff --git a/microsite/blog/assets/backstage-guide-DS.png b/microsite/blog/assets/backstage-guide-DS.png
index 8c0c7e7c2e..4d57201010 100644
Binary files a/microsite/blog/assets/backstage-guide-DS.png and b/microsite/blog/assets/backstage-guide-DS.png differ
diff --git a/microsite/blog/assets/backstage-guidelines-ds.png b/microsite/blog/assets/backstage-guidelines-ds.png
index be5e9ff16f..3176a694db 100644
Binary files a/microsite/blog/assets/backstage-guidelines-ds.png and b/microsite/blog/assets/backstage-guidelines-ds.png differ
diff --git a/microsite/blog/assets/backstage-storybook-ds.png b/microsite/blog/assets/backstage-storybook-ds.png
index aeff6650eb..94d255b154 100644
Binary files a/microsite/blog/assets/backstage-storybook-ds.png and b/microsite/blog/assets/backstage-storybook-ds.png differ
diff --git a/microsite/blog/assets/backstage-world-DS.png b/microsite/blog/assets/backstage-world-DS.png
index 18e0da1425..354bf62829 100644
Binary files a/microsite/blog/assets/backstage-world-DS.png and b/microsite/blog/assets/backstage-world-DS.png differ
diff --git a/microsite/blog/assets/blog_1.png b/microsite/blog/assets/blog_1.png
index f8c3516fa7..7667c54538 100644
Binary files a/microsite/blog/assets/blog_1.png and b/microsite/blog/assets/blog_1.png differ
diff --git a/microsite/blog/assets/cncf-sandbox/cncf.png b/microsite/blog/assets/cncf-sandbox/cncf.png
index 624094b016..2edfeb9c8e 100644
Binary files a/microsite/blog/assets/cncf-sandbox/cncf.png and b/microsite/blog/assets/cncf-sandbox/cncf.png differ
diff --git a/microsite/blog/assets/illustration.svg b/microsite/blog/assets/illustration.svg
index 50e865ed4f..bba49c0449 100644
--- a/microsite/blog/assets/illustration.svg
+++ b/microsite/blog/assets/illustration.svg
@@ -1,104 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/blog/assets/marketplace.png b/microsite/blog/assets/marketplace.png
index 7681b436c7..01132b6aa2 100644
Binary files a/microsite/blog/assets/marketplace.png and b/microsite/blog/assets/marketplace.png differ
diff --git a/microsite/package.json b/microsite/package.json
index 734f1e4d49..335ada7212 100644
--- a/microsite/package.json
+++ b/microsite/package.json
@@ -7,13 +7,17 @@
"examples": "docusaurus-examples",
"start": "docusaurus-start",
"build": "docusaurus-build",
+ "prettier:check": "prettier --check .",
"publish-gh-pages": "docusaurus-publish",
"write-translations": "docusaurus-write-translations",
"version": "docusaurus-version",
"rename-version": "docusaurus-rename-version"
},
"devDependencies": {
- "docusaurus": "^2.0.0-alpha.64",
- "js-yaml": "^3.14.0"
- }
+ "@spotify/prettier-config": "^8.0.0",
+ "docusaurus": "^2.0.0-alpha.65",
+ "js-yaml": "^3.14.0",
+ "prettier": "^2.0.5"
+ },
+ "prettier": "@spotify/prettier-config"
}
diff --git a/microsite/pages/en/plugins.js b/microsite/pages/en/plugins.js
index 0ef8791970..a8255bcd0d 100644
--- a/microsite/pages/en/plugins.js
+++ b/microsite/pages/en/plugins.js
@@ -29,7 +29,7 @@ const Plugins = () => (
-
Plugin marketplace
+
Plugin Marketplace
Open source plugins that you can add to your Backstage deployment.
Learn how to build a plugin .
@@ -39,7 +39,7 @@ const Plugins = () => (
className="PluginAddNewButton ButtonFilled"
href={addPluginDocsLink}
>
- Add to marketplace
+ Add to Marketplace
@@ -93,7 +93,7 @@ const Plugins = () => (
}}
>
- Add to marketplace
+ Add to Marketplace
diff --git a/microsite/static/animations/backstage-logos-hero-8.gif b/microsite/static/animations/backstage-logos-hero-8.gif
index b9a218f395..2d332d3f38 100644
Binary files a/microsite/static/animations/backstage-logos-hero-8.gif and b/microsite/static/animations/backstage-logos-hero-8.gif differ
diff --git a/microsite/static/animations/backstage-plugin-icon-2.gif b/microsite/static/animations/backstage-plugin-icon-2.gif
index f8e4107e55..e02a8089f4 100644
Binary files a/microsite/static/animations/backstage-plugin-icon-2.gif and b/microsite/static/animations/backstage-plugin-icon-2.gif differ
diff --git a/microsite/static/animations/backstage-service-catalog-icon-1.gif b/microsite/static/animations/backstage-service-catalog-icon-1.gif
index e888a46e90..72414a8a5d 100644
Binary files a/microsite/static/animations/backstage-service-catalog-icon-1.gif and b/microsite/static/animations/backstage-service-catalog-icon-1.gif differ
diff --git a/microsite/static/animations/backstage-software-templates-icon-5.gif b/microsite/static/animations/backstage-software-templates-icon-5.gif
index 8e885393de..a5332ab3c8 100644
Binary files a/microsite/static/animations/backstage-software-templates-icon-5.gif and b/microsite/static/animations/backstage-software-templates-icon-5.gif differ
diff --git a/microsite/static/animations/backstage-speed-paradox-7.gif b/microsite/static/animations/backstage-speed-paradox-7.gif
index 3112e7698f..b6f16d2394 100644
Binary files a/microsite/static/animations/backstage-speed-paradox-7.gif and b/microsite/static/animations/backstage-speed-paradox-7.gif differ
diff --git a/microsite/static/animations/backstage-standards-paradox-4.gif b/microsite/static/animations/backstage-standards-paradox-4.gif
index 9cd921cce2..775cc38336 100644
Binary files a/microsite/static/animations/backstage-standards-paradox-4.gif and b/microsite/static/animations/backstage-standards-paradox-4.gif differ
diff --git a/microsite/static/animations/backstage-techdocs-icon-1.gif b/microsite/static/animations/backstage-techdocs-icon-1.gif
index 40f4ca4dd6..72aba32b09 100644
Binary files a/microsite/static/animations/backstage-techdocs-icon-1.gif and b/microsite/static/animations/backstage-techdocs-icon-1.gif differ
diff --git a/microsite/static/css/plugins.css b/microsite/static/css/plugins.css
index a753e16047..9a8bbafa46 100644
--- a/microsite/static/css/plugins.css
+++ b/microsite/static/css/plugins.css
@@ -1,5 +1,5 @@
.PluginCard {
- background-color: #272822;
+ background-color: #282828;
height: 100%;
padding: 16px;
display: flex;
@@ -53,13 +53,12 @@
.ButtonFilled {
padding: 4px 8px;
border-radius: 4px;
- background-color: #36baa2;
- color: white;
+ color: #69ddc7;
margin-top: 36px;
}
.ButtonFilled:hover {
- border: 1px solid #36baa2;
+ border: 1px solid #69ddc7;
background-color: transparent;
}
@@ -67,8 +66,8 @@
font-size: small;
border-radius: 16px;
padding: 2px 8px;
- border: 1px solid #36baa2;
- color: #36baa2;
+ border: 1px solid #69ddc7;
+ color: #69ddc7;
}
.PluginCardLink {
@@ -112,5 +111,5 @@
}
#add-plugin-card {
- border: 1px solid #36baa2;
+ border: 1px solid #69ddc7;
}
diff --git a/microsite/static/img/android-chrome-192x192.png b/microsite/static/img/android-chrome-192x192.png
index 4660f988c1..6b1af9bd95 100644
Binary files a/microsite/static/img/android-chrome-192x192.png and b/microsite/static/img/android-chrome-192x192.png differ
diff --git a/microsite/static/img/android-chrome-512x512.png b/microsite/static/img/android-chrome-512x512.png
index d9b0a6ba78..736ff90f8b 100644
Binary files a/microsite/static/img/android-chrome-512x512.png and b/microsite/static/img/android-chrome-512x512.png differ
diff --git a/microsite/static/img/apple-touch-icon.png b/microsite/static/img/apple-touch-icon.png
index 57c05cfc9a..c1dcaad2a8 100644
Binary files a/microsite/static/img/apple-touch-icon.png and b/microsite/static/img/apple-touch-icon.png differ
diff --git a/microsite/static/img/backstage-logo-cncf.svg b/microsite/static/img/backstage-logo-cncf.svg
index b7d10e429c..b5ff591d1b 100644
--- a/microsite/static/img/backstage-logo-cncf.svg
+++ b/microsite/static/img/backstage-logo-cncf.svg
@@ -1 +1 @@
-05 Logo_Black
\ No newline at end of file
+05 Logo_Black
\ No newline at end of file
diff --git a/microsite/static/img/cards-mobile.png b/microsite/static/img/cards-mobile.png
index 9cb74e0aff..780da80e3b 100644
Binary files a/microsite/static/img/cards-mobile.png and b/microsite/static/img/cards-mobile.png differ
diff --git a/microsite/static/img/cards-wide.png b/microsite/static/img/cards-wide.png
index db72cc072b..392bd38382 100644
Binary files a/microsite/static/img/cards-wide.png and b/microsite/static/img/cards-wide.png differ
diff --git a/microsite/static/img/cards.png b/microsite/static/img/cards.png
index f388dfdb4c..b0f843e29c 100644
Binary files a/microsite/static/img/cards.png and b/microsite/static/img/cards.png differ
diff --git a/microsite/static/img/cncf-color.svg b/microsite/static/img/cncf-color.svg
index 12f7d3e48a..2b9d5117f9 100644
--- a/microsite/static/img/cncf-color.svg
+++ b/microsite/static/img/cncf-color.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg
index d94aaf3249..c5e3b2af9a 100644
--- a/microsite/static/img/cncf-white.svg
+++ b/microsite/static/img/cncf-white.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/microsite/static/img/code.gif b/microsite/static/img/code.gif
index c0d510f0cc..f5031ed50c 100644
Binary files a/microsite/static/img/code.gif and b/microsite/static/img/code.gif differ
diff --git a/microsite/static/img/code.png b/microsite/static/img/code.png
index b9d53ee4c5..5e33734b5f 100644
Binary files a/microsite/static/img/code.png and b/microsite/static/img/code.png differ
diff --git a/microsite/static/img/compliance.svg b/microsite/static/img/compliance.svg
index 7214c5bd9e..b4e72def73 100644
--- a/microsite/static/img/compliance.svg
+++ b/microsite/static/img/compliance.svg
@@ -1,68 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/components-with-filter-small.png b/microsite/static/img/components-with-filter-small.png
index 4cbe2e8371..bf80faaafb 100644
Binary files a/microsite/static/img/components-with-filter-small.png and b/microsite/static/img/components-with-filter-small.png differ
diff --git a/microsite/static/img/components-with-filter.png b/microsite/static/img/components-with-filter.png
index 294e078cf2..a232699fa4 100644
Binary files a/microsite/static/img/components-with-filter.png and b/microsite/static/img/components-with-filter.png differ
diff --git a/microsite/static/img/demo-illustration.svg b/microsite/static/img/demo-illustration.svg
index 984b97335b..036912cafa 100644
--- a/microsite/static/img/demo-illustration.svg
+++ b/microsite/static/img/demo-illustration.svg
@@ -1,66 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/developers.svg b/microsite/static/img/developers.svg
index f761828ec7..0c15235213 100644
--- a/microsite/static/img/developers.svg
+++ b/microsite/static/img/developers.svg
@@ -1,49 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/docs-like-code.png b/microsite/static/img/docs-like-code.png
index c9fbd0c466..0913ee1cf4 100644
Binary files a/microsite/static/img/docs-like-code.png and b/microsite/static/img/docs-like-code.png differ
diff --git a/microsite/static/img/dot.svg b/microsite/static/img/dot.svg
index 93c7f25bdd..7090c946f7 100644
--- a/microsite/static/img/dot.svg
+++ b/microsite/static/img/dot.svg
@@ -1,6146 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/embraced.svg b/microsite/static/img/embraced.svg
index f1c023eea6..e0bacf2a9b 100644
--- a/microsite/static/img/embraced.svg
+++ b/microsite/static/img/embraced.svg
@@ -1,94 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/favicon.svg b/microsite/static/img/favicon.svg
index 535eea5543..8422122718 100644
--- a/microsite/static/img/favicon.svg
+++ b/microsite/static/img/favicon.svg
@@ -1,17 +1 @@
-
-
- favicon
-
-
-
-
-
+favicon
\ No newline at end of file
diff --git a/microsite/static/img/laptop-screen.svg b/microsite/static/img/laptop-screen.svg
index a35f880a22..6e63cb50a1 100644
--- a/microsite/static/img/laptop-screen.svg
+++ b/microsite/static/img/laptop-screen.svg
@@ -1,9 +1 @@
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/laptop.svg b/microsite/static/img/laptop.svg
index c3aad71ce9..5a4b97d216 100644
--- a/microsite/static/img/laptop.svg
+++ b/microsite/static/img/laptop.svg
@@ -1,45 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/logo-black-248x250.png b/microsite/static/img/logo-black-248x250.png
index fdf9a00311..a9cb12ca8e 100644
Binary files a/microsite/static/img/logo-black-248x250.png and b/microsite/static/img/logo-black-248x250.png differ
diff --git a/microsite/static/img/logo-gradient-on-dark.svg b/microsite/static/img/logo-gradient-on-dark.svg
index 57bb1bdd24..2cd4fad4e5 100644
--- a/microsite/static/img/logo-gradient-on-dark.svg
+++ b/microsite/static/img/logo-gradient-on-dark.svg
@@ -1,16 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/logo.svg b/microsite/static/img/logo.svg
index b3babaa39a..bee663d69e 100644
--- a/microsite/static/img/logo.svg
+++ b/microsite/static/img/logo.svg
@@ -1 +1 @@
-03 Logo_Teal
\ No newline at end of file
+03 Logo_Teal
\ No newline at end of file
diff --git a/microsite/static/img/logos-background.svg b/microsite/static/img/logos-background.svg
index 141b9428c6..610085b9c0 100644
--- a/microsite/static/img/logos-background.svg
+++ b/microsite/static/img/logos-background.svg
@@ -1,19 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/logos-signature.svg b/microsite/static/img/logos-signature.svg
index 5acbac2a8c..2a10e8f3b4 100644
--- a/microsite/static/img/logos-signature.svg
+++ b/microsite/static/img/logos-signature.svg
@@ -1,5 +1 @@
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/logos.svg b/microsite/static/img/logos.svg
index d2e0f16c3d..5e9163ee3a 100644
--- a/microsite/static/img/logos.svg
+++ b/microsite/static/img/logos.svg
@@ -1,142 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/mstile-150x150.png b/microsite/static/img/mstile-150x150.png
index 6a63de8a44..713e7eb495 100644
Binary files a/microsite/static/img/mstile-150x150.png and b/microsite/static/img/mstile-150x150.png differ
diff --git a/microsite/static/img/open-platform.svg b/microsite/static/img/open-platform.svg
index 68b35afbec..6abbc0c7d9 100644
--- a/microsite/static/img/open-platform.svg
+++ b/microsite/static/img/open-platform.svg
@@ -1,54 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/photo-montage.svg b/microsite/static/img/photo-montage.svg
index bb7142b19a..88c1809d2d 100644
--- a/microsite/static/img/photo-montage.svg
+++ b/microsite/static/img/photo-montage.svg
@@ -1,35 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/plugin-mobile.svg b/microsite/static/img/plugin-mobile.svg
index 503e075cea..3d8db71d02 100644
--- a/microsite/static/img/plugin-mobile.svg
+++ b/microsite/static/img/plugin-mobile.svg
@@ -1,104 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/plugin.svg b/microsite/static/img/plugin.svg
index 6ac64e3059..7885f414a7 100644
--- a/microsite/static/img/plugin.svg
+++ b/microsite/static/img/plugin.svg
@@ -1,108 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/microsite/static/img/screen.gif b/microsite/static/img/screen.gif
index ecc82d0875..f790d639cc 100644
Binary files a/microsite/static/img/screen.gif and b/microsite/static/img/screen.gif differ
diff --git a/microsite/static/img/spring-boot-service.png b/microsite/static/img/spring-boot-service.png
index fccea79822..da0122abb1 100644
Binary files a/microsite/static/img/spring-boot-service.png and b/microsite/static/img/spring-boot-service.png differ
diff --git a/microsite/static/img/techdocs-static-mobile.png b/microsite/static/img/techdocs-static-mobile.png
index 082518fe21..c827b49a83 100644
Binary files a/microsite/static/img/techdocs-static-mobile.png and b/microsite/static/img/techdocs-static-mobile.png differ
diff --git a/microsite/static/img/techdocs-web.png b/microsite/static/img/techdocs-web.png
index d1306fb6e7..c460e8eb4d 100644
Binary files a/microsite/static/img/techdocs-web.png and b/microsite/static/img/techdocs-web.png differ
diff --git a/microsite/static/img/techdocs.gif b/microsite/static/img/techdocs.gif
index 1f0b2b034d..b133caf058 100644
Binary files a/microsite/static/img/techdocs.gif and b/microsite/static/img/techdocs.gif differ
diff --git a/microsite/static/img/techdocs.png b/microsite/static/img/techdocs.png
index a726012b68..f789745582 100644
Binary files a/microsite/static/img/techdocs.png and b/microsite/static/img/techdocs.png differ
diff --git a/microsite/static/img/techdocs2.gif b/microsite/static/img/techdocs2.gif
index ba937581b3..201f192330 100644
Binary files a/microsite/static/img/techdocs2.gif and b/microsite/static/img/techdocs2.gif differ
diff --git a/microsite/static/img/techs.png b/microsite/static/img/techs.png
index 20026ccc92..46dfad36db 100644
Binary files a/microsite/static/img/techs.png and b/microsite/static/img/techs.png differ
diff --git a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg
index cb0b6c7959..93b69e963c 100644
Binary files a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg and b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg differ
diff --git a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg
index cbd97e78ea..76f691ba5e 100644
Binary files a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg and b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png
index 84a1dba855..02db1a275c 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png
index 29264f8ac0..0820c6ae50 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png
index 092599ce63..b75137ed31 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png
index d662301101..a9c69443ff 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png
index 7da1808784..dfc99e57ed 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png
index 5049d54513..b33b97254c 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png differ
diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png
index 889017fbfb..8a21bc14ae 100644
Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png differ
diff --git a/microsite/static/logo_assets/svg/Icon_Black.svg b/microsite/static/logo_assets/svg/Icon_Black.svg
index 2eb2dda646..bcd35e6648 100644
--- a/microsite/static/logo_assets/svg/Icon_Black.svg
+++ b/microsite/static/logo_assets/svg/Icon_Black.svg
@@ -1 +1 @@
-06 Icon_Black
\ No newline at end of file
+06 Icon_Black
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Icon_Gradient.svg b/microsite/static/logo_assets/svg/Icon_Gradient.svg
index bbdb4bba27..2f361046e7 100644
--- a/microsite/static/logo_assets/svg/Icon_Gradient.svg
+++ b/microsite/static/logo_assets/svg/Icon_Gradient.svg
@@ -1 +1 @@
-07 Large Icon_Gradient
\ No newline at end of file
+07 Large Icon_Gradient
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Icon_Teal.svg b/microsite/static/logo_assets/svg/Icon_Teal.svg
index 7152749073..eaad1cf2d3 100644
--- a/microsite/static/logo_assets/svg/Icon_Teal.svg
+++ b/microsite/static/logo_assets/svg/Icon_Teal.svg
@@ -1 +1 @@
-04 Icon_Teal
\ No newline at end of file
+04 Icon_Teal
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Icon_White.svg b/microsite/static/logo_assets/svg/Icon_White.svg
index c1d6f388b4..84e3c546a5 100644
--- a/microsite/static/logo_assets/svg/Icon_White.svg
+++ b/microsite/static/logo_assets/svg/Icon_White.svg
@@ -1 +1 @@
-02 Icon_White
\ No newline at end of file
+02 Icon_White
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Logo_Black.svg b/microsite/static/logo_assets/svg/Logo_Black.svg
index b7d10e429c..b5ff591d1b 100644
--- a/microsite/static/logo_assets/svg/Logo_Black.svg
+++ b/microsite/static/logo_assets/svg/Logo_Black.svg
@@ -1 +1 @@
-05 Logo_Black
\ No newline at end of file
+05 Logo_Black
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Logo_Teal.svg b/microsite/static/logo_assets/svg/Logo_Teal.svg
index b3babaa39a..bee663d69e 100644
--- a/microsite/static/logo_assets/svg/Logo_Teal.svg
+++ b/microsite/static/logo_assets/svg/Logo_Teal.svg
@@ -1 +1 @@
-03 Logo_Teal
\ No newline at end of file
+03 Logo_Teal
\ No newline at end of file
diff --git a/microsite/static/logo_assets/svg/Logo_White.svg b/microsite/static/logo_assets/svg/Logo_White.svg
index 7b9c543951..f09a8fdc6e 100644
--- a/microsite/static/logo_assets/svg/Logo_White.svg
+++ b/microsite/static/logo_assets/svg/Logo_White.svg
@@ -1 +1 @@
-01 Logo_White
\ No newline at end of file
+01 Logo_White
\ No newline at end of file
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 84b5d50d60..40e46c3545 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -959,6 +959,11 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
+"@spotify/prettier-config@^8.0.0":
+ version "8.0.0"
+ resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2"
+ integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ==
+
"@types/cheerio@^0.22.8":
version "0.22.21"
resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3"
@@ -2228,10 +2233,10 @@ dir-glob@2.0.0:
arrify "^1.0.1"
path-type "^3.0.0"
-docusaurus@^2.0.0-alpha.64:
- version "2.0.0-alpha.64"
- resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.64.tgz#7833960e9d338403894a27b79058aa4076a676d3"
- integrity sha512-ARCx0GwAvc5qx7AHvRVZidZuoDTfaaGXzgmkU23NahU6jzO/aK2Q1bH8IKNEQ5C2JuDerQ/hHDh80N20ijk82g==
+docusaurus@^2.0.0-alpha.65:
+ version "2.0.0-alpha.65"
+ resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.65.tgz#e2b84985529deb79797aff52aa39ffbe03cd8a20"
+ integrity sha512-OEnKbXLPy3EpbQA/Dj0kakB6fg/Qni1LORTIero+5Q5G0QT5OH4FAbBQOy3L1qQH0S5Bv18zESxX9Nvclg20ag==
dependencies:
"@babel/core" "^7.9.0"
"@babel/plugin-proposal-class-properties" "^7.8.3"
@@ -5199,6 +5204,11 @@ prepend-http@^2.0.0:
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+prettier@^2.0.5:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
+ integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==
+
prismjs@^1.17.1:
version "1.21.0"
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3"
diff --git a/packages/app/package.json b/packages/app/package.json
index d194d8dd2e..e9f63a275d 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,39 +1,40 @@
{
"name": "example-app",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": true,
"bundled": true,
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-api-docs": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/plugin-circleci": "^0.1.1-alpha.23",
- "@backstage/plugin-cloudbuild": "^0.1.1-alpha.23",
- "@backstage/plugin-cost-insights": "^0.1.1-alpha.23",
- "@backstage/plugin-explore": "^0.1.1-alpha.23",
- "@backstage/plugin-gcp-projects": "^0.1.1-alpha.23",
- "@backstage/plugin-github-actions": "^0.1.1-alpha.23",
- "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.23",
- "@backstage/plugin-graphiql": "^0.1.1-alpha.23",
- "@backstage/plugin-jenkins": "^0.1.1-alpha.23",
- "@backstage/plugin-kubernetes": "^0.1.1-alpha.23",
- "@backstage/plugin-lighthouse": "^0.1.1-alpha.23",
- "@backstage/plugin-newrelic": "^0.1.1-alpha.23",
- "@backstage/plugin-register-component": "^0.1.1-alpha.23",
- "@backstage/plugin-rollbar": "^0.1.1-alpha.23",
- "@backstage/plugin-scaffolder": "^0.1.1-alpha.23",
- "@backstage/plugin-sentry": "^0.1.1-alpha.23",
- "@backstage/plugin-tech-radar": "^0.1.1-alpha.23",
- "@backstage/plugin-techdocs": "^0.1.1-alpha.23",
- "@backstage/plugin-welcome": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-api-docs": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/plugin-circleci": "^0.1.1-alpha.24",
+ "@backstage/plugin-cloudbuild": "^0.1.1-alpha.24",
+ "@backstage/plugin-cost-insights": "^0.1.1-alpha.24",
+ "@backstage/plugin-explore": "^0.1.1-alpha.24",
+ "@backstage/plugin-gcp-projects": "^0.1.1-alpha.24",
+ "@backstage/plugin-github-actions": "^0.1.1-alpha.24",
+ "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.24",
+ "@backstage/plugin-graphiql": "^0.1.1-alpha.24",
+ "@backstage/plugin-jenkins": "^0.1.1-alpha.24",
+ "@backstage/plugin-kubernetes": "^0.1.1-alpha.24",
+ "@backstage/plugin-lighthouse": "^0.1.1-alpha.24",
+ "@backstage/plugin-newrelic": "^0.1.1-alpha.24",
+ "@backstage/plugin-register-component": "^0.1.1-alpha.24",
+ "@backstage/plugin-rollbar": "^0.1.1-alpha.24",
+ "@backstage/plugin-scaffolder": "^0.1.1-alpha.24",
+ "@backstage/plugin-sentry": "^0.1.1-alpha.24",
+ "@backstage/plugin-tech-radar": "^0.1.1-alpha.24",
+ "@backstage/plugin-techdocs": "^0.1.1-alpha.24",
+ "@backstage/plugin-welcome": "^0.1.1-alpha.24",
+ "@backstage/plugin-user-settings": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
- "@roadiehq/backstage-plugin-github-pull-requests": "0.3.0",
+ "@roadiehq/backstage-plugin-github-pull-requests": "^0.4.3",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"history": "^5.0.0",
"moment": "^2.27.0",
diff --git a/packages/app/public/android-chrome-192x192.png b/packages/app/public/android-chrome-192x192.png
index 4660f988c1..eec0ae25b9 100644
Binary files a/packages/app/public/android-chrome-192x192.png and b/packages/app/public/android-chrome-192x192.png differ
diff --git a/packages/app/public/android-chrome-512x512.png b/packages/app/public/android-chrome-512x512.png
index d9b0a6ba78..ac0578133b 100644
Binary files a/packages/app/public/android-chrome-512x512.png and b/packages/app/public/android-chrome-512x512.png differ
diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png
index 57c05cfc9a..6a45afb38f 100644
Binary files a/packages/app/public/apple-touch-icon.png and b/packages/app/public/apple-touch-icon.png differ
diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg
index 351dcc8809..7f2d375cd8 100644
--- a/packages/app/public/favicon.svg
+++ b/packages/app/public/favicon.svg
@@ -1,17 +1 @@
-
-
- Backstage favicon
-
-
-
-
-
+Backstage favicon
\ No newline at end of file
diff --git a/packages/app/public/safari-pinned-tab.svg b/packages/app/public/safari-pinned-tab.svg
index 3b0f666390..0f500b3002 100644
--- a/packages/app/public/safari-pinned-tab.svg
+++ b/packages/app/public/safari-pinned-tab.svg
@@ -1,28 +1 @@
-
-
-
-
-Created by potrace 1.11, written by Peter Selinger 2001-2013
-
-
-
-
-
-
+Created by potrace 1.11, written by Peter Selinger 2001-2013
\ No newline at end of file
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 667b14366f..81d08f58a9 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -33,6 +33,7 @@ import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
+import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -81,6 +82,7 @@ const AppRoutes = () => (
path="/register-component"
element={ }
/>
+ } />
{...deprecatedAppRoutes}
);
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 45ffca7801..e947e5c91d 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -35,11 +35,10 @@ import {
SidebarDivider,
SidebarSearchField,
SidebarSpace,
- SidebarUserSettings,
- DefaultProviderSettings,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
+import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -103,7 +102,7 @@ const Root: FC<{}> = ({ children }) => (
/>
- } />
+
{children}
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 0c7dfd926e..393ffe2631 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -47,8 +47,13 @@ import {
useEntity,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
-import { Grid } from '@material-ui/core';
-import { WarningPanel } from '@backstage/core';
+import { Button, Grid } from '@material-ui/core';
+import { EmptyState } from '@backstage/core';
+import {
+ EmbeddedRouter as LighthouseRouter,
+ LastLighthouseAuditCard,
+ isPluginApplicableToEntity as isLighthouseAvailable,
+} from '@backstage/plugin-lighthouse/';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
@@ -66,10 +71,20 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => {
return ;
default:
return (
-
- No CI/CD is available for this entity. Check corresponding
- annotations!
-
+
+ Read more
+
+ }
+ />
);
}
};
@@ -105,6 +120,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
+ {isLighthouseAvailable(entity) && (
+
+
+
+ )}
);
@@ -155,6 +175,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
title="CI/CD"
element={ }
/>
+ }
+ />
{
+ const worker = setupServer();
+
+ beforeAll(() => worker.listen({ onUnhandledRequest: 'error' }));
+ afterAll(() => worker.close());
+
+ beforeEach(() => {
+ worker.use(
+ rest.get('*', (req, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ url: req.url.toString(),
+ headers: req.headers.getAllHeaders(),
+ }),
+ ),
+ ),
+ );
+ });
+ afterEach(() => worker.resetHandlers());
+
+ const createConfig = (token?: string) =>
+ new ConfigReader(
+ {
+ integrations: { azure: [{ host: 'dev.azure.com', token }] },
+ },
+ 'test-config',
+ );
+
+ it.each([
+ {
+ url:
+ 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
+ config: createConfig(),
+ response: expect.objectContaining({
+ url:
+ 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
+ }),
+ },
+ {
+ url:
+ 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
+ config: createConfig(),
+ response: expect.objectContaining({
+ url:
+ 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
+ }),
+ },
+ {
+ url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
+ config: createConfig('0123456789'),
+ response: expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: 'Basic OjAxMjM0NTY3ODk=',
+ }),
+ }),
+ },
+ {
+ url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
+ config: createConfig(undefined),
+ response: expect.objectContaining({
+ headers: expect.not.objectContaining({
+ authorization: expect.anything(),
+ }),
+ }),
+ },
+ ])('should handle happy path %#', async ({ url, config, response }) => {
+ const [{ reader }] = AzureUrlReader.factory({ config, logger });
+
+ const data = await reader.read(url);
+ const res = await JSON.parse(data.toString('utf-8'));
+ expect(res).toEqual(response);
+ });
+
+ it.each([
+ {
+ url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
+ config: createConfig(),
+ error:
+ 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
+ },
+ {
+ url: 'com/a/b/blob/master/path/to/c.yaml',
+ config: createConfig(),
+ error:
+ 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
+ },
+ {
+ url: '',
+ config: createConfig(''),
+ error:
+ "Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string",
+ },
+ ])('should handle error path %#', async ({ url, config, error }) => {
+ await expect(async () => {
+ const [{ reader }] = AzureUrlReader.factory({ config, logger });
+ await reader.read(url);
+ }).rejects.toThrow(error);
+ });
+});
diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts
new file mode 100644
index 0000000000..28fbf25eea
--- /dev/null
+++ b/packages/backend-common/src/reading/AzureUrlReader.ts
@@ -0,0 +1,165 @@
+/*
+ * 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 fetch, { RequestInit, HeadersInit, Response } from 'node-fetch';
+import { Config } from '@backstage/config';
+import { NotFoundError } from '../errors';
+import { ReaderFactory, UrlReader } from './types';
+
+type Options = {
+ // TODO: added here for future support, but we only allow dev.azure.com for now
+ host: string;
+ token?: string;
+};
+
+function readConfig(config: Config): Options[] {
+ const optionsArr = Array();
+
+ const providerConfigs =
+ config.getOptionalConfigArray('integrations.azure') ?? [];
+
+ for (const providerConfig of providerConfigs) {
+ const host = providerConfig.getOptionalString('host') ?? 'dev.azure.com';
+ const token = providerConfig.getOptionalString('token');
+
+ optionsArr.push({ host, token });
+ }
+
+ // As a convenience we always make sure there's at least an unauthenticated
+ // reader for public azure repos.
+ if (!optionsArr.some(p => p.host === 'dev.azure.com')) {
+ optionsArr.push({ host: 'dev.azure.com' });
+ }
+
+ return optionsArr;
+}
+
+export class AzureUrlReader implements UrlReader {
+ static factory: ReaderFactory = ({ config }) => {
+ return readConfig(config).map(options => {
+ const reader = new AzureUrlReader(options);
+ const predicate = (url: URL) => url.host === options.host;
+ return { reader, predicate };
+ });
+ };
+
+ constructor(private readonly options: Options) {
+ if (options.host !== 'dev.azure.com') {
+ throw Error(
+ `Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`,
+ );
+ }
+ }
+
+ async read(url: string): Promise {
+ const builtUrl = this.buildRawUrl(url);
+
+ let response: Response;
+ try {
+ response = await fetch(builtUrl.toString(), this.getRequestOptions());
+ } catch (e) {
+ throw new Error(`Unable to read ${url}, ${e}`);
+ }
+
+ // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html
+ if (response.ok && response.status !== 203) {
+ return response.buffer();
+ }
+
+ const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`;
+ if (response.status === 404) {
+ throw new NotFoundError(message);
+ }
+ throw new Error(message);
+ }
+
+ // Converts
+ // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents
+ // to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}
+ private buildRawUrl(target: string): URL {
+ try {
+ const url = new URL(target);
+
+ const [
+ empty,
+ userOrOrg,
+ project,
+ srcKeyword,
+ repoName,
+ ] = url.pathname.split('/');
+
+ const path = url.searchParams.get('path') || '';
+ const ref = url.searchParams.get('version')?.substr(2);
+
+ if (
+ url.hostname !== 'dev.azure.com' ||
+ empty !== '' ||
+ userOrOrg === '' ||
+ project === '' ||
+ srcKeyword !== '_git' ||
+ repoName === '' ||
+ path === '' ||
+ ref === ''
+ ) {
+ throw new Error('Wrong Azure Devops URL or Invalid file path');
+ }
+
+ // transform to api
+ url.pathname = [
+ empty,
+ userOrOrg,
+ project,
+ '_apis',
+ 'git',
+ 'repositories',
+ repoName,
+ 'items',
+ ].join('/');
+
+ const queryParams = [`path=${path}`];
+
+ if (ref) {
+ queryParams.push(`version=${ref}`);
+ }
+
+ url.search = queryParams.join('&');
+
+ url.protocol = 'https';
+
+ return url;
+ } catch (e) {
+ throw new Error(`Incorrect url: ${target}, ${e}`);
+ }
+ }
+
+ private getRequestOptions(): RequestInit {
+ const headers: HeadersInit = {};
+
+ if (this.options.token) {
+ headers.Authorization = `Basic ${Buffer.from(
+ `:${this.options.token}`,
+ 'utf8',
+ ).toString('base64')}`;
+ }
+
+ return { headers };
+ }
+
+ toString() {
+ const { host, token } = this.options;
+ return `azure{host=${host},authed=${Boolean(token)}}`;
+ }
+}
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
new file mode 100644
index 0000000000..c3e61fb821
--- /dev/null
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
@@ -0,0 +1,153 @@
+/*
+ * 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 { rest } from 'msw';
+import { setupServer } from 'msw/node';
+import { ConfigReader } from '@backstage/config';
+import { getVoidLogger } from '../logging';
+import { BitbucketUrlReader } from './BitbucketUrlReader';
+
+const logger = getVoidLogger();
+
+describe('BitbucketUrlReader', () => {
+ const worker = setupServer();
+
+ beforeAll(() => worker.listen({ onUnhandledRequest: 'error' }));
+ afterAll(() => worker.close());
+
+ beforeEach(() => {
+ worker.use(
+ rest.get('*', (req, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ url: req.url.toString(),
+ headers: req.headers.getAllHeaders(),
+ }),
+ ),
+ ),
+ );
+ });
+ afterEach(() => worker.resetHandlers());
+
+ const createConfig = (username?: string, appPassword?: string) =>
+ new ConfigReader(
+ {
+ integrations: {
+ bitbucket: [
+ {
+ host: 'bitbucket.org',
+ username: username,
+ appPassword: appPassword,
+ },
+ ],
+ },
+ },
+ 'test-config',
+ );
+
+ it.each([
+ {
+ url:
+ 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
+ config: createConfig(),
+ response: expect.objectContaining({
+ url:
+ 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
+ }),
+ },
+ {
+ url:
+ 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
+ config: createConfig('some-user', 'my-secret'),
+ response: expect.objectContaining({
+ headers: expect.objectContaining({
+ authorization: 'Basic c29tZS11c2VyOm15LXNlY3JldA==',
+ }),
+ }),
+ },
+ {
+ url:
+ 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
+ config: createConfig(),
+ response: expect.objectContaining({
+ headers: expect.not.objectContaining({
+ authorization: expect.anything(),
+ }),
+ }),
+ },
+ {
+ url:
+ 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
+ config: createConfig(undefined, 'only-password-provided'),
+ response: expect.objectContaining({
+ headers: expect.not.objectContaining({
+ authorization: expect.anything(),
+ }),
+ }),
+ },
+ ])('should handle happy path %#', async ({ url, config, response }) => {
+ const [{ reader }] = BitbucketUrlReader.factory({ config, logger });
+
+ const data = await reader.read(url);
+ const res = await JSON.parse(data.toString('utf-8'));
+ expect(res).toEqual(response);
+ });
+
+ it.each([
+ {
+ url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
+ config: createConfig(),
+ error:
+ 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Bitbucket URL or Invalid file path',
+ },
+ {
+ url: 'com/a/b/blob/master/path/to/c.yaml',
+ config: createConfig(),
+ error:
+ 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
+ },
+ {
+ url: '',
+ config: createConfig('', ''),
+ error:
+ "Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string",
+ },
+ {
+ url: '',
+ config: createConfig('only-user-provided', ''),
+ error:
+ "Invalid type in config for key 'integrations.bitbucket[0].appPassword' in 'test-config', got empty-string, wanted string",
+ },
+ {
+ url: '',
+ config: createConfig('', 'only-password-provided'),
+ error:
+ "Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string",
+ },
+ {
+ url: '',
+ config: createConfig('only-user-provided', undefined),
+ error:
+ "Missing required config value at 'integrations.bitbucket[0].appPassword'",
+ },
+ ])('should handle error path %#', async ({ url, config, error }) => {
+ await expect(async () => {
+ const [{ reader }] = BitbucketUrlReader.factory({ config, logger });
+ await reader.read(url);
+ }).rejects.toThrow(error);
+ });
+});
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts
new file mode 100644
index 0000000000..e2576eed47
--- /dev/null
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts
@@ -0,0 +1,162 @@
+/*
+ * 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 fetch, { RequestInit, HeadersInit, Response } from 'node-fetch';
+import { Config } from '@backstage/config';
+import { ReaderFactory, UrlReader } from './types';
+import { NotFoundError } from '../errors';
+
+type Options = {
+ // TODO: added here for future support, but we only allow bitbucket.org for now
+ host: string;
+ auth?: {
+ username: string;
+ appPassword: string;
+ };
+};
+
+function readConfig(config: Config): Options[] {
+ const optionsArr = Array();
+
+ const providerConfigs =
+ config.getOptionalConfigArray('integrations.bitbucket') ?? [];
+
+ for (const providerConfig of providerConfigs) {
+ const host = providerConfig.getOptionalString('host') ?? 'bitbucket.org';
+
+ let auth;
+ if (providerConfig.has('username')) {
+ const username = providerConfig.getString('username');
+ const appPassword = providerConfig.getString('appPassword');
+ auth = { username, appPassword };
+ }
+
+ optionsArr.push({ host, auth });
+ }
+
+ // As a convenience we always make sure there's at least an unauthenticated
+ // reader for public bitbucket repos.
+ if (!optionsArr.some(p => p.host === 'bitbucket.org')) {
+ optionsArr.push({ host: 'bitbucket.org' });
+ }
+
+ return optionsArr;
+}
+
+export class BitbucketUrlReader implements UrlReader {
+ static factory: ReaderFactory = ({ config }) => {
+ return readConfig(config).map(options => {
+ const reader = new BitbucketUrlReader(options);
+ const predicate = (url: URL) => url.host === options.host;
+ return { reader, predicate };
+ });
+ };
+
+ constructor(private readonly options: Options) {
+ if (options.host !== 'bitbucket.org') {
+ throw Error(
+ `Bitbucket integration currently only supports 'bitbucket.org', tried to use host '${options.host}'`,
+ );
+ }
+ }
+
+ async read(url: string): Promise {
+ const builtUrl = this.buildRawUrl(url);
+
+ let response: Response;
+ try {
+ response = await fetch(builtUrl.toString(), this.getRequestOptions());
+ } catch (e) {
+ throw new Error(`Unable to read ${url}, ${e}`);
+ }
+
+ if (response.ok) {
+ return response.buffer();
+ }
+
+ const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`;
+ if (response.status === 404) {
+ throw new NotFoundError(message);
+ }
+ throw new Error(message);
+ }
+
+ // Converts
+ // from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
+ // to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
+ private buildRawUrl(target: string): URL {
+ try {
+ const url = new URL(target);
+
+ const [
+ empty,
+ userOrOrg,
+ repoName,
+ srcKeyword,
+ ref,
+ ...restOfPath
+ ] = url.pathname.split('/');
+
+ if (
+ url.hostname !== 'bitbucket.org' ||
+ empty !== '' ||
+ userOrOrg === '' ||
+ repoName === '' ||
+ srcKeyword !== 'src'
+ ) {
+ throw new Error('Wrong Bitbucket URL or Invalid file path');
+ }
+
+ // transform to api
+ url.pathname = [
+ empty,
+ '2.0',
+ 'repositories',
+ userOrOrg,
+ repoName,
+ 'src',
+ ref,
+ ...restOfPath,
+ ].join('/');
+ url.hostname = 'api.bitbucket.org';
+ url.protocol = 'https';
+
+ return url;
+ } catch (e) {
+ throw new Error(`Incorrect url: ${target}, ${e}`);
+ }
+ }
+
+ private getRequestOptions(): RequestInit {
+ const headers: HeadersInit = {};
+
+ if (this.options.auth) {
+ headers.Authorization = `Basic ${Buffer.from(
+ `${this.options.auth.username}:${this.options.auth.appPassword}`,
+ 'utf8',
+ ).toString('base64')}`;
+ }
+
+ return {
+ headers,
+ };
+ }
+
+ toString() {
+ const { host, auth } = this.options;
+ return `bitbucket{host=${host},authed=${Boolean(auth)}}`;
+ }
+}
diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts
new file mode 100644
index 0000000000..ea56f2182d
--- /dev/null
+++ b/packages/backend-common/src/reading/FetchUrlReader.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fetch, { Response } from 'node-fetch';
+import { NotFoundError } from '../errors';
+import { UrlReader } from './types';
+
+/**
+ * A UrlReader that does a plain fetch of the URL.
+ */
+export class FetchUrlReader implements UrlReader {
+ async read(url: string): Promise {
+ let response: Response;
+ try {
+ response = await fetch(url);
+ } catch (e) {
+ throw new Error(`Unable to read ${url}, ${e}`);
+ }
+
+ if (response.ok) {
+ return response.buffer();
+ }
+
+ const message = `could not read ${url}, ${response.status} ${response.statusText}`;
+ if (response.status === 404) {
+ throw new NotFoundError(message);
+ }
+ throw new Error(message);
+ }
+
+ toString() {
+ return 'fetch{}';
+ }
+}
diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts
similarity index 66%
rename from plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts
rename to packages/backend-common/src/reading/GithubUrlReader.test.ts
index 8eb46db967..8df464db9f 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts
@@ -14,23 +14,21 @@
* limitations under the License.
*/
-import { getVoidLogger } from '@backstage/backend-common';
-import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
getApiRequestOptions,
getApiUrl,
getRawRequestOptions,
getRawUrl,
- GithubReaderProcessor,
+ GithubUrlReader,
ProviderConfig,
readConfig,
-} from './GithubReaderProcessor';
+} from './GithubUrlReader';
-describe('GithubReaderProcessor', () => {
+describe('GithubUrlReader', () => {
describe('getApiRequestOptions', () => {
it('sets the correct API version', () => {
- const config: ProviderConfig = { target: '', apiBaseUrl: '' };
+ const config: ProviderConfig = { host: '', apiBaseUrl: '' };
expect((getApiRequestOptions(config).headers as any).Accept).toEqual(
'application/vnd.github.v3.raw',
);
@@ -38,12 +36,12 @@ describe('GithubReaderProcessor', () => {
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
- target: '',
+ host: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
- target: '',
+ host: '',
apiBaseUrl: '',
};
expect(
@@ -58,12 +56,12 @@ describe('GithubReaderProcessor', () => {
describe('getRawRequestOptions', () => {
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
- target: '',
+ host: '',
rawBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
- target: '',
+ host: '',
rawBaseUrl: '',
};
expect(
@@ -77,13 +75,13 @@ describe('GithubReaderProcessor', () => {
describe('getApiUrl', () => {
it('rejects targets that do not look like URLs', () => {
- const config: ProviderConfig = { target: '', apiBaseUrl: '' };
+ const config: ProviderConfig = { host: '', apiBaseUrl: '' };
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
- target: 'https://github.com',
+ host: 'github.com',
apiBaseUrl: 'https://api.github.com',
};
expect(
@@ -110,7 +108,7 @@ describe('GithubReaderProcessor', () => {
it('happy path for ghe', () => {
const config: ProviderConfig = {
- target: 'https://ghe.mycompany.net',
+ host: 'ghe.mycompany.net',
apiBaseUrl: 'https://ghe.mycompany.net/api/v3',
};
expect(
@@ -128,13 +126,13 @@ describe('GithubReaderProcessor', () => {
describe('getRawUrl', () => {
it('rejects targets that do not look like URLs', () => {
- const config: ProviderConfig = { target: '', apiBaseUrl: '' };
+ const config: ProviderConfig = { host: '', apiBaseUrl: '' };
expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for github', () => {
const config: ProviderConfig = {
- target: 'https://github.com',
+ host: 'github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
};
expect(
@@ -151,7 +149,7 @@ describe('GithubReaderProcessor', () => {
it('happy path for ghe', () => {
const config: ProviderConfig = {
- target: 'https://ghe.mycompany.net',
+ host: 'ghe.mycompany.net',
rawBaseUrl: 'https://ghe.mycompany.net/raw',
};
expect(
@@ -167,23 +165,23 @@ describe('GithubReaderProcessor', () => {
describe('readConfig', () => {
function config(
- providers: { target: string; apiBaseUrl?: string; token?: string }[],
+ providers: { host: string; apiBaseUrl?: string; token?: string }[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
- catalog: { processors: { github: { providers } } },
+ integrations: { github: providers },
},
},
]);
}
it('adds a default GitHub entry when missing', () => {
- const output = readConfig(config([]), getVoidLogger());
+ const output = readConfig(config([]));
expect(output).toEqual([
{
- target: 'https://github.com',
+ host: 'github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
@@ -191,13 +189,10 @@ describe('GithubReaderProcessor', () => {
});
it('injects the correct GitHub API base URL when missing', () => {
- const output = readConfig(
- config([{ target: 'https://github.com' }]),
- getVoidLogger(),
- );
+ const output = readConfig(config([{ host: 'github.com' }]));
expect(output).toEqual([
{
- target: 'https://github.com',
+ host: 'github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
},
@@ -205,64 +200,33 @@ describe('GithubReaderProcessor', () => {
});
it('rejects custom targets with no base URLs', () => {
- expect(() =>
- readConfig(
- config([{ target: 'https://ghe.company.com' }]),
- getVoidLogger(),
- ),
- ).toThrow(
- 'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl',
+ expect(() => readConfig(config([{ host: 'ghe.company.com' }]))).toThrow(
+ "GitHub integration for 'ghe.company.com' must configure an explicit apiBaseUrl and rawBaseUrl",
);
});
it('rejects funky configs', () => {
+ expect(() => readConfig(config([{ host: 7 } as any]))).toThrow(/host/);
+ expect(() => readConfig(config([{ token: 7 } as any]))).toThrow(/token/);
expect(() =>
- readConfig(config([{ target: 7 } as any]), getVoidLogger()),
- ).toThrow(/target/);
- expect(() =>
- readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()),
- ).toThrow(/target/);
- expect(() =>
- readConfig(
- config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]),
- getVoidLogger(),
- ),
+ readConfig(config([{ host: 'github.com', apiBaseUrl: 7 } as any])),
).toThrow(/apiBaseUrl/);
expect(() =>
- readConfig(
- config([{ target: 'https://github.com', token: 7 } as any]),
- getVoidLogger(),
- ),
+ readConfig(config([{ host: 'github.com', token: 7 } as any])),
).toThrow(/token/);
});
});
describe('implementation', () => {
- it('rejects unknown types', async () => {
- const processor = new GithubReaderProcessor([
- { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
- ]);
- const location: LocationSpec = {
- type: 'not-github',
- target: 'https://github.com',
- };
- await expect(
- processor.readLocation(location, false, () => {}),
- ).resolves.toBeFalsy();
- });
-
it('rejects unknown targets', async () => {
- const processor = new GithubReaderProcessor([
- { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
- ]);
- const location: LocationSpec = {
- type: 'github',
- target: 'https://not.github.com/apa',
- };
+ const processor = new GithubUrlReader({
+ host: 'github.com',
+ apiBaseUrl: 'https://api.github.com',
+ });
await expect(
- processor.readLocation(location, false, () => {}),
+ processor.read('https://not.github.com/apa'),
).rejects.toThrow(
- /There is no GitHub provider that matches https:\/\/not.github.com\/apa/,
+ 'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path',
);
});
});
diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts
new file mode 100644
index 0000000000..e5bed6dd26
--- /dev/null
+++ b/packages/backend-common/src/reading/GithubUrlReader.ts
@@ -0,0 +1,236 @@
+/*
+ * 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 { Config } from '@backstage/config';
+import parseGitUri from 'git-url-parse';
+import fetch, { HeadersInit, RequestInit, Response } from 'node-fetch';
+import { NotFoundError } from '../errors';
+import { ReaderFactory, UrlReader } from './types';
+
+/**
+ * The configuration parameters for a single GitHub API provider.
+ */
+export type ProviderConfig = {
+ /**
+ * The host of the target that this matches on, e.g. "github.com"
+ */
+ host: string;
+
+ /**
+ * The base URL of the API of this provider, e.g. "https://api.github.com",
+ * with no trailing slash.
+ *
+ * May be omitted specifically for GitHub; then it will be deduced.
+ *
+ * The API will always be preferred if both its base URL and a token are
+ * present.
+ */
+ apiBaseUrl?: string;
+
+ /**
+ * The base URL of the raw fetch endpoint of this provider, e.g.
+ * "https://raw.githubusercontent.com", with no trailing slash.
+ *
+ * May be omitted specifically for GitHub; then it will be deduced.
+ *
+ * The API will always be preferred if both its base URL and a token are
+ * present.
+ */
+ rawBaseUrl?: string;
+
+ /**
+ * The authorization token to use for requests to this provider.
+ *
+ * If no token is specified, anonymous access is used.
+ */
+ token?: string;
+};
+
+export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
+ const headers: HeadersInit = {
+ Accept: 'application/vnd.github.v3.raw',
+ };
+
+ if (provider.token) {
+ headers.Authorization = `token ${provider.token}`;
+ }
+
+ return {
+ headers,
+ };
+}
+
+export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
+ const headers: HeadersInit = {};
+
+ if (provider.token) {
+ headers.Authorization = `token ${provider.token}`;
+ }
+
+ return {
+ headers,
+ };
+}
+
+// Converts for example
+// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
+// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
+export function getApiUrl(target: string, provider: ProviderConfig): URL {
+ try {
+ const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
+
+ if (
+ !owner ||
+ !name ||
+ !ref ||
+ (filepathtype !== 'blob' && filepathtype !== 'raw')
+ ) {
+ throw new Error('Invalid GitHub URL or file path');
+ }
+
+ const pathWithoutSlash = filepath.replace(/^\//, '');
+ return new URL(
+ `${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
+ );
+ } catch (e) {
+ throw new Error(`Incorrect URL: ${target}, ${e}`);
+ }
+}
+
+// Converts for example
+// from: https://github.com/a/b/blob/branchname/c.yaml
+// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
+export function getRawUrl(target: string, provider: ProviderConfig): URL {
+ try {
+ const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
+
+ if (
+ !owner ||
+ !name ||
+ !ref ||
+ (filepathtype !== 'blob' && filepathtype !== 'raw')
+ ) {
+ throw new Error('Invalid GitHub URL or file path');
+ }
+
+ const pathWithoutSlash = filepath.replace(/^\//, '');
+ return new URL(
+ `${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
+ );
+ } catch (e) {
+ throw new Error(`Incorrect URL: ${target}, ${e}`);
+ }
+}
+
+export function readConfig(config: Config): ProviderConfig[] {
+ const providers: ProviderConfig[] = [];
+
+ const providerConfigs =
+ config.getOptionalConfigArray('integrations.github') ?? [];
+
+ // First read all the explicit providers
+ for (const providerConfig of providerConfigs) {
+ const host = providerConfig.getOptionalString('host') ?? 'github.com';
+ let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
+ let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
+ const token = providerConfig.getOptionalString('token');
+
+ if (apiBaseUrl) {
+ apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
+ } else if (host === 'github.com') {
+ apiBaseUrl = 'https://api.github.com';
+ }
+
+ if (rawBaseUrl) {
+ rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
+ } else if (host === 'github.com') {
+ rawBaseUrl = 'https://raw.githubusercontent.com';
+ }
+
+ if (!apiBaseUrl && !rawBaseUrl) {
+ throw new Error(
+ `GitHub integration for '${host}' must configure an explicit apiBaseUrl and rawBaseUrl`,
+ );
+ }
+
+ providers.push({ host, apiBaseUrl, rawBaseUrl, token });
+ }
+
+ // If no explicit github.com provider was added, put one in the list as
+ // a convenience
+ if (!providers.some(p => p.host === 'github.com')) {
+ providers.push({
+ host: 'github.com',
+ apiBaseUrl: 'https://api.github.com',
+ rawBaseUrl: 'https://raw.githubusercontent.com',
+ });
+ }
+
+ return providers;
+}
+
+/**
+ * A processor that adds the ability to read files from GitHub v3 APIs, such as
+ * the one exposed by GitHub itself.
+ */
+export class GithubUrlReader implements UrlReader {
+ private config: ProviderConfig;
+
+ static factory: ReaderFactory = ({ config }) => {
+ return readConfig(config).map(provider => {
+ const reader = new GithubUrlReader(provider);
+ const predicate = (url: URL) => url.host === provider.host;
+ return { reader, predicate };
+ });
+ };
+
+ constructor(config: ProviderConfig) {
+ this.config = config;
+ }
+
+ async read(url: string): Promise {
+ const useApi =
+ this.config.apiBaseUrl && (this.config.token || !this.config.rawBaseUrl);
+ const ghUrl = useApi
+ ? getApiUrl(url, this.config)
+ : getRawUrl(url, this.config);
+ const options = useApi
+ ? getApiRequestOptions(this.config)
+ : getRawRequestOptions(this.config);
+
+ let response: Response;
+ try {
+ response = await fetch(ghUrl.toString(), options);
+ } catch (e) {
+ throw new Error(`Unable to read ${url}, ${e}`);
+ }
+
+ if (response.ok) {
+ return response.buffer();
+ }
+
+ const message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`;
+ if (response.status === 404) {
+ throw new NotFoundError(message);
+ }
+ throw new Error(message);
+ }
+
+ toString() {
+ const { host, token } = this.config;
+ return `github{host=${host},authed=${Boolean(token)}}`;
+ }
+}
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
new file mode 100644
index 0000000000..09da9c4e0a
--- /dev/null
+++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 { rest } from 'msw';
+import { setupServer } from 'msw/node';
+import { ConfigReader } from '@backstage/config';
+import { getVoidLogger } from '../logging';
+import { GitlabUrlReader } from './GitlabUrlReader';
+
+const logger = getVoidLogger();
+
+describe('GitlabUrlReader', () => {
+ const worker = setupServer();
+
+ beforeAll(() => worker.listen({ onUnhandledRequest: 'error' }));
+ afterAll(() => worker.close());
+
+ beforeEach(() => {
+ worker.use(
+ rest.get('*/api/v4/projects/:name', (_, res, ctx) =>
+ res(ctx.status(200), ctx.json({ id: 12345 })),
+ ),
+ rest.get('*', (req, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.json({
+ url: req.url.toString(),
+ headers: req.headers.getAllHeaders(),
+ }),
+ ),
+ ),
+ );
+ });
+ afterEach(() => worker.resetHandlers());
+
+ const createConfig = (token?: string) =>
+ new ConfigReader(
+ {
+ integrations: { gitlab: [{ host: 'gitlab.com', token }] },
+ },
+ 'test-config',
+ );
+
+ it.each([
+ // Project URLs
+ {
+ url:
+ 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
+ config: createConfig(),
+ response: expect.objectContaining({
+ url:
+ 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
+ headers: expect.objectContaining({
+ 'private-token': '',
+ }),
+ }),
+ },
+ {
+ url:
+ 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
+ config: createConfig('0123456789'),
+ response: expect.objectContaining({
+ url:
+ 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
+ headers: expect.objectContaining({
+ 'private-token': '0123456789',
+ }),
+ }),
+ },
+ {
+ url:
+ 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
+ config: createConfig(),
+ response: expect.objectContaining({
+ url:
+ 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
+ }),
+ },
+
+ // Raw URLs
+ {
+ url: 'https://gitlab.example.com/a/b/blob/master/c.yaml',
+ config: createConfig(),
+ response: expect.objectContaining({
+ url: 'https://gitlab.example.com/a/b/raw/master/c.yaml',
+ }),
+ },
+ ])('should handle happy path %#', async ({ url, config, response }) => {
+ const [{ reader }] = GitlabUrlReader.factory({ config, logger });
+
+ const data = await reader.read(url);
+ const res = await JSON.parse(data.toString('utf-8'));
+ expect(res).toEqual(response);
+ });
+
+ it.each([
+ {
+ url: '',
+ config: createConfig(''),
+ error:
+ "Invalid type in config for key 'integrations.gitlab[0].token' in 'test-config', got empty-string, wanted string",
+ },
+ ])('should handle error path %#', async ({ url, config, error }) => {
+ await expect(async () => {
+ const [{ reader }] = GitlabUrlReader.factory({ config, logger });
+ await reader.read(url);
+ }).rejects.toThrow(error);
+ });
+});
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts
new file mode 100644
index 0000000000..0e430d650c
--- /dev/null
+++ b/packages/backend-common/src/reading/GitlabUrlReader.ts
@@ -0,0 +1,197 @@
+/*
+ * 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 fetch, { RequestInit, Response } from 'node-fetch';
+import { Config } from '@backstage/config';
+import { NotFoundError } from '../errors';
+import { ReaderFactory, UrlReader } from './types';
+
+type Options = {
+ host: string;
+ token?: string;
+};
+
+function readConfig(config: Config): Options[] {
+ const optionsArr = Array();
+
+ const providerConfigs =
+ config.getOptionalConfigArray('integrations.gitlab') ?? [];
+
+ for (const providerConfig of providerConfigs) {
+ const host = providerConfig.getOptionalString('host') ?? 'gitlab.com';
+ const token = providerConfig.getOptionalString('token');
+
+ optionsArr.push({ host, token });
+ }
+
+ // As a convenience we always make sure there's at least an unauthenticated
+ // reader for public gitlab repos.
+ if (!optionsArr.some(p => p.host === 'gitlab.com')) {
+ optionsArr.push({ host: 'gitlab.com' });
+ }
+
+ return optionsArr;
+}
+
+export class GitlabUrlReader implements UrlReader {
+ static factory: ReaderFactory = ({ config }) => {
+ return readConfig(config).map(options => {
+ const reader = new GitlabUrlReader(options);
+ const predicate = (url: URL) => url.host === options.host;
+ return { reader, predicate };
+ });
+ };
+
+ constructor(private readonly options: Options) {}
+
+ async read(url: string): Promise {
+ // TODO(Rugvip): merged the old GitlabReaderProcessor in here and used
+ // the existence of /~/blob/ to switch the logic. Don't know if this
+ // makes sense and it might require some more work.
+ let builtUrl: URL;
+ if (url.includes('/-/blob/')) {
+ const projectID = await this.getProjectID(url);
+ builtUrl = this.buildProjectUrl(url, projectID);
+ } else {
+ builtUrl = this.buildRawUrl(url);
+ }
+
+ let response: Response;
+ try {
+ response = await fetch(builtUrl.toString(), this.getRequestOptions());
+ } catch (e) {
+ throw new Error(`Unable to read ${url}, ${e}`);
+ }
+
+ if (response.ok) {
+ return response.buffer();
+ }
+
+ const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`;
+ if (response.status === 404) {
+ throw new NotFoundError(message);
+ }
+ throw new Error(message);
+ }
+
+ // Converts
+ // from: https://gitlab.example.com/a/b/blob/master/c.yaml
+ // to: https://gitlab.example.com/a/b/raw/master/c.yaml
+ private buildRawUrl(target: string): URL {
+ try {
+ const url = new URL(target);
+
+ const [
+ empty,
+ userOrOrg,
+ repoName,
+ blobKeyword,
+ ...restOfPath
+ ] = url.pathname.split('/');
+
+ if (
+ empty !== '' ||
+ userOrOrg === '' ||
+ repoName === '' ||
+ blobKeyword !== 'blob' ||
+ !restOfPath.join('/').match(/\.yaml$/)
+ ) {
+ throw new Error('Wrong GitLab URL');
+ }
+
+ // Replace 'blob' with 'raw'
+ url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join(
+ '/',
+ );
+
+ return url;
+ } catch (e) {
+ throw new Error(`Incorrect url: ${target}, ${e}`);
+ }
+ }
+
+ // convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
+ // to https://gitlab.com/api/v4/projects//repository/files/filepath?ref=branch
+ private buildProjectUrl(target: string, projectID: Number): URL {
+ try {
+ const url = new URL(target);
+
+ const branchAndfilePath = url.pathname.split('/-/blob/')[1];
+
+ const [branch, ...filePath] = branchAndfilePath.split('/');
+
+ url.pathname = [
+ '/api/v4/projects',
+ projectID,
+ 'repository/files',
+ encodeURIComponent(filePath.join('/')),
+ 'raw',
+ ].join('/');
+ url.search = `?ref=${branch}`;
+
+ return url;
+ } catch (e) {
+ throw new Error(`Incorrect url: ${target}, ${e}`);
+ }
+ }
+
+ private async getProjectID(target: string): Promise {
+ const url = new URL(target);
+
+ if (
+ // absPaths to gitlab files should contain /-/blob
+ // ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
+ !url.pathname.match(/\/\-\/blob\//)
+ ) {
+ throw new Error('Please provide full path to yaml file from Gitlab');
+ }
+ try {
+ const repo = url.pathname.split('/-/blob/')[0];
+
+ // Find ProjectID from url
+ // convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath'
+ // to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo'
+ const repoIDLookup = new URL(
+ `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent(
+ repo.replace(/^\//, ''),
+ )}`,
+ );
+ const response = await fetch(
+ repoIDLookup.toString(),
+ this.getRequestOptions(),
+ );
+ const projectIDJson = await response.json();
+ const projectID: Number = projectIDJson.id;
+
+ return projectID;
+ } catch (e) {
+ throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`);
+ }
+ }
+
+ private getRequestOptions(): RequestInit {
+ return {
+ headers: {
+ ['PRIVATE-TOKEN']: this.options.token ?? '',
+ },
+ };
+ }
+
+ toString() {
+ const { host, token } = this.options;
+ return `gitlab{host=${host},authed=${Boolean(token)}}`;
+ }
+}
diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts
new file mode 100644
index 0000000000..7654cc8aac
--- /dev/null
+++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { UrlReader, UrlReaderPredicateTuple } from './types';
+
+type Options = {
+ // UrlReader to fall back to if no other reader is matched
+ fallback?: UrlReader;
+};
+
+/**
+ * A UrlReader implementation that selects from a set of UrlReaders
+ * based on a predicate tied to each reader.
+ */
+export class UrlReaderPredicateMux implements UrlReader {
+ private readonly readers: UrlReaderPredicateTuple[] = [];
+ private readonly fallback?: UrlReader;
+
+ constructor({ fallback }: Options) {
+ this.fallback = fallback;
+ }
+
+ register(tuple: UrlReaderPredicateTuple): void {
+ this.readers.push(tuple);
+ }
+
+ read(url: string): Promise {
+ const parsed = new URL(url);
+
+ for (const { predicate, reader } of this.readers) {
+ if (predicate(parsed)) {
+ return reader.read(url);
+ }
+ }
+
+ if (this.fallback) {
+ return this.fallback.read(url);
+ }
+
+ throw new Error(`No reader found that could handle '${url}'`);
+ }
+
+ toString() {
+ return `predicateMux{readers=${this.readers
+ .map(t => t.reader)
+ .join(',')},fallback=${this.fallback}}`;
+ }
+}
diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts
new file mode 100644
index 0000000000..e1d99a2c49
--- /dev/null
+++ b/packages/backend-common/src/reading/UrlReaders.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 { Logger } from 'winston';
+import { Config } from '@backstage/config';
+import { ReaderFactory, UrlReader } from './types';
+import { UrlReaderPredicateMux } from './UrlReaderPredicateMux';
+import { AzureUrlReader } from './AzureUrlReader';
+import { BitbucketUrlReader } from './BitbucketUrlReader';
+import { GithubUrlReader } from './GithubUrlReader';
+import { GitlabUrlReader } from './GitlabUrlReader';
+import { FetchUrlReader } from './FetchUrlReader';
+
+type CreateOptions = {
+ /** Root config object */
+ config: Config;
+ /** Logger used by all the readers */
+ logger: Logger;
+ /** A list of factories used to construct individual readers that match on URLs */
+ factories?: ReaderFactory[];
+ /** Fallback reader to use if none of the readers created by the factories match */
+ fallback?: UrlReader;
+};
+
+/**
+ * UrlReaders provide various utilities related to the UrlReader interface.
+ */
+export class UrlReaders {
+ /**
+ * Creates a UrlReader without any known types.
+ */
+ static create({
+ logger,
+ config,
+ factories,
+ fallback,
+ }: CreateOptions): UrlReader {
+ const mux = new UrlReaderPredicateMux({ fallback: fallback });
+
+ for (const factory of factories ?? []) {
+ const tuples = factory({ config, logger: logger });
+
+ for (const tuple of tuples) {
+ mux.register(tuple);
+ }
+ }
+
+ return mux;
+ }
+
+ /**
+ * Creates a UrlReader that includes all the default factories from this package.
+ *
+ * Any additional factories passed will be loaded before the default ones.
+ *
+ * If no fallback reader is passed, a plain fetch reader will be used.
+ */
+ static default({ logger, config, factories = [], fallback }: CreateOptions) {
+ return UrlReaders.create({
+ logger,
+ config,
+ factories: factories.concat([
+ AzureUrlReader.factory,
+ BitbucketUrlReader.factory,
+ GithubUrlReader.factory,
+ GitlabUrlReader.factory,
+ ]),
+ fallback: fallback ?? new FetchUrlReader(),
+ });
+ }
+}
diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/packages/backend-common/src/reading/index.ts
similarity index 62%
rename from packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx
rename to packages/backend-common/src/reading/index.ts
index 5dad178ccd..8ecc08ebca 100644
--- a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx
+++ b/packages/backend-common/src/reading/index.ts
@@ -13,14 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React from 'react';
-import { List, ListSubheader } from '@material-ui/core';
-import { SidebarThemeToggle } from './ThemeToggle';
-import { SidebarPinButton } from './PinButton';
-export const AppSettingsList = () => (
- App Settings}>
-
-
-
-);
+export type { UrlReader } from './types';
+export { UrlReaders } from './UrlReaders';
+export { AzureUrlReader } from './AzureUrlReader';
+export { BitbucketUrlReader } from './BitbucketUrlReader';
+export { GithubUrlReader } from './GithubUrlReader';
+export { GitlabUrlReader } from './GitlabUrlReader';
diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts
new file mode 100644
index 0000000000..e423db3eca
--- /dev/null
+++ b/packages/backend-common/src/reading/types.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { Logger } from 'winston';
+import { Config } from '@backstage/config';
+
+/**
+ * A generic interface for fetching plain data from URLs.
+ */
+export type UrlReader = {
+ read(url: string): Promise;
+};
+
+export type UrlReaderPredicateTuple = {
+ predicate: (url: URL) => boolean;
+ reader: UrlReader;
+};
+
+/**
+ * A factory function that can read config to construct zero or more
+ * UrlReaders along with a predicate for when it should be used.
+ */
+export type ReaderFactory = (options: {
+ config: Config;
+ logger: Logger;
+}) => UrlReaderPredicateTuple[];
diff --git a/packages/backend-common/src/setupTests.ts b/packages/backend-common/src/setupTests.ts
index f7b6ca962d..ba33cf996b 100644
--- a/packages/backend-common/src/setupTests.ts
+++ b/packages/backend-common/src/setupTests.ts
@@ -14,6 +14,4 @@
* limitations under the License.
*/
-require('jest-fetch-mock').enableMocks();
-
export {};
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 0941af248a..c3a912c93e 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,24 +18,24 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/plugin-app-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-auth-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-graphql-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-proxy-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-sentry-backend": "^0.1.1-alpha.23",
- "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/plugin-app-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-auth-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-graphql-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-proxy-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-sentry-backend": "^0.1.1-alpha.24",
+ "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.24",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.0",
- "example-app": "^0.1.1-alpha.23",
+ "example-app": "^0.1.1-alpha.24",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"knex": "^0.21.1",
@@ -45,7 +45,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index e86034dd29..959af71427 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -32,6 +32,7 @@ import {
useHotMemoize,
notFoundHandler,
SingleHostDiscovery,
+ UrlReaders,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
@@ -49,9 +50,14 @@ import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
const config = ConfigReader.fromConfigs(loadedConfigs);
+ const root = getRootLogger();
+ const reader = UrlReaders.default({ logger: root, config });
+ const discovery = SingleHostDiscovery.fromConfig(config);
+
+ root.info(`Created UrlReader ${reader}`);
return (plugin: string): PluginEnvironment => {
- const logger = getRootLogger().child({ type: 'plugin', plugin });
+ const logger = root.child({ type: 'plugin', plugin });
const database = createDatabaseClient(
config.getConfig('backend.database'),
{
@@ -60,8 +66,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
},
},
);
- const discovery = SingleHostDiscovery.fromConfig(config);
- return { logger, database, config, discovery };
+ return { logger, database, config, reader, discovery };
};
}
diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts
index 219b04a0f7..c1d390f8cc 100644
--- a/packages/backend/src/plugins/catalog.ts
+++ b/packages/backend/src/plugins/catalog.ts
@@ -28,10 +28,11 @@ import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
- database,
config,
+ reader,
+ database,
}: PluginEnvironment) {
- const locationReader = new LocationReaders({ logger, config });
+ const locationReader = new LocationReaders({ logger, reader, config });
const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts
index c5b36ab8da..90667bfb16 100644
--- a/packages/backend/src/plugins/scaffolder.ts
+++ b/packages/backend/src/plugins/scaffolder.ts
@@ -47,13 +47,12 @@ export default async function createPlugin({
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
- const githubPreparer = new GithubPreparer();
+
const gitlabPreparer = new GitlabPreparer(config);
const azurePreparer = new AzurePreparer(config);
const preparers = new Preparers();
preparers.register('file', filePreparer);
- preparers.register('github', githubPreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
preparers.register('azure/api', azurePreparer);
@@ -75,6 +74,10 @@ export default async function createPlugin({
token: githubToken,
repoVisibility,
});
+
+ const githubPreparer = new GithubPreparer({ token: githubToken });
+
+ preparers.register('github', githubPreparer);
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
} catch (e) {
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 3709fc8d9a..9257fcc9cf 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -17,11 +17,12 @@
import Knex from 'knex';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
-import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { PluginEndpointDiscovery, UrlReader } from '@backstage/backend-common';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
config: Config;
+ reader: UrlReader;
discovery: PluginEndpointDiscovery;
};
diff --git a/packages/catalog-model/examples/spotify-api.yaml b/packages/catalog-model/examples/spotify-api.yaml
index 30524dfdd4..14deb17abe 100644
--- a/packages/catalog-model/examples/spotify-api.yaml
+++ b/packages/catalog-model/examples/spotify-api.yaml
@@ -7,8 +7,11 @@ metadata:
- spotify
- rest
annotations:
+ # The annotation is deprecated, we use placeholders (see below) instead, remove it later.
backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml'
spec:
type: openapi
lifecycle: production
owner: spotify@example.com
+ definition:
+ $text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/spotify.com/v1/swagger.yaml
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index 96433a0f0e..6a543f7f84 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"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.23",
+ "@backstage/config": "^0.1.1-alpha.24",
"@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.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts
index d9787fe675..bd7cc9477d 100644
--- a/packages/catalog-model/src/entity/ref.test.ts
+++ b/packages/catalog-model/src/entity/ref.test.ts
@@ -15,6 +15,7 @@
*/
import { ENTITY_DEFAULT_NAMESPACE } from './constants';
+import { Entity } from './Entity';
import { parseEntityName, parseEntityRef, serializeEntityRef } from './ref';
describe('ref', () => {
@@ -333,6 +334,26 @@ describe('ref', () => {
expect(serializeEntityRef({ name: 'c' })).toEqual('c');
});
+ it('handles entities', () => {
+ const entityWithNamespace: Entity = {
+ apiVersion: 'a',
+ kind: 'b',
+ metadata: {
+ name: 'c',
+ namespace: 'd',
+ },
+ };
+ const entityWithoutNamespace: Entity = {
+ apiVersion: 'a',
+ kind: 'b',
+ metadata: {
+ name: 'c',
+ },
+ };
+ expect(serializeEntityRef(entityWithNamespace)).toEqual('b:d/c');
+ expect(serializeEntityRef(entityWithoutNamespace)).toEqual('b:c');
+ });
+
it('picks the least complex form', () => {
expect(
serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }),
diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts
index c4c86b176c..2cacfce53a 100644
--- a/packages/catalog-model/src/entity/ref.ts
+++ b/packages/catalog-model/src/entity/ref.ts
@@ -160,12 +160,29 @@ export function parseEntityRef(
* @param ref The reference to serialize
* @returns The same reference on either string or compound form
*/
-export function serializeEntityRef(ref: {
- kind?: string;
- namespace?: string;
- name: string;
-}): EntityRef {
- const { kind, namespace, name } = ref;
+export function serializeEntityRef(
+ ref:
+ | Entity
+ | {
+ kind?: string;
+ namespace?: string;
+ name: string;
+ },
+): EntityRef {
+ let kind;
+ let namespace;
+ let name;
+
+ if ('metadata' in ref) {
+ kind = ref.kind;
+ namespace = ref.metadata.namespace;
+ name = ref.metadata.name;
+ } else {
+ kind = ref.kind;
+ namespace = ref.namespace;
+ name = ref.name;
+ }
+
if (
kind?.includes(':') ||
kind?.includes('/') ||
diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts
index 200e90b406..457ef64611 100644
--- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts
+++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts
@@ -161,18 +161,4 @@ describe('CommonValidatorFunctions', () => {
])(`isValidDnsLabel %p ? %p`, (value, result) => {
expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result);
});
-
- it.each([
- ['', ''],
- ['a', 'a'],
- ['a-b', 'ab'],
- ['-a-b', 'ab'],
- ['a_b', 'ab'],
- [`${'a'.repeat(6000)}`, `${'a'.repeat(6000)}`],
- ['_:;>!"#€', ''],
- ])(`normalizeToLowercaseAlphanum %p ? %p`, (value, result) => {
- expect(CommonValidatorFunctions.normalizeToLowercaseAlphanum(value)).toBe(
- result,
- );
- });
});
diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts
index 96a91aca06..349d6d7f72 100644
--- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts
+++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts
@@ -92,17 +92,4 @@ export class CommonValidatorFunctions {
/^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value)
);
}
-
- /**
- * Normalizes by keeping only a-z, A-Z, and 0-9; and converts to lowercase.
- *
- * @param value The value to normalize
- */
- static normalizeToLowercaseAlphanum(value: string): string {
- return value
- .split('')
- .filter(x => /[a-zA-Z0-9]/.test(x))
- .join('')
- .toLowerCase();
- }
}
diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts
index 3602c01a63..63341f5c33 100644
--- a/packages/catalog-model/src/validation/makeValidator.ts
+++ b/packages/catalog-model/src/validation/makeValidator.ts
@@ -23,7 +23,6 @@ const defaultValidators: Validators = {
isValidKind: KubernetesValidatorFunctions.isValidKind,
isValidEntityName: KubernetesValidatorFunctions.isValidObjectName,
isValidNamespace: KubernetesValidatorFunctions.isValidNamespace,
- normalizeEntityName: CommonValidatorFunctions.normalizeToLowercaseAlphanum,
isValidLabelKey: KubernetesValidatorFunctions.isValidLabelKey,
isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue,
isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey,
diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts
index ff00036991..14706485ca 100644
--- a/packages/catalog-model/src/validation/types.ts
+++ b/packages/catalog-model/src/validation/types.ts
@@ -19,7 +19,6 @@ export type Validators = {
isValidKind(value: any): boolean;
isValidEntityName(value: any): boolean;
isValidNamespace(value: any): boolean;
- normalizeEntityName(value: string): string;
isValidLabelKey(value: any): boolean;
isValidLabelValue(value: any): boolean;
isValidAnnotationKey(value: any): boolean;
diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json
index bbd9a67387..abb4b5f9a1 100644
--- a/packages/cli-common/package.json
+++ b/packages/cli-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index dc2ac1c5f7..9bbbe9f29a 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,9 +28,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
- "@backstage/cli-common": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/config-loader": "^0.1.1-alpha.23",
+ "@backstage/cli-common": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/config-loader": "^0.1.1-alpha.24",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
@@ -91,7 +91,7 @@
"typescript": "^3.9.3",
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
- "webpack-dev-server": "^3.10.3",
+ "webpack-dev-server": "^3.11.0",
"webpack-node-externals": "^2.5.0",
"yaml": "^1.10.0",
"yml-loader": "^2.1.0",
@@ -112,7 +112,7 @@
"@types/rollup-plugin-postcss": "^2.0.0",
"@types/tar": "^4.0.3",
"@types/webpack": "^4.41.7",
- "@types/webpack-dev-server": "^3.10.0",
+ "@types/webpack-dev-server": "^3.11.0",
"del": "^5.1.0",
"mock-fs": "^4.13.0",
"nodemon": "^2.0.2",
diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts
index 2566460494..2e7847fdb1 100644
--- a/packages/cli/src/lib/builder/config.ts
+++ b/packages/cli/src/lib/builder/config.ts
@@ -64,6 +64,7 @@ export const makeConfigs = async (
entryFileNames: 'index.cjs.js',
chunkFileNames: 'cjs/[name]-[hash].js',
format: 'commonjs',
+ sourcemap: true,
});
}
if (options.outputs.has(Output.esm)) {
@@ -72,6 +73,7 @@ export const makeConfigs = async (
entryFileNames: 'index.esm.js',
chunkFileNames: 'esm/[name]-[hash].js',
format: 'module',
+ sourcemap: true,
});
// Assume we're building for the browser if ESM output is included
mainFields.unshift('browser');
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 09f8da3b70..6a17eba76c 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/config": "^0.1.1-alpha.24",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
diff --git a/packages/config-loader/src/lib/reader.test.ts b/packages/config-loader/src/lib/reader.test.ts
index 238a556061..6ccceef60d 100644
--- a/packages/config-loader/src/lib/reader.test.ts
+++ b/packages/config-loader/src/lib/reader.test.ts
@@ -84,6 +84,47 @@ describe('readConfigFile', () => {
});
it('should read secrets', async () => {
+ const readFile = memoryFiles({
+ './app-config.yaml': 'app: { $file: "./my-secret" }',
+ });
+ const readSecret = jest.fn().mockResolvedValue('secret');
+
+ const config = readConfigFile('./app-config.yaml', {
+ ...mockContext,
+ readFile,
+ readSecret: readSecret as ReadSecretFunc,
+ });
+
+ await expect(config).resolves.toEqual({
+ data: {
+ app: 'secret',
+ },
+ context: 'app-config.yaml',
+ });
+ expect(readSecret).toHaveBeenCalledWith('.app', {
+ file: './my-secret',
+ });
+ });
+
+ it('should not allow keys adjacent to secrets', async () => {
+ const readFile = memoryFiles({
+ './app-config.yaml': 'app: { extraKey: 3, $file: "./my-secret" }',
+ });
+ const readSecret = jest.fn().mockResolvedValue('secret');
+
+ const config = readConfigFile('./app-config.yaml', {
+ ...mockContext,
+ readFile,
+ readSecret: readSecret as ReadSecretFunc,
+ });
+
+ await expect(config).rejects.toThrow(
+ "Secret key '$file' has adjacent keys at .app",
+ );
+ expect(readSecret).not.toHaveBeenCalled();
+ });
+
+ it('should read deprecated secrets', async () => {
const readFile = memoryFiles({
'./app-config.yaml': 'app: { $secret: { file: "./my-secret" } }',
});
@@ -106,7 +147,7 @@ describe('readConfigFile', () => {
});
});
- it('should require secrets to be objects', async () => {
+ it('should require deprecated secrets to be objects', async () => {
const readFile = memoryFiles({
'./app-config.yaml': 'app: { $secret: ["wrong-type"] }',
});
diff --git a/packages/config-loader/src/lib/reader.ts b/packages/config-loader/src/lib/reader.ts
index e2c9bebdf3..8eadae0fe0 100644
--- a/packages/config-loader/src/lib/reader.ts
+++ b/packages/config-loader/src/lib/reader.ts
@@ -31,6 +31,8 @@ export async function readConfigFile(
const configYaml = await ctx.readFile(filePath);
const config = yaml.parse(configYaml);
+ const context = basename(filePath);
+
async function transform(
obj: JsonValue,
path: string,
@@ -56,7 +58,11 @@ export async function readConfigFile(
return arr;
}
+ // TODO(Rugvip): This form of declaring secrets is deprecated, warn and remove in the future
if ('$secret' in obj) {
+ console.warn(
+ `Deprecated secret declaration at '${path}' in '${context}', use $env, $file, etc. instead`,
+ );
if (!isObject(obj.$secret)) {
throw TypeError(`Expected object at secret ${path}.$secret`);
}
@@ -68,6 +74,24 @@ export async function readConfigFile(
}
}
+ // Check if there's any key that starts with a '$', in that case we treat
+ // this entire object as a secret.
+ const [secretKey] = Object.keys(obj).filter(key => key.startsWith('$'));
+ if (secretKey) {
+ if (Object.keys(obj).length !== 1) {
+ throw new Error(
+ `Secret key '${secretKey}' has adjacent keys at ${path}`,
+ );
+ }
+ try {
+ return await ctx.readSecret(path, {
+ [secretKey.slice(1)]: obj[secretKey],
+ });
+ } catch (error) {
+ throw new Error(`Invalid secret at ${path}: ${error.message}`);
+ }
+ }
+
const out: JsonObject = {};
for (const [key, value] of Object.entries(obj)) {
@@ -87,5 +111,5 @@ export async function readConfigFile(
if (!isObject(finalConfig)) {
throw new TypeError('Expected object at config root');
}
- return { data: finalConfig, context: basename(filePath) };
+ return { data: finalConfig, context };
}
diff --git a/packages/config-loader/src/lib/secrets.test.ts b/packages/config-loader/src/lib/secrets.test.ts
index fd95e527af..cfc4150d68 100644
--- a/packages/config-loader/src/lib/secrets.test.ts
+++ b/packages/config-loader/src/lib/secrets.test.ts
@@ -56,21 +56,33 @@ describe('readSecret', () => {
});
it('should read data secrets', async () => {
+ // Deprecated object form
await expect(
readSecret({ data: 'my-data.json', path: 'a.b.c' }, ctx),
).resolves.toBe('42');
-
await expect(
readSecret({ data: 'my-data.yaml', path: 'some.yaml.key' }, ctx),
).resolves.toBe('7');
-
await expect(
readSecret({ data: 'my-data.yml', path: 'different.key' }, ctx),
).resolves.toBe('hello');
-
await expect(
readSecret({ data: 'no-data.yml', path: 'different.key' }, ctx),
).rejects.toThrow('File not found!');
+
+ // New format with path in fragment
+ await expect(readSecret({ data: 'my-data.json#a.b.c' }, ctx)).resolves.toBe(
+ '42',
+ );
+ await expect(
+ readSecret({ data: 'my-data.yaml#some.yaml.key' }, ctx),
+ ).resolves.toBe('7');
+ await expect(
+ readSecret({ data: 'my-data.yml#different.key' }, ctx),
+ ).resolves.toBe('hello');
+ await expect(
+ readSecret({ data: 'no-data.yml#different.key' }, ctx),
+ ).rejects.toThrow('File not found!');
});
it('should reject invalid secrets', async () => {
@@ -84,7 +96,7 @@ describe('readSecret', () => {
"Secret must contain one of 'file', 'env', 'data'",
);
await expect(readSecret({ data: 'no-data.yml' }, ctx)).rejects.toThrow(
- 'path is a required field',
+ "Invalid format for data secret value, must be of the form #, got 'no-data.yml'",
);
await expect(
readSecret({ data: 'no-parser.js', path: '.' }, ctx),
diff --git a/packages/config-loader/src/lib/secrets.ts b/packages/config-loader/src/lib/secrets.ts
index 348a41db58..4c3812d527 100644
--- a/packages/config-loader/src/lib/secrets.ts
+++ b/packages/config-loader/src/lib/secrets.ts
@@ -38,9 +38,8 @@ type EnvSecret = {
type DataSecret = {
// Path to the data secret file, relative to the config file.
data: string;
- // The path to the value inside the data file.
- // Either a '.' separated list, or an array of path segments.
- path: string | string[];
+ // The path to the value inside the data file, each element separated by '.'.
+ path?: string;
};
type Secret = FileSecret | EnvSecret | DataSecret;
@@ -55,12 +54,6 @@ const secretLoaderSchemas = {
}),
data: yup.object({
data: yup.string().required(),
- path: yup.lazy(value => {
- if (typeof value === 'string') {
- return yup.string().required();
- }
- return yup.array().of(yup.string().required()).required();
- }),
}),
};
@@ -111,24 +104,30 @@ export async function readSecret(
return ctx.env[secret.env];
}
if ('data' in secret) {
- const ext = extname(secret.data);
+ const url =
+ 'path' in secret ? `${secret.data}#${secret.path}` : secret.data;
+ const [filePath, dataPath] = url.split(/#(.*)/);
+ if (!dataPath) {
+ throw new Error(
+ `Invalid format for data secret value, must be of the form #, got '${url}'`,
+ );
+ }
+
+ const ext = extname(filePath);
const parser = dataSecretParser[ext];
if (!parser) {
throw new Error(`No data secret parser available for extension ${ext}`);
}
- const content = await ctx.readFile(secret.data);
+ const content = await ctx.readFile(filePath);
- const { path } = secret;
- const parts = typeof path === 'string' ? path.split('.') : path;
+ const parts = dataPath.split('.');
let value: JsonValue | undefined = await parser(content);
for (const [index, part] of parts.entries()) {
if (!isObject(value)) {
const errPath = parts.slice(0, index).join('.');
- throw new Error(
- `Value is not an object at ${errPath} in ${secret.data}`,
- );
+ throw new Error(`Value is not an object at ${errPath} in ${filePath}`);
}
value = value[part];
}
diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts
index a7547ee8fb..087e2309c0 100644
--- a/packages/config-loader/src/loader.test.ts
+++ b/packages/config-loader/src/loader.test.ts
@@ -24,8 +24,7 @@ describe('loadConfig', () => {
app:
title: Example App
sessionKey:
- $secret:
- file: secrets/session-key.txt
+ $file: secrets/session-key.txt
`,
'/root/app-config.development.yaml': `
app:
diff --git a/packages/config/package.json b/packages/config/package.json
index 58c86d6e96..d3695ebf33 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
diff --git a/packages/core-api/package.json b/packages/core-api/package.json
index 7d338c944d..c925471695 100644
--- a/packages/core-api/package.json
+++ b/packages/core-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@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.23",
- "@backstage/test-utils-core": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/test-utils-core": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts
index de70adabd9..3f580450f0 100644
--- a/packages/core-api/src/apis/definitions/AppThemeApi.ts
+++ b/packages/core-api/src/apis/definitions/AppThemeApi.ts
@@ -17,6 +17,7 @@
import { createApiRef } from '../ApiRef';
import { BackstageTheme } from '@backstage/theme';
import { Observable } from '../../types';
+import { SvgIconProps } from '@material-ui/core';
/**
* Describes a theme provided by the app.
@@ -41,6 +42,11 @@ export type AppTheme = {
* The specialized MaterialUI theme instance.
*/
theme: BackstageTheme;
+
+ /**
+ * An Icon for the theme mode setting.
+ */
+ icon?: React.ReactElement;
};
/**
diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts
index b6aac75620..dd9657d7d8 100644
--- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts
+++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts
@@ -15,11 +15,8 @@
*/
import { createApiRef } from '../ApiRef';
-import {
- UserFlags,
- FeatureFlagsRegistry,
- FeatureFlagsRegistryItem,
-} from '../../app/FeatureFlags';
+import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags';
+import { FeatureFlagName } from '../../plugin';
/**
* The feature flags API is used to toggle functionality to users across plugins and Backstage.
@@ -55,6 +52,11 @@ export interface FeatureFlagsApi {
getRegisteredFlags(): FeatureFlagsRegistry;
}
+export interface FeatureFlagsRegistryItem {
+ pluginId: string;
+ name: FeatureFlagName;
+}
+
export const featureFlagsApiRef = createApiRef({
id: 'core.featureflags',
description: 'Used to toggle functionality in features across Backstage',
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 9dd2fb138f..c222c3c420 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -30,12 +30,12 @@ import {
SignInPageProps,
} from './types';
import { BackstagePlugin } from '../plugin';
-import { FeatureFlagsRegistryItem } from './FeatureFlags';
import {
featureFlagsApiRef,
AppThemeApi,
ConfigApi,
identityApiRef,
+ FeatureFlagsRegistryItem,
} from '../apis/definitions';
import { AppThemeProvider } from './AppThemeProvider';
diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx
index 3db2a18a02..7a69fa5456 100644
--- a/packages/core-api/src/app/FeatureFlags.tsx
+++ b/packages/core-api/src/app/FeatureFlags.tsx
@@ -15,7 +15,11 @@
*/
import { FeatureFlagName } from '../plugin/types';
-import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions';
+import {
+ FeatureFlagState,
+ FeatureFlagsApi,
+ FeatureFlagsRegistryItem,
+} from '../apis/definitions';
/**
* Helper method for validating compatibility and flag name.
@@ -129,10 +133,6 @@ export class UserFlags extends Map {
* This acts as a holding data structure for feature flags
* that plugins wish to register for use in Backstage.
*/
-export interface FeatureFlagsRegistryItem {
- pluginId: string;
- name: FeatureFlagName;
-}
export class FeatureFlagsRegistry extends Array {
static from(entries: FeatureFlagsRegistryItem[]) {
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
index 882ecd3d8b..8a1eb92e8e 100644
--- a/packages/core-api/src/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -112,11 +112,13 @@ export type AppOptions = {
* title: 'Light Theme',
* variant: 'light',
* theme: lightTheme,
+ * icon: ,
* }, {
* id: 'dark',
* title: 'Dark Theme',
* variant: 'dark',
* theme: darkTheme,
+ * icon: ,
* }]
* ```
*/
diff --git a/packages/core/package.json b/packages/core/package.json
index 8749a37b92..4890552c99 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/core-api": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/core-api": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -39,8 +39,9 @@
"@types/react-sparklines": "^1.7.0",
"classnames": "^2.2.6",
"clsx": "^1.1.0",
+ "immer": "^7.0.9",
"lodash": "^4.17.15",
- "material-table": "1.68.0",
+ "material-table": "^1.69.1",
"prop-types": "^15.7.2",
"rc-progress": "^3.0.0",
"react": "^16.12.0",
@@ -54,8 +55,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx
index 7fe77423e2..c7d0f5ce01 100644
--- a/packages/core/src/api-wrappers/createApp.tsx
+++ b/packages/core/src/api-wrappers/createApp.tsx
@@ -22,7 +22,8 @@ import privateExports, {
AppConfigLoader,
} from '@backstage/core-api';
import { BrowserRouter, MemoryRouter } from 'react-router-dom';
-
+import LightIcon from '@material-ui/icons/WbSunny';
+import DarkIcon from '@material-ui/icons/Brightness2';
import { ErrorPage } from '../layout/ErrorPage';
import { Progress } from '../components/Progress';
import { defaultApis } from './defaultApis';
@@ -110,12 +111,14 @@ export function createApp(options?: AppOptions) {
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
+ icon: ,
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
theme: darkTheme,
+ icon: ,
},
];
const configLoader = options?.configLoader ?? defaultConfigLoader;
diff --git a/packages/core/src/components/Button/Button.stories.tsx b/packages/core/src/components/Button/Button.stories.tsx
index aaca389f82..a5005574fe 100644
--- a/packages/core/src/components/Button/Button.stories.tsx
+++ b/packages/core/src/components/Button/Button.stories.tsx
@@ -24,7 +24,7 @@ const Location = () => {
};
export default {
- title: 'Button',
+ title: 'Inputs/Button',
component: Button,
decorators: [
(storyFn: FunctionComponentFactory<{}>) => (
diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx
new file mode 100644
index 0000000000..12f19b17a1
--- /dev/null
+++ b/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { CheckboxTree } from '.';
+
+const CHECKBOX_TREE_ITEMS = [
+ {
+ label: 'Genereic subcategory name 1',
+ options: [
+ {
+ label: 'Option 1',
+ value: 1,
+ },
+ {
+ label: 'Option 2',
+ value: 2,
+ },
+ ],
+ },
+ {
+ label: 'Genereic subcategory name 2',
+ options: [
+ {
+ label: 'Option 1',
+ value: 1,
+ },
+ {
+ label: 'Option 2',
+ value: 2,
+ },
+ ],
+ },
+ {
+ label: 'Genereic subcategory name 3',
+ options: [
+ {
+ label: 'Option 1',
+ value: 1,
+ },
+ {
+ label: 'Option 2',
+ value: 2,
+ },
+ ],
+ },
+];
+
+export default {
+ title: 'CheckboxTree',
+ component: CheckboxTree,
+};
+
+export const Default = () => (
+ {}}
+ label="default"
+ subCategories={CHECKBOX_TREE_ITEMS}
+ />
+);
diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx
new file mode 100644
index 0000000000..f5eaa0e58b
--- /dev/null
+++ b/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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, fireEvent } from '@testing-library/react';
+
+import { CheckboxTree } from '.';
+
+const CHECKBOX_TREE_ITEMS = [
+ {
+ label: 'Genereic subcategory name 1',
+ options: [
+ {
+ label: 'Option 1',
+ value: 1,
+ },
+ {
+ label: 'Option 2',
+ value: 2,
+ },
+ ],
+ },
+];
+
+const minProps = {
+ onChange: jest.fn(),
+ label: 'Default',
+ subCategories: CHECKBOX_TREE_ITEMS,
+};
+
+describe(' ', () => {
+ it('renders without exploding', async () => {
+ const { getByText, getByTestId } = render( );
+
+ expect(getByText('Genereic subcategory name 1')).toBeInTheDocument();
+ const checkbox = await getByTestId('expandable');
+
+ // Simulate click on expandable arrow
+ fireEvent.click(checkbox);
+
+ // Simulate click on option
+ const option = getByText('Option 1');
+ expect(getByText('Option 1')).toBeInTheDocument();
+ fireEvent.click(option);
+ expect(minProps.onChange).toHaveBeenCalled();
+ });
+});
diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.tsx
new file mode 100644
index 0000000000..6825704971
--- /dev/null
+++ b/packages/core/src/components/CheckboxTree/CheckboxTree.tsx
@@ -0,0 +1,294 @@
+/*
+ * 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.
+ */
+/* eslint-disable guard-for-in */
+import React, { useEffect, useReducer } from 'react';
+import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
+import {
+ List,
+ ListItem,
+ ListItemIcon,
+ Checkbox,
+ ListItemText,
+ Collapse,
+ Typography,
+} from '@material-ui/core';
+import ExpandLess from '@material-ui/icons/ExpandLess';
+import ExpandMore from '@material-ui/icons/ExpandMore';
+import produce from 'immer';
+
+type IndexedObject = {
+ [key: string]: T;
+};
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ root: {
+ width: '100%',
+ minWidth: 10,
+ maxWidth: 360,
+ backgroundColor: 'transparent',
+ '&:hover': {
+ backgroundColor: 'transparent',
+ },
+ '&:active': {
+ animation: 'none',
+ transform: 'none',
+ },
+ },
+ nested: {
+ paddingLeft: theme.spacing(5),
+ height: '32px',
+ '&:hover': {
+ backgroundColor: 'transparent',
+ },
+ },
+ listItemIcon: {
+ minWidth: 10,
+ },
+ listItem: {
+ '&:hover': {
+ backgroundColor: 'transparent',
+ },
+ },
+ text: {
+ '& span, & svg': {
+ fontWeight: 'normal',
+ fontSize: 14,
+ },
+ },
+ }),
+);
+
+/* SUB_CATEGORY */
+
+type SubCategory = {
+ label: string;
+ isChecked?: boolean;
+ isOpen?: boolean;
+ options?: Option[];
+};
+
+type SubCategoryWithIndexedOptions = {
+ label: string;
+ isChecked?: boolean;
+ isOpen?: boolean;
+ options: IndexedObject;
+};
+
+/* OPTION */
+
+type Option = {
+ label: string;
+ value: string | number;
+ isChecked?: boolean;
+};
+
+export type CheckboxTreeProps = {
+ subCategories: SubCategory[];
+ label: string;
+ triggerReset?: boolean;
+ onChange: (arg: any) => any;
+};
+
+/* REDUCER */
+
+type checkOptionPayload = {
+ subCategoryLabel: string;
+ optionLabel: string;
+};
+
+type Action =
+ | { type: 'checkOption'; payload: checkOptionPayload }
+ | { type: 'checkCategory'; payload: string }
+ | { type: 'toggleCategory'; payload: string }
+ | { type: 'triggerReset' };
+
+const reducer = (
+ state: IndexedObject,
+ action: Action,
+) => {
+ switch (action.type) {
+ case 'checkOption': {
+ return produce(state, newState => {
+ const category = newState[action.payload.subCategoryLabel];
+ const option = category.options[action.payload.optionLabel];
+ option.isChecked = !option.isChecked;
+ category.isChecked = Object.values(category.options).every(
+ o => o.isChecked,
+ );
+ });
+ }
+ case 'checkCategory': {
+ return produce(state, newState => {
+ const category = newState[action.payload];
+ const options = category.options;
+ category.isChecked = !category.isChecked;
+ for (const option in options) {
+ options[option].isChecked = category.isChecked;
+ }
+ });
+ }
+ case 'toggleCategory':
+ return produce(state, newState => {
+ const category = newState[action.payload];
+ category.isOpen = !category.isOpen;
+ });
+ case 'triggerReset': {
+ return produce(state, newState => {
+ for (const category in newState) {
+ newState[category].isChecked = false;
+ for (const option in newState[category].options) {
+ newState[category].options[option].isChecked =
+ newState[category].isChecked;
+ }
+ }
+ });
+ }
+ default:
+ return state;
+ }
+};
+
+const indexer = (
+ arr: SubCategory[],
+): IndexedObject =>
+ arr.reduce((accumulator, el) => {
+ if (el.options) {
+ return {
+ ...accumulator,
+ [el.label]: {
+ label: el.label,
+ isChecked: el.isChecked || false,
+ isOpen: false,
+ options: indexer(el.options),
+ },
+ };
+ }
+ return {
+ ...accumulator,
+ [el.label]: { ...el, isChecked: el.isChecked || false },
+ };
+ }, {});
+
+export const CheckboxTree = (props: CheckboxTreeProps) => {
+ const { onChange } = props;
+ const classes = useStyles();
+
+ const [state, dispatch] = useReducer(reducer, indexer(props.subCategories));
+
+ const handleOpen = (event: any, value: any) => {
+ event.stopPropagation();
+ dispatch({ type: 'toggleCategory', payload: value });
+ };
+
+ useEffect(() => {
+ const values = Object.values(state).map(category => ({
+ category: category.isChecked ? category.label : null,
+ selectedChilds: Object.values(category.options)
+ .filter(option => option.isChecked)
+ .map(option => option.value),
+ }));
+ onChange(values);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state]);
+
+ useEffect(() => {
+ dispatch({ type: 'triggerReset' });
+ }, [props.triggerReset]);
+
+ return (
+
+
{props.label}
+
+ {Object.values(state).map(item => (
+
+
+ dispatch({
+ type: 'checkCategory',
+ payload: item.label,
+ })
+ }
+ >
+
+
+
+
+ {Object.values(item.options).length ? (
+ <>
+ {item.isOpen ? (
+ handleOpen(event, item.label)}
+ />
+ ) : (
+ handleOpen(event, item.label)}
+ />
+ )}
+ >
+ ) : null}
+
+
+
+ {Object.values(item.options).map(option => (
+
+ dispatch({
+ type: 'checkOption',
+ payload: {
+ subCategoryLabel: item.label,
+ optionLabel: option.label,
+ },
+ })
+ }
+ >
+
+
+
+
+
+ ))}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/packages/core/src/components/CheckboxTree/index.tsx b/packages/core/src/components/CheckboxTree/index.tsx
new file mode 100644
index 0000000000..d8e62460ab
--- /dev/null
+++ b/packages/core/src/components/CheckboxTree/index.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { CheckboxTree } from './CheckboxTree';
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx
index 77f3ca51cc..6df7e5bb6d 100644
--- a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx
@@ -19,7 +19,7 @@ import { CodeSnippet } from './CodeSnippet';
import { InfoCard } from '../../layout/InfoCard';
export default {
- title: 'CodeSnippet',
+ title: 'Data Display/CodeSnippet',
component: CodeSnippet,
};
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
index b6d9b3a5aa..84b5aa403e 100644
--- a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import React, { FC } from 'react';
-import PropTypes from 'prop-types';
+import React from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { useTheme } from '@material-ui/core';
@@ -27,28 +26,39 @@ type Props = {
language: string;
showLineNumbers?: boolean;
showCopyCodeButton?: boolean;
+ highlightedNumbers?: number[];
+ customStyle?: any;
};
-const defaultProps = {
- showLineNumbers: false,
- showCopyCodeButton: false,
-};
-
-export const CodeSnippet: FC = props => {
- const { text, language, showLineNumbers, showCopyCodeButton } = {
- ...defaultProps,
- ...props,
- };
-
+export const CodeSnippet = ({
+ text,
+ language,
+ showLineNumbers = false,
+ showCopyCodeButton = false,
+ highlightedNumbers,
+ customStyle,
+}: Props) => {
const theme = useTheme();
const mode = theme.palette.type === 'dark' ? dark : docco;
-
+ const highlightColor = theme.palette.type === 'dark' ? '#256bf3' : '#e6ffed';
return (
+ highlightedNumbers?.includes(lineNumber)
+ ? {
+ style: {
+ backgroundColor: highlightColor,
+ },
+ }
+ : {}
+ }
>
{text}
@@ -60,11 +70,3 @@ export const CodeSnippet: FC
= props => {
);
};
-
-// Type check for the JS files using this core component
-CodeSnippet.propTypes = {
- text: PropTypes.string.isRequired,
- language: PropTypes.string.isRequired,
- showLineNumbers: PropTypes.bool,
- showCopyCodeButton: PropTypes.bool,
-};
diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
index 1894574df2..e6f5f8cece 100644
--- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
+++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { CopyTextButton } from '.';
export default {
- title: 'CopyTextButton',
+ title: 'Inputs/CopyTextButton',
component: CopyTextButton,
};
diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx
index 148e7b6803..d77a713b34 100644
--- a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx
+++ b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx
@@ -28,7 +28,7 @@ import {
} from '@backstage/core-api';
export default {
- title: 'DismissableBanner',
+ title: 'Feedback/DismissableBanner',
component: DismissableBanner,
};
diff --git a/packages/core/src/components/EmptyState/EmptyState.stories.tsx b/packages/core/src/components/EmptyState/EmptyState.stories.tsx
index f3e40bd31c..8f846797a1 100644
--- a/packages/core/src/components/EmptyState/EmptyState.stories.tsx
+++ b/packages/core/src/components/EmptyState/EmptyState.stories.tsx
@@ -17,15 +17,16 @@
import React from 'react';
import { EmptyState } from './EmptyState';
import { Button } from '@material-ui/core';
+import { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState';
export default {
- title: 'EmptyState',
+ title: 'Feedback/EmptyState',
component: EmptyState,
};
const containerStyle = { width: '100%', height: '100vh' };
-export const MissingAnnotation = () => (
+export const Field = () => (
(
);
-export const NoInformation = () => (
+export const Info = () => (
(
);
-export const CreateComponent = () => (
+export const Content = () => (
(
);
-export const NoBuild = () => (
+export const Data = () => (
(
/>
);
+
+export const MissingAnnotation = () => (
+
+
+
+);
diff --git a/packages/core/src/components/EmptyState/EmptyState.tsx b/packages/core/src/components/EmptyState/EmptyState.tsx
index 88becc7c90..4469c3ce86 100644
--- a/packages/core/src/components/EmptyState/EmptyState.tsx
+++ b/packages/core/src/components/EmptyState/EmptyState.tsx
@@ -22,7 +22,7 @@ import Background from './assets/Background.svg';
const useStyles = makeStyles(theme => ({
root: {
backgroundColor: theme.palette.background.default,
- padding: theme.spacing(10, 0, 0, 0),
+ padding: theme.spacing(2, 0, 0, 0),
},
action: {
marginTop: theme.spacing(2),
@@ -56,7 +56,7 @@ export const EmptyState = ({ title, description, missing, action }: Props) => {
>
- {title}
+ {title}
{description}
diff --git a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx
new file mode 100644
index 0000000000..d8847fdf47
--- /dev/null
+++ b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx
@@ -0,0 +1,82 @@
+/*
+ * 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 { Button, makeStyles, Typography } from '@material-ui/core';
+import { BackstageTheme } from '@backstage/theme';
+import { EmptyState } from './EmptyState';
+import { CodeSnippet } from '../CodeSnippet';
+
+const COMPONENT_YAML = `# Example
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: example
+ description: example.com
+ annotations:
+ ANNOTATION: value
+spec:
+ type: website
+ lifecycle: production
+ owner: guest
+`;
+
+type Props = {
+ annotation: string;
+};
+
+const useStyles = makeStyles(theme => ({
+ code: {
+ borderRadius: 6,
+ margin: `${theme.spacing(2)}px 0px`,
+ background: theme.palette.type === 'dark' ? '#444' : '#fff',
+ },
+}));
+
+export const MissingAnnotationEmptyState = ({ annotation }: Props) => {
+ const classes = useStyles();
+ return (
+
+
+ Add the annotation to your component YAML as shown in the
+ highlighted example below:
+
+
+
+
+
+ Read more
+
+ >
+ }
+ />
+ );
+};
diff --git a/packages/core/src/components/EmptyState/assets/Background.svg b/packages/core/src/components/EmptyState/assets/Background.svg
index e2fa538f7f..ce9aae6739 100644
--- a/packages/core/src/components/EmptyState/assets/Background.svg
+++ b/packages/core/src/components/EmptyState/assets/Background.svg
@@ -1,10 +1 @@
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/components/EmptyState/assets/createComponent.svg b/packages/core/src/components/EmptyState/assets/createComponent.svg
index af2ddd5678..bb8b50e1ef 100644
--- a/packages/core/src/components/EmptyState/assets/createComponent.svg
+++ b/packages/core/src/components/EmptyState/assets/createComponent.svg
@@ -1,35 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/components/EmptyState/assets/missingAnnotation.svg b/packages/core/src/components/EmptyState/assets/missingAnnotation.svg
index 475cda9699..39534656fa 100644
--- a/packages/core/src/components/EmptyState/assets/missingAnnotation.svg
+++ b/packages/core/src/components/EmptyState/assets/missingAnnotation.svg
@@ -1,41 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/components/EmptyState/assets/noBuild.svg b/packages/core/src/components/EmptyState/assets/noBuild.svg
index 76a41749b9..ef45e1c736 100644
--- a/packages/core/src/components/EmptyState/assets/noBuild.svg
+++ b/packages/core/src/components/EmptyState/assets/noBuild.svg
@@ -1,43 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/components/EmptyState/assets/noInformation.svg b/packages/core/src/components/EmptyState/assets/noInformation.svg
index e41cf94645..9a1c230e97 100644
--- a/packages/core/src/components/EmptyState/assets/noInformation.svg
+++ b/packages/core/src/components/EmptyState/assets/noInformation.svg
@@ -1,42 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/components/EmptyState/index.ts b/packages/core/src/components/EmptyState/index.ts
index 3774010467..2e2a88be94 100644
--- a/packages/core/src/components/EmptyState/index.ts
+++ b/packages/core/src/components/EmptyState/index.ts
@@ -15,3 +15,4 @@
*/
export { EmptyState } from './EmptyState';
+export { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState';
diff --git a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx
index b65c5bff5d..9718393427 100644
--- a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx
+++ b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.stories.tsx
@@ -21,7 +21,7 @@ const containerStyle = { width: 800, height: 400, margin: 20 };
const opacityArray = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
export default {
- title: 'HorizontalScrollGrid',
+ title: 'Layout/HorizontalScrollGrid',
component: HorizontalScrollGrid,
};
diff --git a/packages/core/src/components/Lifecycle/Lifecycle.stories.tsx b/packages/core/src/components/Lifecycle/Lifecycle.stories.tsx
index c29eb8a02f..ccce7fba25 100644
--- a/packages/core/src/components/Lifecycle/Lifecycle.stories.tsx
+++ b/packages/core/src/components/Lifecycle/Lifecycle.stories.tsx
@@ -17,7 +17,7 @@ import React from 'react';
import { Lifecycle } from './Lifecycle';
export default {
- title: 'Lifecycle',
+ title: 'Feedback/Lifecycle',
component: Lifecycle,
};
diff --git a/packages/core/src/components/Link/Link.stories.tsx b/packages/core/src/components/Link/Link.stories.tsx
index 2bd499fa33..852d51970f 100644
--- a/packages/core/src/components/Link/Link.stories.tsx
+++ b/packages/core/src/components/Link/Link.stories.tsx
@@ -29,7 +29,7 @@ const Location = () => {
};
export default {
- title: 'Link',
+ title: 'Navigation/Link',
component: Link,
decorators: [
(storyFn: FunctionComponentFactory<{}>) => (
diff --git a/packages/core/src/components/Progress/Progress.stories.tsx b/packages/core/src/components/Progress/Progress.stories.tsx
index b935f60695..cd17dce8b4 100644
--- a/packages/core/src/components/Progress/Progress.stories.tsx
+++ b/packages/core/src/components/Progress/Progress.stories.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { Progress } from '.';
export default {
- title: 'Progress',
+ title: 'Feedback/Progress',
component: Progress,
};
diff --git a/packages/core/src/components/ProgressBars/GaugeProgress.stories.tsx b/packages/core/src/components/ProgressBars/Gauge.stories.tsx
similarity index 77%
rename from packages/core/src/components/ProgressBars/GaugeProgress.stories.tsx
rename to packages/core/src/components/ProgressBars/Gauge.stories.tsx
index 48f4cb673e..7882714529 100644
--- a/packages/core/src/components/ProgressBars/GaugeProgress.stories.tsx
+++ b/packages/core/src/components/ProgressBars/Gauge.stories.tsx
@@ -15,41 +15,41 @@
*/
import React from 'react';
-import { GaugeProgress } from './GaugeProgress';
+import { Gauge } from './Gauge';
const containerStyle = { width: 300 };
export default {
- title: 'GaugeProgress',
- component: GaugeProgress,
+ title: 'Feedback/Gauge',
+ component: Gauge,
};
export const Default = () => (
-
+
);
export const MediumProgress = () => (
-
+
);
export const LowProgress = () => (
-
+
);
export const InverseLowProgress = () => (
-
+
);
export const AbsoluteProgress = () => (
-
+
);
diff --git a/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx b/packages/core/src/components/ProgressBars/Gauge.test.jsx
similarity index 82%
rename from packages/core/src/components/ProgressBars/GaugeProgress.test.jsx
rename to packages/core/src/components/ProgressBars/Gauge.test.jsx
index 778abdf12c..709de7b0c4 100644
--- a/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx
+++ b/packages/core/src/components/ProgressBars/Gauge.test.jsx
@@ -17,32 +17,32 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
-import { GaugeProgress, getProgressColor } from './GaugeProgress';
+import { Gauge, getProgressColor } from './Gauge';
-describe(' ', () => {
+describe(' ', () => {
it('renders without exploding', () => {
const { getByText } = render(
- wrapInTestApp( ),
+ wrapInTestApp( ),
);
getByText('10%');
});
it('handles fractional prop', () => {
const { getByText } = render(
- wrapInTestApp( ),
+ wrapInTestApp( ),
);
getByText('10%');
});
it('handles max prop', () => {
const { getByText } = render(
- wrapInTestApp( ),
+ wrapInTestApp( ),
);
getByText('1%');
});
it('handles unit prop', () => {
const { getByText } = render(
- wrapInTestApp( ),
+ wrapInTestApp( ),
);
getByText('10m');
});
diff --git a/packages/core/src/components/ProgressBars/GaugeProgress.tsx b/packages/core/src/components/ProgressBars/Gauge.tsx
similarity index 98%
rename from packages/core/src/components/ProgressBars/GaugeProgress.tsx
rename to packages/core/src/components/ProgressBars/Gauge.tsx
index 14776ed431..4c339bf8e1 100644
--- a/packages/core/src/components/ProgressBars/GaugeProgress.tsx
+++ b/packages/core/src/components/ProgressBars/Gauge.tsx
@@ -77,7 +77,7 @@ export function getProgressColor(
return palette.status.ok;
}
-export const GaugeProgress: FC = props => {
+export const Gauge: FC = props => {
const classes = useStyles(props);
const theme = useTheme();
const { value, fractional, inverse, unit, max } = {
diff --git a/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
index 922bb63b2f..45de49cd2d 100644
--- a/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
+++ b/packages/core/src/components/ProgressBars/GaugeCard.stories.tsx
@@ -21,7 +21,7 @@ import { Grid } from '@material-ui/core';
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export default {
- title: 'Progress Card',
+ title: 'Data Display/Progress Card',
component: GaugeCard,
};
diff --git a/packages/core/src/components/ProgressBars/GaugeCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx
index fc7055f705..4eb4b2e075 100644
--- a/packages/core/src/components/ProgressBars/GaugeCard.tsx
+++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import { InfoCard } from '../../layout/InfoCard';
import { BottomLinkProps } from '../../layout/BottomLink';
-import { GaugeProgress } from './GaugeProgress';
+import { Gauge } from './Gauge';
type Props = {
title: string;
@@ -48,7 +48,7 @@ export const GaugeCard: FC = props => {
deepLink={deepLink}
variant={variant}
>
-
+
);
diff --git a/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx
index c4492986b6..a745d210b4 100644
--- a/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx
+++ b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx
@@ -20,7 +20,7 @@ import { LinearGauge } from './LinearGauge';
const containerStyle = { width: 300 };
export default {
- title: 'LinearGauge',
+ title: 'Feedback/LinearGauge',
component: LinearGauge,
};
diff --git a/packages/core/src/components/ProgressBars/LinearGauge.tsx b/packages/core/src/components/ProgressBars/LinearGauge.tsx
index 73163b345f..2b8e77838d 100644
--- a/packages/core/src/components/ProgressBars/LinearGauge.tsx
+++ b/packages/core/src/components/ProgressBars/LinearGauge.tsx
@@ -19,7 +19,7 @@ import { Tooltip, useTheme } from '@material-ui/core';
// @ts-ignore
import { Line } from 'rc-progress';
import { BackstageTheme } from '@backstage/theme';
-import { getProgressColor } from './GaugeProgress';
+import { getProgressColor } from './Gauge';
type Props = {
/**
diff --git a/packages/core/src/components/ProgressBars/index.ts b/packages/core/src/components/ProgressBars/index.ts
index c7131c8831..4463aea29b 100644
--- a/packages/core/src/components/ProgressBars/index.ts
+++ b/packages/core/src/components/ProgressBars/index.ts
@@ -15,5 +15,5 @@
*/
export { GaugeCard } from './GaugeCard';
-export { GaugeProgress } from './GaugeProgress';
+export { Gauge } from './Gauge';
export { LinearGauge } from './LinearGauge';
diff --git a/packages/core/src/components/Select/Select.stories.tsx b/packages/core/src/components/Select/Select.stories.tsx
new file mode 100644
index 0000000000..feb5c1cffb
--- /dev/null
+++ b/packages/core/src/components/Select/Select.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 { Select } from '.';
+
+export default {
+ title: 'Select',
+ component: Select,
+};
+
+const SELECT_ITEMS = [
+ {
+ label: 'test 1',
+ value: 'test_1',
+ },
+ {
+ label: 'test 2',
+ value: 'test_2',
+ },
+ {
+ label: 'test 3',
+ value: 'test_3',
+ },
+];
+
+export const Default = () => (
+ {}}
+ placeholder="All results"
+ label="Default"
+ items={SELECT_ITEMS}
+ />
+);
+
+export const Multiple = () => (
+ {}}
+ />
+);
diff --git a/packages/core/src/components/Select/Select.test.tsx b/packages/core/src/components/Select/Select.test.tsx
new file mode 100644
index 0000000000..9063804573
--- /dev/null
+++ b/packages/core/src/components/Select/Select.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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, fireEvent } from '@testing-library/react';
+
+import { Select } from '.';
+
+const SELECT_ITEMS = [
+ {
+ label: 'test 1',
+ value: 'test_1',
+ },
+ {
+ label: 'test 2',
+ value: 'test_2',
+ },
+];
+
+const minProps = {
+ onChange: jest.fn(),
+ label: 'Default',
+ placeholder: 'All results',
+ items: SELECT_ITEMS,
+};
+
+describe(' ', () => {
+ it('renders without exploding', async () => {
+ const { getByText, getByTestId } = render( );
+
+ expect(getByText('Default')).toBeInTheDocument();
+ const input = await getByTestId('select');
+ expect(input.textContent).toBe('All results');
+
+ // Simulate click on input
+ fireEvent.click(input);
+
+ expect(getByText('test 1')).toBeInTheDocument();
+ const option = getByText('test 1');
+
+ // Simulate click on option
+ fireEvent.click(option);
+ expect(input.textContent).toBe('test 1');
+ });
+});
diff --git a/packages/core/src/components/Select/Select.tsx b/packages/core/src/components/Select/Select.tsx
new file mode 100644
index 0000000000..a2efcc7725
--- /dev/null
+++ b/packages/core/src/components/Select/Select.tsx
@@ -0,0 +1,219 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import {
+ createStyles,
+ makeStyles,
+ withStyles,
+ Theme,
+} from '@material-ui/core/styles';
+
+import {
+ FormControl,
+ Select,
+ MenuItem,
+ InputBase,
+ Chip,
+ Typography,
+ Checkbox,
+ ClickAwayListener,
+} from '@material-ui/core';
+
+import ClosedDropdown from './static/ClosedDropdown';
+import OpenedDropdown from './static/OpenedDropdown';
+
+const BootstrapInput = withStyles((theme: Theme) =>
+ createStyles({
+ root: {
+ 'label + &': {
+ marginTop: theme.spacing(3),
+ },
+ },
+ input: {
+ borderRadius: 4,
+ position: 'relative',
+ backgroundColor: theme.palette.background.paper,
+ border: '1px solid #ced4da',
+ fontSize: 16,
+ padding: '10px 26px 10px 12px',
+ transition: theme.transitions.create(['border-color', 'box-shadow']),
+ fontFamily: 'Helvetica Neue',
+ '&:focus': {
+ background: theme.palette.background.paper,
+ borderRadius: 4,
+ },
+ },
+ }),
+)(InputBase);
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: `${theme.spacing(1)} 0px`,
+ maxWidth: 300,
+ },
+ label: {
+ transform: 'initial',
+ fontWeight: 'bold',
+ fontSize: 14,
+ fontFamily: theme.typography.fontFamily,
+ color: theme.palette.text.primary,
+ '&.Mui-focused': {
+ color: theme.palette.text.primary,
+ },
+ },
+ chips: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ },
+ chip: {
+ margin: 2,
+ },
+ checkbox: {},
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ }),
+);
+
+type Item = {
+ label: string;
+ value: string | number;
+};
+
+export type SelectProps = {
+ multiple?: boolean;
+ items: Item[];
+ label: string;
+ placeholder?: string;
+ onChange: (arg: any) => any;
+ triggerReset?: boolean;
+};
+
+export const SelectComponent = (props: SelectProps) => {
+ const { multiple, items, label, placeholder, onChange } = props;
+ const classes = useStyles();
+ const [value, setValue] = useState(
+ multiple ? [] : '',
+ );
+ const [canOpen, setCanOpen] = React.useState(false);
+
+ useEffect(() => {
+ setValue(multiple ? [] : '');
+ }, [props.triggerReset, multiple]);
+
+ const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+ setValue(event.target.value as any);
+ onChange(event.target.value);
+ };
+
+ const selectHandleOnOpen = () => {
+ setCanOpen(previous => {
+ if (multiple) {
+ return true;
+ }
+ return !previous;
+ });
+ };
+
+ const handleClickAway = (event: React.ChangeEvent) => {
+ if (event.target.id !== 'menu-item') {
+ setCanOpen(false);
+ }
+ };
+
+ const handleDelete = (selectedValue: string | number) => () => {
+ const newValue = (value as any[]).filter(chip => chip !== selectedValue);
+ setValue(newValue);
+ onChange(newValue);
+ };
+
+ return (
+
+
{label}
+
+
+ }
+ renderValue={selected =>
+ multiple && (value as any[]).length !== 0 ? (
+
+ {(selected as string[]).map(selectedValue => (
+ el.value === selectedValue)?.value}
+ label={
+ items.find(el => el.value === selectedValue)?.label
+ }
+ clickable
+ onDelete={handleDelete(selectedValue)}
+ className={classes.chip}
+ />
+ ))}
+
+ ) : (
+
+ {(value as any[]).length === 0
+ ? placeholder || ''
+ : items.find(el => el.value === selected)?.label}
+
+ )
+ }
+ IconComponent={() =>
+ !canOpen ? :
+ }
+ MenuProps={{
+ anchorOrigin: {
+ vertical: 'bottom',
+ horizontal: 'left',
+ },
+ transformOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ getContentAnchorEl: null,
+ }}
+ >
+ {placeholder && !multiple && (
+ {placeholder}
+ )}
+ {items &&
+ items.map(item => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/packages/core/src/components/Select/index.tsx b/packages/core/src/components/Select/index.tsx
new file mode 100644
index 0000000000..977ebc88eb
--- /dev/null
+++ b/packages/core/src/components/Select/index.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { SelectComponent as Select } from './Select';
diff --git a/packages/core/src/components/Select/static/ClosedDropdown.tsx b/packages/core/src/components/Select/static/ClosedDropdown.tsx
new file mode 100644
index 0000000000..235a91963b
--- /dev/null
+++ b/packages/core/src/components/Select/static/ClosedDropdown.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { SvgIcon, makeStyles, createStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(() =>
+ createStyles({
+ icon: {
+ position: 'absolute',
+ right: '4px',
+ pointerEvents: 'none',
+ },
+ }),
+);
+
+const ClosedDropdown = () => {
+ const classes = useStyles();
+ return (
+
+
+
+ );
+};
+
+export default ClosedDropdown;
diff --git a/packages/core/src/components/Select/static/OpenedDropdown.tsx b/packages/core/src/components/Select/static/OpenedDropdown.tsx
new file mode 100644
index 0000000000..e4a8021017
--- /dev/null
+++ b/packages/core/src/components/Select/static/OpenedDropdown.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { SvgIcon, makeStyles, createStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(() =>
+ createStyles({
+ icon: {
+ position: 'absolute',
+ right: '4px',
+ pointerEvents: 'none',
+ },
+ }),
+);
+
+const OpenedDropdown = () => {
+ const classes = useStyles();
+ return (
+
+
+
+ );
+};
+
+export default OpenedDropdown;
diff --git a/packages/core/src/components/SimpleStepper/SimpleStepper.stories.tsx b/packages/core/src/components/SimpleStepper/SimpleStepper.stories.tsx
index 52710ffeaf..e7163ea533 100644
--- a/packages/core/src/components/SimpleStepper/SimpleStepper.stories.tsx
+++ b/packages/core/src/components/SimpleStepper/SimpleStepper.stories.tsx
@@ -19,7 +19,7 @@ import { TextField } from '@material-ui/core';
import { SimpleStepper, SimpleStepperStep } from '.';
export default {
- title: 'SimpleStepper',
+ title: 'Navigation/SimpleStepper',
component: SimpleStepper,
};
diff --git a/packages/core/src/components/Status/Status.stories.tsx b/packages/core/src/components/Status/Status.stories.tsx
index 01e2876524..560d07ad87 100644
--- a/packages/core/src/components/Status/Status.stories.tsx
+++ b/packages/core/src/components/Status/Status.stories.tsx
@@ -27,7 +27,7 @@ import { Table } from '../Table';
import { InfoCard } from '../../layout/InfoCard';
export default {
- title: 'Status',
+ title: 'Feedback/Status',
component: StatusOK,
};
diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx
index 9db7385872..97591ebab3 100644
--- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx
+++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx
@@ -37,7 +37,7 @@ const metadata = {
};
export default {
- title: 'Structured Metadata Table',
+ title: 'Data Display/Structured Metadata Table',
component: StructuredMetadataTable,
};
diff --git a/packages/core/src/components/Table/Filters.tsx b/packages/core/src/components/Table/Filters.tsx
new file mode 100644
index 0000000000..c8f468559a
--- /dev/null
+++ b/packages/core/src/components/Table/Filters.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { BackstageTheme } from '@backstage/theme';
+import { Button, makeStyles } from '@material-ui/core';
+import { Select } from '../Select';
+import { CheckboxTree } from '../CheckboxTree';
+import { CheckboxTreeProps } from '../CheckboxTree/CheckboxTree';
+import { SelectProps } from '../Select/Select';
+
+const useSubvalueCellStyles = makeStyles(theme => ({
+ root: {
+ height: '100%',
+ width: '315px',
+ display: 'flex',
+ flexDirection: 'column',
+ marginRight: theme.spacing(3),
+ },
+ value: {
+ fontWeight: 'bold',
+ fontSize: 18,
+ },
+ header: {
+ display: 'flex',
+ alignItems: 'center',
+ height: '60px',
+ justifyContent: 'space-between',
+ borderBottom: `1px solid ${theme.palette.grey[500]}`,
+ },
+ filters: {
+ display: 'flex',
+ flexDirection: 'column',
+ '& > *': {
+ marginTop: theme.spacing(2),
+ },
+ },
+}));
+
+type Without = Pick>;
+
+export type Filter = {
+ type: 'select' | 'checkbox-tree' | 'multiple-select';
+ element:
+ | Without
+ | Without;
+};
+
+export type SelectedFilters = {
+ [key: string]: string | string[];
+};
+
+type Props = {
+ filters: Filter[];
+ onChangeFilters: (arg: any) => any;
+};
+
+export const Filters = (props: Props) => {
+ const classes = useSubvalueCellStyles();
+
+ const { onChangeFilters } = props;
+
+ const [filters, setFilters] = useState(props.filters);
+ const [selectedFilters, setSelectedFilters] = useState({});
+ const [reset, triggerReset] = useState(false);
+
+ // Trigger re-rendering
+ const handleClick = () => {
+ setSelectedFilters({});
+ setFilters([...props.filters]);
+ triggerReset(el => !el);
+ };
+
+ useEffect(() => {
+ onChangeFilters(selectedFilters);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedFilters]);
+
+ // As material table doesn't provide a way to add a column filter tab we will make our own filter logic
+ return (
+
+
+
Filters
+
+ Clear all
+
+
+
+ {filters?.length &&
+ filters.map(filter =>
+ filter.type === 'checkbox-tree' ? (
+
+ setSelectedFilters({
+ ...selectedFilters,
+ [filter.element.label]: el
+ .filter(
+ (checkboxFilter: any) =>
+ checkboxFilter.category !== null ||
+ checkboxFilter.selectedChilds.length,
+ )
+ .map((checkboxFilter: any) =>
+ checkboxFilter.category !== null
+ ? [
+ ...checkboxFilter.selectedChilds,
+ checkboxFilter.category,
+ ]
+ : checkboxFilter.selectedChilds,
+ )
+ .flat(),
+ })
+ }
+ />
+ ) : (
+
+ setSelectedFilters({
+ ...selectedFilters,
+ [filter.element.label]: el,
+ })
+ }
+ />
+ ),
+ )}
+
+
+ );
+};
diff --git a/packages/core/src/components/Table/Table.stories.tsx b/packages/core/src/components/Table/Table.stories.tsx
index 11fa0c44dc..4be85e494f 100644
--- a/packages/core/src/components/Table/Table.stories.tsx
+++ b/packages/core/src/components/Table/Table.stories.tsx
@@ -16,9 +16,10 @@
import React from 'react';
import { Table, SubvalueCell, TableColumn } from './';
+import { TableFilter } from './Table';
export default {
- title: 'Table',
+ title: 'Data Display/Table',
component: Table,
};
@@ -220,3 +221,53 @@ export const DenseTable = () => {
);
};
+
+export const FilterTable = () => {
+ const columns: TableColumn[] = [
+ {
+ title: 'Column 1',
+ field: 'col1',
+ highlight: true,
+ },
+ {
+ title: 'Column 2',
+ field: 'col2',
+ },
+ {
+ title: 'Numeric value',
+ field: 'number',
+ type: 'numeric',
+ },
+ {
+ title: 'A Date',
+ field: 'date',
+ type: 'date',
+ },
+ ];
+
+ const filters: TableFilter[] = [
+ {
+ column: 'Column 1',
+ type: 'select',
+ },
+ {
+ column: 'Column 2',
+ type: 'multiple-select',
+ },
+ {
+ column: 'Numeric value',
+ type: 'checkbox-tree',
+ },
+ ];
+
+ return (
+
+ );
+};
diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx
index 882272cb43..5d0b7d8344 100644
--- a/packages/core/src/components/Table/Table.tsx
+++ b/packages/core/src/components/Table/Table.tsx
@@ -15,7 +15,12 @@
*/
import { BackstageTheme } from '@backstage/theme';
-import { makeStyles, Typography, useTheme } from '@material-ui/core';
+import {
+ makeStyles,
+ Typography,
+ useTheme,
+ IconButton,
+} from '@material-ui/core';
// Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51
import AddBox from '@material-ui/icons/AddBox';
import ArrowUpward from '@material-ui/icons/ArrowUpward';
@@ -39,7 +44,8 @@ import MTable, {
MTableToolbar,
Options,
} from 'material-table';
-import React, { forwardRef } from 'react';
+import React, { forwardRef, useCallback, useEffect, useState } from 'react';
+import { Filters, SelectedFilters } from './Filters';
const tableIcons = {
Add: forwardRef((props, ref: React.Ref) => (
@@ -121,6 +127,26 @@ const useToolbarStyles = makeStyles(theme => ({
},
}));
+const useFilterStyles = makeStyles(() => ({
+ root: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 18,
+ whiteSpace: 'nowrap',
+ },
+}));
+
+const useTableStyles = makeStyles(() => ({
+ root: {
+ display: 'flex',
+ alignItems: 'start',
+ },
+}));
+
function convertColumns(
columns: TableColumn[],
theme: BackstageTheme,
@@ -148,10 +174,16 @@ export interface TableColumn extends Column {
width?: string;
}
+export type TableFilter = {
+ column: string;
+ type: 'select' | 'multiple-select' | 'checkbox-tree';
+};
+
export interface TableProps
extends MaterialTableProps {
columns: TableColumn[];
subtitle?: string;
+ filters?: TableFilter[];
}
export function Table({
@@ -159,12 +191,23 @@ export function Table({
options,
title,
subtitle,
+ filters,
...props
}: TableProps) {
const headerClasses = useHeaderStyles();
const toolbarClasses = useToolbarStyles();
+ const tableClasses = useTableStyles();
+ const filtersClasses = useFilterStyles();
+
+ const { data, ...propsWithoutData } = props;
+
const theme = useTheme();
+ const [filtersOpen, toggleFilters] = useState(false);
+ const [selectedFiltersLength, setSelectedFiltersLength] = useState(0);
+ const [tableData, setTableData] = useState(data as any[]);
+ const [selectedFilters, setSelectedFilters] = useState();
+
const MTColumns = convertColumns(columns, theme);
const defaultOptions: Options = {
@@ -173,30 +216,114 @@ export function Table({
},
};
+ const getFieldByTitle = useCallback(
+ (titleValue: string | keyof T) =>
+ columns.find(el => el.title === titleValue)?.field,
+ [columns],
+ );
+
+ useEffect(() => {
+ if (!selectedFilters) {
+ setTableData(data as any[]);
+ return;
+ }
+
+ const selectedFiltersArray = Object.values(selectedFilters);
+ if (selectedFiltersArray.flat().length) {
+ const newData = (data as any[]).filter(
+ el =>
+ !!Object.entries(selectedFilters)
+ .filter(([, value]) => !!value.length)
+ .every(([key, value]) => {
+ if (Array.isArray(value)) {
+ return value.includes(el[getFieldByTitle(key)]);
+ }
+ return el[getFieldByTitle(key)] === value;
+ }),
+ );
+ setTableData(newData);
+ } else {
+ setTableData(data as any[]);
+ }
+ setSelectedFiltersLength(selectedFiltersArray.flat().length);
+ }, [data, selectedFilters, getFieldByTitle]);
+
+ const constructFilters = (filterConfig: TableFilter[], dataValue: any[]) => {
+ const extractColumnData = (column: string | keyof T) =>
+ dataValue.map(el => ({ label: el[column], options: [] }));
+
+ return filterConfig.map(filter => ({
+ type: filter.type,
+ element:
+ filter.type === 'checkbox-tree'
+ ? {
+ label: filter.column,
+ subCategories: extractColumnData(
+ getFieldByTitle(filter.column) || '',
+ ),
+ }
+ : {
+ placeholder: 'All results',
+ label: filter.column,
+ multiple: filter.type === 'multiple-select',
+ items: dataValue.map(el => ({
+ label: el[getFieldByTitle(filter.column) || ''],
+ value: el[getFieldByTitle(filter.column) || ''],
+ })),
+ },
+ }));
+ };
+
return (
-
- components={{
- Header: headerProps => (
-
- ),
- Toolbar: toolbarProps => (
-
- ),
- }}
- options={{ ...defaultOptions, ...options }}
- columns={MTColumns}
- icons={tableIcons}
- title={
- <>
- {title}
- {subtitle && (
-
- {subtitle}
-
- )}
- >
- }
- {...props}
- />
+
+ {filtersOpen && filters?.length && (
+
+ )}
+
+ components={{
+ Header: headerProps => (
+
+ ),
+ Toolbar: toolbarProps =>
+ filters?.length ? (
+
+
+ toggleFilters(el => !el)}
+ aria-label="filter list"
+ >
+
+
+
+ Filters ({selectedFiltersLength})
+
+
+
+
+ ) : (
+
+ ),
+ }}
+ options={{ ...defaultOptions, ...options }}
+ columns={MTColumns}
+ icons={tableIcons}
+ title={
+ <>
+ {title}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ >
+ }
+ data={tableData}
+ style={{ width: '100%' }}
+ {...propsWithoutData}
+ />
+
);
}
diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx
index 930a825392..00819c44f1 100644
--- a/packages/core/src/components/Tabs/Tabs.stories.tsx
+++ b/packages/core/src/components/Tabs/Tabs.stories.tsx
@@ -19,7 +19,7 @@ import { Tabs } from './Tabs';
import AccessAlarmIcon from '@material-ui/icons/AccessAlarm';
export default {
- title: 'Tabs',
+ title: 'Navigation/Tabs',
component: Tabs,
};
diff --git a/packages/core/src/components/TrendLine/TrendLine.stories.tsx b/packages/core/src/components/TrendLine/TrendLine.stories.tsx
index cc2fdda052..4eb74b2f17 100644
--- a/packages/core/src/components/TrendLine/TrendLine.stories.tsx
+++ b/packages/core/src/components/TrendLine/TrendLine.stories.tsx
@@ -20,7 +20,7 @@ import { TrendLine } from './TrendLine';
import { InfoCard } from '../../layout/InfoCard';
export default {
- title: 'TrendLine',
+ title: 'Data Display/TrendLine',
component: TrendLine,
};
diff --git a/packages/core/src/components/WarningPanel/WarningPanel.stories.tsx b/packages/core/src/components/WarningPanel/WarningPanel.stories.tsx
index 92487ed8c5..5098f0aa31 100644
--- a/packages/core/src/components/WarningPanel/WarningPanel.stories.tsx
+++ b/packages/core/src/components/WarningPanel/WarningPanel.stories.tsx
@@ -19,7 +19,7 @@ import { WarningPanel } from './WarningPanel';
import { Link, Button } from '@material-ui/core';
export default {
- title: 'Warning Panel',
+ title: 'Feedback/Warning Panel',
component: WarningPanel,
};
diff --git a/packages/core/src/components/stories/Chip.stories.tsx b/packages/core/src/components/stories/Chip.stories.tsx
index c0e644fcc5..a605666482 100644
--- a/packages/core/src/components/stories/Chip.stories.tsx
+++ b/packages/core/src/components/stories/Chip.stories.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { Chip } from '@material-ui/core';
export default {
- title: 'Chip',
+ title: 'Data Display/Chip',
component: Chip,
};
diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
index 71a24652ba..3d80955cc9 100644
--- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx
+++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
@@ -65,7 +65,12 @@ export const ErrorPage = ({
Go back
... or if you think this is a bug, please file an{' '}
- issue.
+
+ issue.
+
diff --git a/packages/core/src/layout/ErrorPage/mic-drop.svg b/packages/core/src/layout/ErrorPage/mic-drop.svg
index adc027b551..5a7d7d8759 100644
--- a/packages/core/src/layout/ErrorPage/mic-drop.svg
+++ b/packages/core/src/layout/ErrorPage/mic-drop.svg
@@ -1,126 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx
index 39b9133c0a..91045d4bb4 100644
--- a/packages/core/src/layout/Header/Header.stories.tsx
+++ b/packages/core/src/layout/Header/Header.stories.tsx
@@ -19,7 +19,7 @@ import { HeaderLabel } from '../HeaderLabel';
import { Page, pageTheme } from '../Page';
export default {
- title: 'Header',
+ title: 'Layout/Header',
component: Header,
};
diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx
index 5d28c6633f..320592c218 100644
--- a/packages/core/src/layout/Header/Header.test.tsx
+++ b/packages/core/src/layout/Header/Header.test.tsx
@@ -58,4 +58,11 @@ describe('', () => {
);
rendered.getByText('tool');
});
+
+ it('should have breadcrumb rendered', () => {
+ const rendered = render(
+ wrapInTestApp(),
+ );
+ rendered.getAllByText('Title');
+ });
});
diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx
index 6b9ea05245..3bd95d0ef3 100644
--- a/packages/core/src/layout/Header/Header.tsx
+++ b/packages/core/src/layout/Header/Header.tsx
@@ -16,9 +16,15 @@
import React, { ReactNode, CSSProperties, FC, useContext } from 'react';
import { Helmet } from 'react-helmet';
-import { Typography, Tooltip, makeStyles } from '@material-ui/core';
+import {
+ Link,
+ Typography,
+ Tooltip,
+ makeStyles,
+ Breadcrumbs,
+} from '@material-ui/core';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import { BackstageTheme } from '@backstage/theme';
-
import { PageThemeContext } from '../Page/Page';
const useStyles = makeStyles(
@@ -69,6 +75,21 @@ const useStyles = makeStyles(
marginBottom: theme.spacing(1),
color: theme.palette.bursts.fontColor,
},
+ breadcrumb: {
+ fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))',
+ color: theme.palette.bursts.fontColor,
+ },
+ breadcrumbType: {
+ fontSize: 'inherit',
+ opacity: 0.7,
+ marginRight: -theme.spacing(0.3),
+ marginBottom: theme.spacing(0.3),
+ },
+ breadcrumbTitle: {
+ fontSize: 'inherit',
+ marginLeft: -theme.spacing(0.3),
+ marginBottom: theme.spacing(0.3),
+ },
}),
);
@@ -87,7 +108,8 @@ type Props = {
type TypeFragmentProps = {
classes: HeaderStyles;
- type?: Props['title'];
+ pageTitle: string | ReactNode;
+ type?: Props['type'];
typeLink?: Props['typeLink'];
};
@@ -102,17 +124,32 @@ type SubtitleFragmentProps = {
subtitle?: Props['subtitle'];
};
-const TypeFragment: FC = ({ type, typeLink, classes }) => {
+const TypeFragment: FC = ({
+ type,
+ typeLink,
+ classes,
+ pageTitle,
+}) => {
if (!type) {
return null;
}
if (!typeLink) {
- // TODO: Add breadcrumbs.
return {type} ;
}
- return {type} ;
+ return (
+ }
+ className={classes.breadcrumb}
+ >
+
+ {type}
+
+ {pageTitle}
+
+ );
};
const TitleFragment: FC = ({
@@ -175,7 +212,12 @@ export const Header: FC = ({
-
+
{
- const configApi = useApi(configApiRef);
- const providersConfig = configApi.getOptionalConfig('auth.providers');
- const providers = providersConfig?.keys() ?? [];
-
- return (
- <>
- {providers.includes('google') && (
-
- )}
- {providers.includes('microsoft') && (
-
- )}
- {providers.includes('github') && (
-
- )}
- {providers.includes('gitlab') && (
-
- )}
- {providers.includes('okta') && (
-
- )}
- {providers.includes('saml') && (
-
- )}
- {providers.includes('oauth2') && (
-
- )}
- >
- );
-};
diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx
deleted file mode 100644
index 71c4899862..0000000000
--- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx
+++ /dev/null
@@ -1,74 +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 from 'react';
-import {
- Card,
- CardContent,
- CardHeader,
- makeStyles,
- Divider,
-} from '@material-ui/core';
-import { AppSettingsList } from './AppSettingsList';
-import { AuthProvidersList } from './AuthProviderList';
-import { FeatureFlagsList } from './FeatureFlagsList';
-import { SignInAvatar } from './SignInAvatar';
-import { UserSettingsMenu } from './UserSettingsMenu';
-import { useUserProfile } from './useUserProfileInfo';
-import { useApi, featureFlagsApiRef } from '@backstage/core-api';
-
-const useStyles = makeStyles({
- root: {
- minWidth: 400,
- },
-});
-
-type Props = {
- providerSettings?: React.ReactNode;
-};
-
-export const SettingsDialog = ({ providerSettings }: Props) => {
- const classes = useStyles();
- const { profile, displayName } = useUserProfile();
- const featureFlagsApi = useApi(featureFlagsApiRef);
- const featureFlags = featureFlagsApi.getRegisteredFlags();
-
- return (
-
- }
- action={ }
- title={displayName}
- subheader={profile.email}
- />
-
-
- {providerSettings && (
- <>
-
-
- >
- )}
- {featureFlags.length > 0 && (
- <>
-
-
- >
- )}
-
-
- );
-};
diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx
deleted file mode 100644
index 85dbefff9b..0000000000
--- a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx
+++ /dev/null
@@ -1,77 +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, { useEffect, useContext } from 'react';
-import { Popover } from '@material-ui/core';
-import { SignInAvatar } from './SignInAvatar';
-import { SettingsDialog } from './SettingsDialog';
-import { SidebarItem } from '../Items';
-import { useUserProfile } from './useUserProfileInfo';
-import { SidebarContext } from '../config';
-
-type Props = {
- providerSettings?: React.ReactNode;
-};
-
-export const SidebarUserSettings = ({ providerSettings }: Props) => {
- const { isOpen: sidebarOpen } = useContext(SidebarContext);
- const { displayName } = useUserProfile();
- const [open, setOpen] = React.useState(false);
- const [anchorEl, setAnchorEl] = React.useState(
- undefined,
- );
-
- const handleOpen = (event?: React.MouseEvent) => {
- setAnchorEl(event?.currentTarget ?? undefined);
- setOpen(true);
- };
-
- const handleClose = () => {
- setAnchorEl(undefined);
- setOpen(false);
- };
-
- useEffect(() => {
- if (!sidebarOpen && open) setOpen(false);
- }, [open, sidebarOpen]);
-
- const SidebarAvatar = () => ;
-
- return (
- <>
-
-
-
-
- >
- );
-};
diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx
index 61975a728a..d6a01fea33 100644
--- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx
+++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx
@@ -22,17 +22,13 @@ import {
SidebarDivider,
SidebarSearchField,
SidebarSpace,
- SidebarUserSettings,
- ProviderSettingsItem,
} from '.';
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
-import Star from '@material-ui/icons/Star';
import { MemoryRouter } from 'react-router-dom';
-import { githubAuthApiRef } from '@backstage/core-api';
export default {
- title: 'Sidebar',
+ title: 'Layout/Sidebar',
component: Sidebar,
decorators: [
(storyFn: () => JSX.Element) => (
@@ -48,7 +44,6 @@ const handleSearch = (input: string) => {
export const SampleSidebar = () => (
- {/* */}
@@ -57,15 +52,5 @@ export const SampleSidebar = () => (
-
-
- }
- />
);
diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts
index a644c06e14..803306478d 100644
--- a/packages/core/src/layout/Sidebar/index.ts
+++ b/packages/core/src/layout/Sidebar/index.ts
@@ -31,5 +31,3 @@ export {
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
-export { DefaultProviderSettings } from './DefaultProviderSettings';
-export * from './Settings';
diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx
index 23c2ab5b49..f4af6dcf9f 100644
--- a/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx
+++ b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx
@@ -20,7 +20,7 @@ import { Grid } from '@material-ui/core';
const cardContentStyle = { height: 200, width: 500 };
export default {
- title: 'Tabbed Card',
+ title: 'Layout/Tabbed Card',
component: TabbedCard,
decorators: [
(storyFn: () => JSX.Element) => (
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index 46ccfa0b40..4ff0baf4fb 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
- "@backstage/cli-common": "^0.1.1-alpha.23",
+ "@backstage/cli-common": "^0.1.1-alpha.24",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"fs-extra": "^9.0.0",
diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts
index 28e3805ce8..681bdb44f3 100644
--- a/packages/create-app/src/createApp.ts
+++ b/packages/create-app/src/createApp.ts
@@ -148,6 +148,10 @@ export default async (cmd: Command): Promise => {
chalk.green(`🥇 Successfully created ${chalk.cyan(answers.name)}`),
);
Task.log();
+ Task.log(
+ 'See https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers',
+ );
+ Task.log();
Task.exit();
} catch (error) {
Task.error(error.message);
diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs
index f1278bd3c3..7175ea0e4f 100644
--- a/packages/create-app/templates/default-app/app-config.yaml.hbs
+++ b/packages/create-app/templates/default-app/app-config.yaml.hbs
@@ -22,22 +22,17 @@ backend:
client: pg
connection:
host:
- $secret:
- env: POSTGRES_HOST
+ $env: POSTGRES_HOST
port:
- $secret:
- env: POSTGRES_PORT
+ $env: POSTGRES_PORT
user:
- $secret:
- env: POSTGRES_USER
+ $env: POSTGRES_USER
password:
- $secret:
- env: POSTGRES_PASSWORD
+ $env: POSTGRES_PASSWORD
# https://node-postgres.com/features/ssl
#ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require)
#ca: # if you have a CA file and want to verify it you can uncomment this section
- # $secret:
- # file: /ca/server.crt
+ # $file: /ca/server.crt
{{/if}}
proxy:
@@ -55,13 +50,13 @@ lighthouse:
baseUrl: http://localhost:3003
auth:
+ # see https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers
providers: {}
scaffolder:
github:
token:
- $secret:
- env: GITHUB_ACCESS_TOKEN
+ $env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
catalog:
@@ -72,21 +67,19 @@ catalog:
providers:
- target: https://github.com
token:
- $secret:
- env: GITHUB_PRIVATE_TOKEN
+ $env: GITHUB_TOKEN
# Example for how to add your GitHub Enterprise instance:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
- # $secret:
- # env: GHE_PRIVATE_TOKEN
+ # $env: GHE_TOKEN
locations:
# Backstage example components
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
# Backstage example APIs
- - type: github
+ - type: url
target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml
# Backstage example templates
diff --git a/packages/create-app/templates/default-app/catalog-info.yaml.hbs b/packages/create-app/templates/default-app/catalog-info.yaml.hbs
new file mode 100644
index 0000000000..a370902020
--- /dev/null
+++ b/packages/create-app/templates/default-app/catalog-info.yaml.hbs
@@ -0,0 +1,13 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: {{name}}
+ description: An example of a Backstage application.
+ # Example for optional annotations
+ # annotations:
+ # github.com/project-slug: spotify/backstage
+ # backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
+spec:
+ type: website
+ owner: john@example.com
+ lifecycle: experimental
\ No newline at end of file
diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs
index a057826ba4..103e75f6e0 100644
--- a/packages/create-app/templates/default-app/packages/app/package.json.hbs
+++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs
@@ -19,6 +19,7 @@
"@backstage/plugin-lighthouse": "^{{version}}",
"@backstage/plugin-tech-radar": "^{{version}}",
"@backstage/plugin-github-actions": "^{{version}}",
+ "@backstage/plugin-user-settings": "^{{version}}",
"@backstage/test-utils": "^{{version}}",
"@backstage/theme": "^{{version}}",
"history": "^5.0.0",
diff --git a/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png b/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png
index 4660f988c1..eec0ae25b9 100644
Binary files a/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png and b/packages/create-app/templates/default-app/packages/app/public/android-chrome-192x192.png differ
diff --git a/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png b/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png
index 57c05cfc9a..3158830ac7 100644
Binary files a/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png and b/packages/create-app/templates/default-app/packages/app/public/apple-touch-icon.png differ
diff --git a/packages/create-app/templates/default-app/packages/app/public/safari-pinned-tab.svg b/packages/create-app/templates/default-app/packages/app/public/safari-pinned-tab.svg
index 3b0f666390..0f500b3002 100644
--- a/packages/create-app/templates/default-app/packages/app/public/safari-pinned-tab.svg
+++ b/packages/create-app/templates/default-app/packages/app/public/safari-pinned-tab.svg
@@ -1,28 +1 @@
-
-
-
-
-Created by potrace 1.11, written by Peter Selinger 2001-2013
-
-
-
-
-
-
+Created by potrace 1.11, written by Peter Selinger 2001-2013
\ No newline at end of file
diff --git a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
index 9dd9ea64c3..901347645e 100644
--- a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
@@ -9,6 +9,7 @@ import { Link, makeStyles } from '@material-ui/core';
import { NavLink } from 'react-router-dom';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
+import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import {
Sidebar,
@@ -17,8 +18,6 @@ import {
sidebarConfig,
SidebarContext,
SidebarSpace,
- SidebarUserSettings,
- DefaultProviderSettings,
} from '@backstage/core';
export const AppSidebar = () => (
@@ -37,7 +36,7 @@ export const AppSidebar = () => (
- } />
+
);
diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts
index 47ab2c5993..684db8aab8 100644
--- a/packages/create-app/templates/default-app/packages/backend/src/index.ts
+++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts
@@ -16,6 +16,7 @@ import {
useHotMemoize,
notFoundHandler,
SingleHostDiscovery,
+ UrlReaders,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
import auth from './plugins/auth';
@@ -27,9 +28,14 @@ import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
const config = ConfigReader.fromConfigs(loadedConfigs);
+ const root = getRootLogger();
+ const reader = UrlReaders.default({ logger: root, config });
+ const discovery = SingleHostDiscovery.fromConfig(config);
+
+ root.info(`Created UrlReader ${reader}`);
return (plugin: string): PluginEnvironment => {
- const logger = getRootLogger().child({ type: 'plugin', plugin });
+ const logger = root.child({ type: 'plugin', plugin });
const database = createDatabaseClient(
config.getConfig('backend.database'),
{
@@ -38,8 +44,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
},
},
);
- const discovery = SingleHostDiscovery.fromConfig(config);
- return { logger, database, config, discovery };
+ return { logger, database, config, reader, discovery };
};
}
diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts
index aa0538a6d8..345ac4c23d 100644
--- a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts
+++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts
@@ -13,9 +13,10 @@ import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
config,
+ reader,
database,
}: PluginEnvironment) {
- const locationReader = new LocationReaders({ logger, config });
+ const locationReader = new LocationReaders({ logger, reader, config });
const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
diff --git a/packages/create-app/templates/default-app/packages/backend/src/types.ts b/packages/create-app/templates/default-app/packages/backend/src/types.ts
index d145255390..aa003f63a5 100644
--- a/packages/create-app/templates/default-app/packages/backend/src/types.ts
+++ b/packages/create-app/templates/default-app/packages/backend/src/types.ts
@@ -1,11 +1,12 @@
import Knex from 'knex';
import { Logger } from 'winston';
import { Config } from '@backstage/config';
-import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { PluginEndpointDiscovery, UrlReader } from '@backstage/backend-common';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
config: Config;
+ reader: UrlReader
discovery: PluginEndpointDiscovery;
};
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index 54880cee21..05bab2f415 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
diff --git a/packages/docgen/package.json b/packages/docgen/package.json
index a60a4d1d70..99591f5869 100644
--- a/packages/docgen/package.json
+++ b/packages/docgen/package.json
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": true,
"homepage": "https://backstage.io",
"repository": {
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index fdd8d089c7..8dcfd98ab5 100644
--- a/packages/e2e-test/package.json
+++ b/packages/e2e-test/package.json
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": true,
"homepage": "https://backstage.io",
"repository": {
@@ -21,7 +21,7 @@
"test:e2e": "yarn start"
},
"devDependencies": {
- "@backstage/cli-common": "^0.1.1-alpha.23",
+ "@backstage/cli-common": "^0.1.1-alpha.24",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"fs-extra": "^9.0.0",
diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts
index c7f6137ff1..6fbdbf3c88 100644
--- a/packages/e2e-test/src/e2e-test.ts
+++ b/packages/e2e-test/src/e2e-test.ts
@@ -285,7 +285,7 @@ async function testAppServe(pluginName: string, appDir: string) {
cwd: appDir,
env: {
...process.env,
- GITHUB_ACCESS_TOKEN: 'abc',
+ GITHUB_TOKEN: 'abc',
},
});
Browser.localhost('localhost', 3000);
@@ -368,7 +368,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) {
cwd: appDir,
env: {
...process.env,
- GITHUB_ACCESS_TOKEN: 'abc',
+ GITHUB_TOKEN: 'abc',
},
});
diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js
index c83bb221be..2c58144acc 100644
--- a/packages/storybook/.storybook/main.js
+++ b/packages/storybook/.storybook/main.js
@@ -5,6 +5,7 @@ module.exports = {
stories: [
'../../core/src/layout/**/*.stories.tsx',
'../../core/src/components/**/*.stories.tsx',
+ '../../../plugins/**/src/**/*.stories.tsx',
],
addons: [
'@storybook/addon-actions',
diff --git a/packages/storybook/package.json b/packages/storybook/package.json
index 87d41d47d1..4cfc21be59 100644
--- a/packages/storybook/package.json
+++ b/packages/storybook/package.json
@@ -1,6 +1,6 @@
{
"name": "storybook",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
- "@backstage/theme": "^0.1.1-alpha.23"
+ "@backstage/theme": "^0.1.1-alpha.24"
},
"devDependencies": {
"@storybook/addon-actions": "^6.0.21",
diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json
index 6b31b4aae7..77940d8299 100644
--- a/packages/techdocs-cli/package.json
+++ b/packages/techdocs-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public"
@@ -44,7 +44,7 @@
"ext": "ts"
},
"dependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"commander": "^6.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
diff --git a/packages/techdocs-cli/src/index.ts b/packages/techdocs-cli/src/index.ts
index b48f03d35b..da196d10b8 100644
--- a/packages/techdocs-cli/src/index.ts
+++ b/packages/techdocs-cli/src/index.ts
@@ -20,11 +20,7 @@ import path from 'path';
import HTTPServer from './lib/httpServer';
import openBrowser from 'react-dev-utils/openBrowser';
-const run = (
- workingDirectory: string,
- name: string,
- args: string[] = [],
-): ChildProcess => {
+const run = (name: string, args: string[] = []): ChildProcess => {
const [stdin, stdout, stderr] = [
'inherit' as const,
'pipe' as const,
@@ -32,7 +28,6 @@ const run = (
];
const childProcess = spawn(name, args, {
- cwd: workingDirectory,
stdio: [stdin, stdout, stderr],
shell: true,
env: {
@@ -59,13 +54,13 @@ const runMkdocsServer = (options?: {
const devAddr = options?.devAddr ?? '0.0.0.0:8000';
return new Promise(resolve => {
- const childProcess = run(process.env.PWD!, 'docker', [
+ const childProcess = run('docker', [
'run',
'-it',
'-w',
'/content',
'-v',
- '$(pwd):/content',
+ `${process.cwd()}:/content`,
'-p',
'8000:8000',
'spotify/techdocs',
diff --git a/packages/techdocs-container/README.md b/packages/techdocs-container/README.md
index aa1bf15974..cc92071f5d 100644
--- a/packages/techdocs-container/README.md
+++ b/packages/techdocs-container/README.md
@@ -19,3 +19,13 @@ docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs:loca
```
Then open up `http://localhost:8000` on your local machine.
+
+## Publishing
+
+This container is published on DockerHub - https://hub.docker.com/r/spotify/techdocs
+
+The publishing is configured by [Automated Builds](https://hub.docker.com/repository/docker/spotify/techdocs/builds/edit) feature on Docker Hub which is triggered from GitHub (on new commits and releases). @spotify/techdocs-core team has access to the settings.
+
+The `latest` tag on Docker Hub points to the recent commits in the `master` branch. The [version tags](https://hub.docker.com/r/spotify/techdocs/tags) (e.g. v0.1.1-alpha.24) point to the GitHub tags created from [releases](https://github.com/spotify/backstage/releases) of this GitHub repository.
+
+Note: We recommend using a specific version of the container instead of `latest` release for stability and avoiding unexpected changes.
diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json
index e30a1e9409..ffdcdc02fb 100644
--- a/packages/test-utils-core/package.json
+++ b/packages/test-utils-core/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 2ca946e028..efe5cafaed 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/core-api": "^0.1.1-alpha.23",
- "@backstage/test-utils-core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/core-api": "^0.1.1-alpha.24",
+ "@backstage/test-utils-core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
diff --git a/packages/theme/package.json b/packages/theme/package.json
index bd8705933f..e9f3a346ce 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23"
+ "@backstage/cli": "^0.1.1-alpha.24"
},
"files": [
"dist"
diff --git a/plugins/api-docs/docs/api_list.png b/plugins/api-docs/docs/api_list.png
index abc948b3c6..4ca2f09542 100644
Binary files a/plugins/api-docs/docs/api_list.png and b/plugins/api-docs/docs/api_list.png differ
diff --git a/plugins/api-docs/docs/entity_tab_api.png b/plugins/api-docs/docs/entity_tab_api.png
index 1ab9de02cd..439fac73fa 100644
Binary files a/plugins/api-docs/docs/entity_tab_api.png and b/plugins/api-docs/docs/entity_tab_api.png differ
diff --git a/plugins/api-docs/docs/openapi_definition.png b/plugins/api-docs/docs/openapi_definition.png
index af04946b19..49d12a08e8 100644
Binary files a/plugins/api-docs/docs/openapi_definition.png and b/plugins/api-docs/docs/openapi_definition.png differ
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index f47a5e0ef4..a703b6afe5 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@kyma-project/asyncapi-react": "^0.13.1",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
@@ -39,9 +39,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index d403580f86..98873f88c7 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/config-loader": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/config-loader": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -30,7 +30,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/supertest": "^2.0.8",
"msw": "^0.20.5",
"supertest": "^4.0.2"
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index d8fe00ff31..fca8fb7178 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
@@ -51,7 +51,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js
new file mode 100644
index 0000000000..26cc98e74e
--- /dev/null
+++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table.text('full_name').nullable();
+ });
+
+ await knex('entities').update({
+ full_name: knex.raw(
+ "LOWER(kind) || ':' || LOWER(COALESCE(namespace, 'default')) || '/' || LOWER(name)",
+ ),
+ });
+
+ try {
+ await knex.schema.alterTable('entities', table => {
+ table.text('full_name').notNullable().alter();
+ });
+ } catch (e) {
+ // SQLite does not support alter column, ignore
+ }
+
+ await knex.schema.alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.unique(['full_name'], 'entities_unique_full_name');
+ table.dropUnique([], 'entities_unique_name');
+ });
+};
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
+ table.dropUnique([], 'entities_unique_full_name');
+ table.unique(['kind', 'namespace', 'name'], 'entities_unique_name');
+ });
+
+ await knex.schema.alterTable('entities_search', table => {
+ table.dropColumn('full_name');
+ });
+};
diff --git a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js
new file mode 100644
index 0000000000..fdabf72fce
--- /dev/null
+++ b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .text('data')
+ .nullable()
+ .comment('The entire JSON data blob of the entity');
+ });
+
+ await knex('entities').update({
+ // apiVersion and kind should not contain any JSON unsafe chars, and both
+ // metadata and spec are already valid serialized JSON
+ data: knex.raw(
+ `'{"apiVersion":"' || api_version || '","kind":"' || kind || '","metadata":' || metadata || COALESCE(',"spec":' || spec, '') || '}'`,
+ ),
+ });
+
+ await knex.schema.alterTable('entities', table => {
+ table.dropColumn('metadata');
+ table.dropColumn('spec');
+ });
+
+ // SQLite does not support ALTER COLUMN. Note that we do not use the try/
+ // catch method as in other migrations, because if the transaction is
+ // partially failed, it will further mess up the already messed-up
+ // statement below this.
+ if (knex.client.config.client !== 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.text('data').notNullable().alter();
+ });
+ }
+
+ // NOTE(freben): For some reason, specifically sqlite3 in-mem just drops some
+ // subset of constraints sometimes, when a table column is dropped - even if
+ // the column had no relation at all to the constraint. We therefore recreate
+ // the constraint here as a stupid fix.
+ if (knex.client.config.client === 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.unique(['full_name'], 'entities_unique_full_name');
+ });
+ }
+};
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .text('metadata')
+ .notNullable()
+ .comment('The entire metadata JSON blob of the entity');
+ table
+ .text('spec')
+ .nullable()
+ .comment('The entire spec JSON blob of the entity');
+ table.dropColumn('data');
+ });
+};
diff --git a/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js b/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js
new file mode 100644
index 0000000000..7bacc67338
--- /dev/null
+++ b/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.up = async function up(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table.dropColumn('api_version');
+ table.dropColumn('kind');
+ table.dropColumn('name');
+ table.dropColumn('namespace');
+ });
+
+ // NOTE(freben): For some reason, specifically sqlite3 in-mem just drops some
+ // subset of constraints sometimes, when a table column is dropped - even if
+ // the column had no relation at all to the constraint. We therefore recreate
+ // the constraint here as a stupid fix.
+ if (knex.client.config.client === 'sqlite3') {
+ await knex.schema.alterTable('entities', table => {
+ table.unique(['full_name'], 'entities_unique_full_name');
+ });
+ }
+};
+
+/**
+ * @param {import('knex')} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('entities', table => {
+ table
+ .string('api_version')
+ .notNullable()
+ .comment('The apiVersion field of the entity');
+ table.string('kind').notNullable().comment('The kind field of the entity');
+ table
+ .string('name')
+ .nullable()
+ .comment('The metadata.name field of the entity');
+ table
+ .string('namespace')
+ .nullable()
+ .comment('The metadata.namespace field of the entity');
+ });
+};
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 4eb437b871..f16f629c44 100644
--- a/plugins/catalog-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@octokit/graphql": "^4.5.6",
"@types/express": "^4.17.6",
"@types/node-fetch": "^2.5.7",
@@ -33,6 +33,7 @@
"fs-extra": "^9.0.0",
"git-url-parse": "^11.2.0",
"knex": "^0.21.1",
+ "ldapjs": "^2.2.0",
"lodash": "^4.17.15",
"morgan": "^1.10.0",
"node-fetch": "^2.6.0",
@@ -44,9 +45,10 @@
"yup": "^0.29.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
+ "@types/ldapjs": "^1.0.9",
"@types/lodash": "^4.14.151",
"@types/supertest": "^2.0.8",
"@types/uuid": "^8.0.0",
diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
index f6aec0c268..750d728c67 100644
--- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
@@ -90,7 +90,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
const colocatedEntities = location
? await this.database.entities(tx, [
{
- key: LOCATION_ANNOTATION,
+ key: `metadata.annotations.${LOCATION_ANNOTATION}`,
values: [location],
},
])
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
index 8b8067f641..2bacec4026 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
@@ -124,7 +124,7 @@ describe('CommonDatabase', () => {
{
...output,
status: DatabaseLocationUpdateLogStatus.FAIL,
- timestamp: expect.any(String),
+ timestamp: expect.anything(),
},
]),
);
@@ -149,24 +149,6 @@ describe('CommonDatabase', () => {
).rejects.toThrow(ConflictError);
});
- it('rejects adding the almost-same-kind entity twice', async () => {
- entityRequest.entity.kind = 'some-kind';
- await db.transaction(tx => db.addEntity(tx, entityRequest));
- entityRequest.entity.kind = 'SomeKind';
- await expect(
- db.transaction(tx => db.addEntity(tx, entityRequest)),
- ).rejects.toThrow(ConflictError);
- });
-
- it('rejects adding the almost-same-named entity twice', async () => {
- entityRequest.entity.metadata.name = 'some-name';
- await db.transaction(tx => db.addEntity(tx, entityRequest));
- entityRequest.entity.metadata.name = 'SomeName';
- await expect(
- db.transaction(tx => db.addEntity(tx, entityRequest)),
- ).rejects.toThrow(ConflictError);
- });
-
it('rejects adding the almost-same-namespace entity twice', async () => {
entityRequest.entity.metadata.namespace = undefined;
await db.transaction(tx => db.addEntity(tx, entityRequest));
@@ -209,24 +191,26 @@ describe('CommonDatabase', () => {
const result = await db.locationHistory(
'dd12620d-0436-422f-93bd-929aa0788123',
);
- expect(result).toEqual([
- {
- created_at: expect.anything(),
- entity_name: null,
- id: expect.anything(),
- location_id: 'dd12620d-0436-422f-93bd-929aa0788123',
- message: null,
- status: DatabaseLocationUpdateLogStatus.SUCCESS,
- },
- {
- created_at: expect.anything(),
- entity_name: null,
- id: expect.anything(),
- location_id: 'dd12620d-0436-422f-93bd-929aa0788123',
- message: 'Something went wrong',
- status: DatabaseLocationUpdateLogStatus.FAIL,
- },
- ]);
+ expect(result).toEqual(
+ expect.arrayContaining([
+ {
+ created_at: expect.anything(),
+ entity_name: null,
+ id: expect.anything(),
+ location_id: 'dd12620d-0436-422f-93bd-929aa0788123',
+ message: null,
+ status: DatabaseLocationUpdateLogStatus.SUCCESS,
+ },
+ {
+ created_at: expect.anything(),
+ entity_name: null,
+ id: expect.anything(),
+ location_id: 'dd12620d-0436-422f-93bd-929aa0788123',
+ message: 'Something went wrong',
+ status: DatabaseLocationUpdateLogStatus.FAIL,
+ },
+ ]),
+ );
});
});
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index 9ac1fff951..890a64781a 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -21,13 +21,11 @@ import {
} from '@backstage/backend-common';
import {
Entity,
- EntityMeta,
EntityName,
ENTITY_DEFAULT_NAMESPACE,
ENTITY_META_GENERATED_FIELDS,
generateEntityEtag,
generateEntityUid,
- getEntityName,
Location,
} from '@backstage/catalog-model';
import Knex from 'knex';
@@ -53,7 +51,6 @@ import type {
export class CommonDatabase implements Database {
constructor(
private readonly database: Knex,
- private readonly normalize: (value: string) => string,
private readonly logger: Logger,
) {}
@@ -88,8 +85,6 @@ export class CommonDatabase implements Database {
throw new InputError('May not specify generation for new entities');
}
- await this.ensureNoSimilarNames(tx, request.entity);
-
const newEntity = lodash.cloneDeep(request.entity);
newEntity.metadata = {
...newEntity.metadata,
@@ -147,8 +142,6 @@ export class CommonDatabase implements Database {
}
}
- await this.ensureNoSimilarNames(tx, request.entity);
-
// Store the updated entity; select on the old etag to ensure that we do
// not lose to another writer
const newRow = this.toEntityRow(request.locationId, request.entity);
@@ -175,7 +168,7 @@ export class CommonDatabase implements Database {
let builder = tx('entities');
for (const [indexU, filter] of (filters ?? []).entries()) {
const index = Number(indexU);
- const key = filter.key.toLowerCase().replace('*', '%');
+ const key = filter.key.toLowerCase().replace(/\*/g, '%');
const keyOp = filter.key.includes('*') ? 'like' : '=';
let matchNulls = false;
@@ -186,7 +179,7 @@ export class CommonDatabase implements Database {
if (!value) {
matchNulls = true;
} else if (value.includes('*')) {
- matchLike.push(value.toLowerCase().replace('*', '%'));
+ matchLike.push(value.toLowerCase().replace(/\*/g, '%'));
} else {
matchIn.push(value.toLowerCase());
}
@@ -214,9 +207,7 @@ export class CommonDatabase implements Database {
const rows = await builder
.select('entities.*')
- .orderBy('kind', 'asc')
- .orderBy('namespace', 'asc')
- .orderBy('name', 'asc')
+ .orderBy('full_name', 'asc')
.groupBy('id');
return rows.map(row => this.toEntityResponse(row));
@@ -229,12 +220,9 @@ export class CommonDatabase implements Database {
const tx = txOpaque as Knex.Transaction;
const rows = await tx('entities')
- .whereRaw(
- tx.raw(
- 'LOWER(kind) = LOWER(?) AND LOWER(namespace) = LOWER(?) AND LOWER(name) = LOWER(?)',
- [name.kind, name.namespace, name.name],
- ),
- )
+ .where({
+ full_name: `${name.kind}:${name.namespace}/${name.name}`.toLowerCase(),
+ })
.select();
if (rows.length !== 1) {
@@ -385,88 +373,36 @@ export class CommonDatabase implements Database {
}
}
- private async ensureNoSimilarNames(
- tx: Knex.Transaction,
- data: Entity,
- ): Promise {
- const {
- kind: newKind,
- namespace: newNamespace,
- name: newName,
- } = getEntityName(data);
- const newKindNorm = this.normalize(newKind);
- const newNamespaceNorm = this.normalize(newNamespace);
- const newNameNorm = this.normalize(newName);
-
- for (const item of await this.entities(tx)) {
- if (data.metadata.uid === item.entity.metadata.uid) {
- continue;
- }
-
- const {
- kind: oldKind,
- namespace: oldNamespace,
- name: oldName,
- } = getEntityName(item.entity);
- const oldKindNorm = this.normalize(oldKind);
- const oldNamespaceNorm = this.normalize(oldNamespace);
- const oldNameNorm = this.normalize(oldName);
-
- if (
- oldKindNorm === newKindNorm &&
- oldNamespaceNorm === newNamespaceNorm &&
- oldNameNorm === newNameNorm
- ) {
- // Only throw if things were actually different - for completely equal
- // things, we let the database handle the conflict
- if (
- oldKind !== newKind ||
- oldNamespace !== newNamespace ||
- oldName !== newName
- ) {
- const message = `Kind, namespace, name are too similar to an existing entity`;
- throw new ConflictError(message);
- }
- }
- }
- }
-
private toEntityRow(
locationId: string | undefined,
entity: Entity,
): DbEntitiesRow {
+ const lowerKind = entity.kind.toLowerCase();
+ const lowerNamespace = (
+ entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
+ ).toLowerCase();
+ const lowerName = entity.metadata.name.toLowerCase();
+
+ const data = {
+ ...entity,
+ metadata: lodash.omit(entity.metadata, ...ENTITY_META_GENERATED_FIELDS),
+ };
+
return {
id: entity.metadata.uid!,
location_id: locationId || null,
etag: entity.metadata.etag!,
generation: entity.metadata.generation!,
- api_version: entity.apiVersion,
- kind: entity.kind,
- name: entity.metadata.name,
- namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE,
- metadata: JSON.stringify(
- lodash.omit(entity.metadata, ...ENTITY_META_GENERATED_FIELDS),
- ),
- spec: entity.spec ? JSON.stringify(entity.spec) : null,
+ full_name: `${lowerKind}:${lowerNamespace}/${lowerName}`,
+ data: JSON.stringify(data),
};
}
private toEntityResponse(row: DbEntitiesRow): DbEntityResponse {
- const entity: Entity = {
- apiVersion: row.api_version,
- kind: row.kind,
- metadata: {
- ...(JSON.parse(row.metadata) as EntityMeta),
- uid: row.id,
- etag: row.etag,
- generation: Number(row.generation), // cast because of sqlite
- },
- };
-
- if (row.spec) {
- const spec = JSON.parse(row.spec);
- entity.spec = spec;
- }
+ const entity = JSON.parse(row.data) as Entity;
+ entity.metadata.uid = row.id;
+ entity.metadata.etag = row.etag;
+ entity.metadata.generation = Number(row.generation); // cast due to sqlite
return {
locationId: row.location_id || undefined,
diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts
index ce91cc737f..eb307c9478 100644
--- a/plugins/catalog-backend/src/database/DatabaseManager.ts
+++ b/plugins/catalog-backend/src/database/DatabaseManager.ts
@@ -15,11 +15,11 @@
*/
import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
-import { makeValidator } from '@backstage/catalog-model';
import Knex from 'knex';
import { Logger } from 'winston';
import { CommonDatabase } from './CommonDatabase';
import { Database } from './types';
+import { v4 as uuidv4 } from 'uuid';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-catalog-backend',
@@ -28,12 +28,10 @@ const migrationsDir = resolvePackagePath(
export type CreateDatabaseOptions = {
logger: Logger;
- fieldNormalizer: (value: string) => string;
};
const defaultOptions: CreateDatabaseOptions = {
logger: getVoidLogger(),
- fieldNormalizer: makeValidator().normalizeEntityName,
};
export class DatabaseManager {
@@ -44,8 +42,8 @@ export class DatabaseManager {
await knex.migrate.latest({
directory: migrationsDir,
});
- const { logger, fieldNormalizer } = { ...defaultOptions, ...options };
- return new CommonDatabase(knex, fieldNormalizer, logger);
+ const { logger } = { ...defaultOptions, ...options };
+ return new CommonDatabase(knex, logger);
}
public static async createInMemoryDatabase(
@@ -63,18 +61,41 @@ export class DatabaseManager {
}
public static async createTestDatabase(): Promise {
- const knex = Knex({
+ const config: Knex.Config = {
+ /*
+ client: 'pg',
+ connection: {
+ host: 'localhost',
+ user: 'postgres',
+ password: 'postgres',
+ },
+ */
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
- });
+ };
+
+ let knex = Knex(config);
+ if (typeof config.connection !== 'string') {
+ const tempDbName = `d${uuidv4().replace(/-/g, '')}`;
+ await knex.raw(`CREATE DATABASE ${tempDbName};`);
+ knex = Knex({
+ ...config,
+ connection: {
+ ...config.connection,
+ database: tempDbName,
+ },
+ });
+ }
+
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
await knex.migrate.latest({
directory: migrationsDir,
});
- const { logger, fieldNormalizer } = defaultOptions;
- return new CommonDatabase(knex, fieldNormalizer, logger);
+
+ const { logger } = defaultOptions;
+ return new CommonDatabase(knex, logger);
}
}
diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts
index 6da87f9150..fad17da733 100644
--- a/plugins/catalog-backend/src/database/search.test.ts
+++ b/plugins/catalog-backend/src/database/search.test.ts
@@ -125,46 +125,7 @@ describe('search', () => {
},
{ entity_id: 'eid', key: 'apiversion', value: 'a' },
{ entity_id: 'eid', key: 'kind', value: 'b' },
- { entity_id: 'eid', key: 'name', value: 'n' },
- { entity_id: 'eid', key: 'namespace', value: null },
- { entity_id: 'eid', key: 'uid', value: null },
- { entity_id: 'eid', key: 'namespace', value: ENTITY_DEFAULT_NAMESPACE },
]);
});
-
- it('adds prefix-stripped versions', () => {
- const input: Entity = {
- apiVersion: 'a',
- kind: 'b',
- metadata: {
- name: 'name',
- labels: {
- lbl: 'lbl',
- },
- annotations: {
- ann: 'ann',
- },
- },
- spec: {
- sub: {
- spc: 'spc',
- },
- },
- };
- expect(buildEntitySearch('eid', input)).toStrictEqual(
- expect.arrayContaining([
- { entity_id: 'eid', key: 'metadata.name', value: 'name' },
- { entity_id: 'eid', key: 'name', value: 'name' },
- { entity_id: 'eid', key: 'metadata.labels.lbl', value: 'lbl' },
- { entity_id: 'eid', key: 'labels.lbl', value: 'lbl' },
- { entity_id: 'eid', key: 'lbl', value: 'lbl' },
- { entity_id: 'eid', key: 'metadata.annotations.ann', value: 'ann' },
- { entity_id: 'eid', key: 'annotations.ann', value: 'ann' },
- { entity_id: 'eid', key: 'ann', value: 'ann' },
- { entity_id: 'eid', key: 'spec.sub.spc', value: 'spc' },
- { entity_id: 'eid', key: 'sub.spc', value: 'spc' },
- ]),
- );
- });
});
});
diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts
index cbcd01a5d0..bbed2fac47 100644
--- a/plugins/catalog-backend/src/database/search.ts
+++ b/plugins/catalog-backend/src/database/search.ts
@@ -17,15 +17,6 @@
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import type { DbEntitiesSearchRow } from './types';
-// Search entries that start with these prefixes, also get a shorthand without
-// that prefix
-const SHORTHAND_KEY_PREFIXES = [
- 'metadata.',
- 'metadata.labels.',
- 'metadata.annotations.',
- 'spec.',
-];
-
// These are excluded in the generic loop, either because they do not make sense
// to index, or because they are special-case always inserted whether they are
// null or not
@@ -159,14 +150,5 @@ export function buildEntitySearch(
// Visit the entire structure recursively
visitEntityPart(entityId, '', entity, result);
- // Generate shorthands for fields directly under some common collections
- for (const row of result.slice()) {
- for (const stripPrefix of SHORTHAND_KEY_PREFIXES) {
- if (row.key.startsWith(stripPrefix)) {
- result.push({ ...row, key: row.key.substr(stripPrefix.length) });
- }
- }
- }
-
return result;
}
diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts
index 8aaf212583..5a577f2223 100644
--- a/plugins/catalog-backend/src/database/types.ts
+++ b/plugins/catalog-backend/src/database/types.ts
@@ -19,14 +19,10 @@ import type { Entity, EntityName, Location } from '@backstage/catalog-model';
export type DbEntitiesRow = {
id: string;
location_id: string | null;
- api_version: string;
- kind: string;
- name: string | null;
- namespace: string | null;
etag: string;
generation: number;
- metadata: string;
- spec: string | null;
+ full_name: string;
+ data: string;
};
export type DbEntityRequest = {
diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts
index e964524bc7..e82699f526 100644
--- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts
+++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts
@@ -74,11 +74,11 @@ export class CatalogRulesEnforcer {
* - allow: [Component, API]
*
* locations:
- * - type: github
+ * - type: url
* target: https://github.com/org/repo/blob/master/users.yaml
* rules:
* - allow: [User, Group]
- * - type: github
+ * - type: url
* target: https://github.com/org/repo/blob/master/systems.yaml
* rules:
* - allow: [System]
diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
index 1b60666c8e..7232b876c7 100644
--- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
+++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
@@ -21,6 +21,7 @@ import {
getEntityName,
Location,
LocationSpec,
+ serializeEntityRef,
} from '@backstage/catalog-model';
import { v4 as uuidv4 } from 'uuid';
import { Logger } from 'winston';
@@ -159,28 +160,23 @@ export class HigherOrderOperations implements HigherOrderOperation {
);
}
+ this.logger.info(
+ `Read ${readerOutput.entities.length} entities from location ${location.type} ${location.target}`,
+ );
+
+ const startTimestamp = process.hrtime();
for (const item of readerOutput.entities) {
const { entity } = item;
- this.logger.debug(
- `Read entity kind="${entity.kind}" namespace="${
- entity.metadata.namespace || ''
- }" name="${entity.metadata.name}"`,
- );
-
try {
const previous = await this.entitiesCatalog.entityByName(
getEntityName(entity),
);
if (!previous) {
- this.logger.debug(`No such entity found, adding`);
await this.entitiesCatalog.addOrUpdateEntity(entity, location.id);
} else if (entityHasChanges(previous, entity)) {
- this.logger.debug(`Different from existing entity, updating`);
await this.entitiesCatalog.addOrUpdateEntity(entity, location.id);
- } else {
- this.logger.debug(`Equal to existing entity, skipping update`);
}
await this.locationsCatalog.logUpdateSuccess(
@@ -188,10 +184,8 @@ export class HigherOrderOperations implements HigherOrderOperation {
entity.metadata.name,
);
} catch (error) {
- this.logger.debug(
- `Failed refresh of entity kind="${entity.kind}" name="${
- entity.metadata.name
- }" namespace="${entity.metadata.namespace || ''}", ${error}`,
+ this.logger.info(
+ `Failed refresh of entity ${serializeEntityRef(entity)}, ${error}`,
);
await this.locationsCatalog.logUpdateFailure(
@@ -201,5 +195,11 @@ export class HigherOrderOperations implements HigherOrderOperation {
);
}
}
+
+ const delta = process.hrtime(startTimestamp);
+ const durationMs = ((delta[0] * 1e9 + delta[1]) / 1e9).toFixed(1);
+ this.logger.info(
+ `Wrote ${readerOutput.entities.length} entities from location ${location.type} ${location.target} in ${durationMs} seconds`,
+ );
}
}
diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts
index 00ee95f3a5..87c75e21e5 100644
--- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts
+++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { getVoidLogger } from '@backstage/backend-common';
+import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import {
Entity,
EntityPolicies,
@@ -26,18 +26,18 @@ import { Config, ConfigReader } from '@backstage/config';
import { Logger } from 'winston';
import { CatalogRulesEnforcer } from './CatalogRules';
import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEntityProcessor';
-import { ApiDefinitionAtLocationProcessor } from './processors/ApiDefinitionAtLocationProcessor';
import { AzureApiReaderProcessor } from './processors/AzureApiReaderProcessor';
import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor';
+import { CodeOwnersProcessor } from './processors/CodeOwnersProcessor';
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
import { FileReaderProcessor } from './processors/FileReaderProcessor';
import { GithubOrgReaderProcessor } from './processors/GithubOrgReaderProcessor';
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor';
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
+import { LdapOrgReaderProcessor } from './processors/LdapOrgReaderProcessor';
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
import { PlaceholderProcessor } from './processors/PlaceholderProcessor';
-import { CodeOwnersProcessor } from './processors/CodeOwnersProcessor';
import * as result from './processors/results';
import { StaticLocationProcessor } from './processors/StaticLocationProcessor';
import {
@@ -57,6 +57,7 @@ import { LocationReader, ReadLocationResult } from './types';
const MAX_DEPTH = 10;
type Options = {
+ reader: UrlReader;
logger?: Logger;
config?: Config;
processors?: LocationProcessor[];
@@ -72,6 +73,7 @@ export class LocationReaders implements LocationReader {
static defaultProcessors(options: {
logger: Logger;
+ reader: UrlReader;
config?: Config;
entityPolicy?: EntityPolicy;
}): LocationProcessor[] {
@@ -80,20 +82,50 @@ export class LocationReaders implements LocationReader {
config = new ConfigReader({}, 'missing-config'),
entityPolicy = new EntityPolicies(),
} = options;
+
+ // TODO(Rugvip): These are added for backwards compatibility if config exists
+ // The idea is to have everyone migrate from using the old processors to the new
+ // integration config driven UrlReaders. In an upcoming release we can then completely
+ // remove support for the old processors, but still keep handling the deprecated location
+ // types for a while, but with a warning.
+ const oldProcessors = [];
+ const pc = config.getOptionalConfig('catalog.processors');
+ if (pc?.has('github')) {
+ logger.warn(
+ `Using deprecated configuration for catalog.processors.github, move to using integrations.github instead`,
+ );
+ oldProcessors.push(GithubReaderProcessor.fromConfig(config, logger));
+ }
+ if (pc?.has('gitlabApi')) {
+ logger.warn(
+ `Using deprecated configuration for catalog.processors.gitlabApi, move to using integrations.gitlab instead`,
+ );
+ oldProcessors.push(new GitlabApiReaderProcessor(config));
+ oldProcessors.push(new GitlabReaderProcessor());
+ }
+ if (pc?.has('bitbucketApi')) {
+ logger.warn(
+ `Using deprecated configuration for catalog.processors.bitbucketApi, move to using integrations.bitbucket instead`,
+ );
+ oldProcessors.push(new BitbucketApiReaderProcessor(config));
+ }
+ if (pc?.has('azureApi')) {
+ logger.warn(
+ `Using deprecated configuration for catalog.processors.azureApi, move to using integrations.azure instead`,
+ );
+ oldProcessors.push(new AzureApiReaderProcessor(config));
+ }
+
return [
StaticLocationProcessor.fromConfig(config),
new FileReaderProcessor(),
- GithubReaderProcessor.fromConfig(config, logger),
- new GitlabApiReaderProcessor(config),
- new GitlabReaderProcessor(),
- new BitbucketApiReaderProcessor(config),
- new AzureApiReaderProcessor(config),
- GithubOrgReaderProcessor.fromConfig(config),
- new UrlReaderProcessor(),
+ ...oldProcessors,
+ GithubOrgReaderProcessor.fromConfig(config, { logger }),
+ LdapOrgReaderProcessor.fromConfig(config, { logger }),
+ new UrlReaderProcessor(options),
new YamlProcessor(),
- PlaceholderProcessor.default(),
- new CodeOwnersProcessor(),
- new ApiDefinitionAtLocationProcessor(),
+ PlaceholderProcessor.default({ reader: options.reader }),
+ new CodeOwnersProcessor({ reader: options.reader }),
new EntityPolicyProcessor(entityPolicy),
new LocationRefProcessor(),
new AnnotateLocationEntityProcessor(),
@@ -103,7 +135,8 @@ export class LocationReaders implements LocationReader {
constructor({
logger = getVoidLogger(),
config,
- processors = LocationReaders.defaultProcessors({ logger, config }),
+ reader,
+ processors = LocationReaders.defaultProcessors({ logger, reader, config }),
}: Options) {
this.logger = logger;
this.processors = processors;
@@ -166,10 +199,6 @@ export class LocationReaders implements LocationReader {
item: LocationProcessorLocationResult,
emit: LocationProcessorEmit,
) {
- this.logger.debug(
- `Reading location ${item.location.type} ${item.location.target} optional=${item.optional}`,
- );
-
for (const processor of this.processors) {
if (processor.readLocation) {
try {
@@ -195,10 +224,6 @@ export class LocationReaders implements LocationReader {
item: LocationProcessorDataResult,
emit: LocationProcessorEmit,
) {
- this.logger.debug(
- `Parsing data from location ${item.location.type} ${item.location.target} (${item.data.byteLength} bytes)`,
- );
-
for (const processor of this.processors) {
if (processor.parseData) {
try {
@@ -221,21 +246,12 @@ export class LocationReaders implements LocationReader {
item: LocationProcessorEntityResult,
emit: LocationProcessorEmit,
): Promise {
- this.logger.debug(
- `Got entity at location ${item.location.type} ${item.location.target}, ${item.entity.apiVersion} ${item.entity.kind}`,
- );
-
let current = item.entity;
for (const processor of this.processors) {
if (processor.processEntity) {
try {
- current = await processor.processEntity(
- current,
- item.location,
- emit,
- this.readLocation.bind(this),
- );
+ current = await processor.processEntity(current, item.location, emit);
} catch (e) {
// Construct the name carefully, if we got validation errors we do
// not want to crash here due to missing metadata or so
@@ -273,40 +289,4 @@ export class LocationReaders implements LocationReader {
}
}
}
-
- private async readLocation(location: LocationSpec): Promise {
- let data: Buffer | undefined = undefined;
- let error: Error | undefined = undefined;
-
- await this.handleLocation(
- {
- type: 'location',
- location,
- optional: false,
- },
- output => {
- if (output.type === 'error' && !error) {
- error = output.error;
- } else if (output.type === 'data') {
- if (data) {
- if (!error) {
- error = new Error(
- 'More than one piece of data loaded unexpectedly',
- );
- }
- } else {
- data = output.data;
- }
- }
- },
- );
-
- if (error) {
- throw error;
- } else if (!data) {
- throw new Error('No data loaded');
- }
-
- return data;
- }
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts
deleted file mode 100644
index 6ae6562339..0000000000
--- a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { ApiEntity, Entity, LocationSpec } from '@backstage/catalog-model';
-/*
- * 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 { ApiDefinitionAtLocationProcessor } from './ApiDefinitionAtLocationProcessor';
-
-describe('ApiDefinitionAtLocationProcessor', () => {
- let processor: ApiDefinitionAtLocationProcessor;
- let entity: Entity;
- let location: LocationSpec;
-
- beforeEach(() => {
- processor = new ApiDefinitionAtLocationProcessor();
- entity = {
- apiVersion: 'backstage.io/v1alpha1',
- kind: 'API',
- metadata: {
- name: 'test',
- },
- spec: {
- lifecycle: 'production',
- owner: 'info@example.com',
- type: 'openapi',
- definition: 'Hello',
- },
- };
- location = {
- type: 'url',
- target: `http://example.com/api.yaml`,
- };
- });
-
- it('should skip entities without annotation', async () => {
- const read = jest.fn().mockRejectedValue(new Error('boo'));
-
- const generated = (await processor.processEntity(
- entity,
- location,
- () => {},
- read,
- )) as ApiEntity;
-
- expect(generated.spec.definition).toBe('Hello');
- });
-
- it('should load from location', async () => {
- entity.metadata.annotations = {
- 'backstage.io/definition-at-location':
- 'url:http://example.com/openapi.yaml',
- };
-
- const read = jest.fn().mockResolvedValue(Buffer.from('Hello'));
-
- const generated = (await processor.processEntity(
- entity,
- location,
- () => {},
- read,
- )) as ApiEntity;
-
- expect(generated.spec.definition).toBe('Hello');
- expect(read.mock.calls[0][0]).toStrictEqual({
- type: 'url',
- target: 'http://example.com/openapi.yaml',
- });
- });
-
- it('should throw errors while loading', async () => {
- entity.metadata.annotations = {
- 'backstage.io/definition-at-location': 'missing',
- };
-
- const read = jest
- .fn()
- .mockRejectedValue(new Error('Failed to load location'));
-
- await expect(
- processor.processEntity(entity, location, () => {}, read),
- ).rejects.toThrow('Failed to load location');
- });
-});
diff --git a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts
deleted file mode 100644
index e64d112e9c..0000000000
--- a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts
+++ /dev/null
@@ -1,59 +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 { ApiEntity, Entity, LocationSpec } from '@backstage/catalog-model';
-import {
- LocationProcessor,
- LocationProcessorEmit,
- LocationProcessorRead,
-} from './types';
-
-const DEFINITION_AT_LOCATION_ANNOTATION = 'backstage.io/definition-at-location';
-
-export class ApiDefinitionAtLocationProcessor implements LocationProcessor {
- async processEntity(
- entity: Entity,
- _location: LocationSpec,
- _emit: LocationProcessorEmit,
- read: LocationProcessorRead,
- ): Promise {
- if (
- entity.kind !== 'API' ||
- !entity.metadata.annotations ||
- !entity.metadata.annotations[DEFINITION_AT_LOCATION_ANNOTATION]
- ) {
- return entity;
- }
-
- const reference =
- entity.metadata.annotations[DEFINITION_AT_LOCATION_ANNOTATION];
- const { type, target } = extractReference(reference);
- const data = await read({ type, target });
- const definition = data.toString();
- const apiEntity = entity as ApiEntity;
- apiEntity.spec.definition = definition;
-
- return entity;
- }
-}
-
-function extractReference(reference: string): { type: string; target: string } {
- const delimiterIndex = reference.indexOf(':');
- const type = reference.slice(0, delimiterIndex);
- const target = reference.slice(delimiterIndex + 1);
-
- return { type, target };
-}
diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts
deleted file mode 100644
index 9b234e3e75..0000000000
--- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.test.ts
+++ /dev/null
@@ -1,122 +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 { AzureApiReaderProcessor } from './AzureApiReaderProcessor';
-import { ConfigReader } from '@backstage/config';
-
-describe('AzureApiReaderProcessor', () => {
- const createConfig = (token: string | undefined) =>
- ConfigReader.fromConfigs([
- {
- context: '',
- data: {
- catalog: {
- processors: {
- azureApi: {
- privateToken: token,
- },
- },
- },
- },
- },
- ]);
-
- it('should build raw api', () => {
- const processor = new AzureApiReaderProcessor(createConfig(undefined));
- const tests = [
- {
- target:
- 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
- url: new URL(
- 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
- ),
- err: undefined,
- },
- {
- target:
- 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
- url: new URL(
- 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
- ),
- err: undefined,
- },
- {
- target: 'https://api.com/a/b/blob/master/path/to/c.yaml',
- url: null,
- err:
- 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
- },
- {
- target: 'com/a/b/blob/master/path/to/c.yaml',
- url: null,
- err:
- 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
- },
- ];
-
- for (const test of tests) {
- if (test.err) {
- expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err);
- } else if (test.url) {
- expect(processor.buildRawUrl(test.target).toString()).toEqual(
- test.url.toString(),
- );
- } else {
- throw new Error(
- 'This should not have happened. Either err or url should have matched.',
- );
- }
- }
- });
-
- it('should return request options', () => {
- const tests = [
- {
- token: '0123456789',
- expect: {
- headers: {
- Authorization: 'Basic OjAxMjM0NTY3ODk=',
- },
- },
- },
- {
- token: '',
- expect: {
- headers: {},
- },
- err:
- "Invalid type in config for key 'catalog.processors.azureApi.privateToken' in '', got empty-string, wanted string",
- },
- {
- token: undefined,
- expect: {
- headers: {},
- },
- },
- ];
-
- for (const test of tests) {
- if (test.err) {
- expect(
- () => new AzureApiReaderProcessor(createConfig(test.token)),
- ).toThrowError(test.err);
- } else {
- const processor = new AzureApiReaderProcessor(createConfig(test.token));
- expect(processor.getRequestOptions()).toEqual(test.expect);
- }
- }
- });
-});
diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts
index 70a84d72df..2a15c18c84 100644
--- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts
@@ -20,6 +20,11 @@ import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { Config } from '@backstage/config';
+// ***********************************************************************
+// * NOTE: This has been replaced by packages/backend-common/src/reading *
+// * Don't implement new functionality here as this file will be removed *
+// ***********************************************************************
+
export class AzureApiReaderProcessor implements LocationProcessor {
private privateToken: string;
diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts
deleted file mode 100644
index 953f0703be..0000000000
--- a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.test.ts
+++ /dev/null
@@ -1,161 +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 { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor';
-import { ConfigReader } from '@backstage/config';
-
-describe('BitbucketApiReaderProcessor', () => {
- const createConfig = (
- username: string | undefined,
- appPassword: string | undefined,
- ) =>
- ConfigReader.fromConfigs([
- {
- context: '',
- data: {
- catalog: {
- processors: {
- bitbucketApi: {
- username: username,
- appPassword: appPassword,
- },
- },
- },
- },
- },
- ]);
-
- it('should build raw api', () => {
- const processor = new BitbucketApiReaderProcessor(
- createConfig(undefined, undefined),
- );
-
- const tests = [
- {
- target:
- 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
- url: new URL(
- 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
- ),
- err: undefined,
- },
- {
- target: 'https://api.com/a/b/blob/master/path/to/c.yaml',
- url: null,
- err:
- 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Bitbucket URL or Invalid file path',
- },
- {
- target: 'com/a/b/blob/master/path/to/c.yaml',
- url: null,
- err:
- 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
- },
- ];
-
- for (const test of tests) {
- if (test.err) {
- expect(() => processor.buildRawUrl(test.target)).toThrowError(test.err);
- } else if (test.url) {
- expect(processor.buildRawUrl(test.target).toString()).toEqual(
- test.url.toString(),
- );
- } else {
- throw new Error(
- 'This should not have happened. Either err or url should have matched.',
- );
- }
- }
- });
-
- it('should return request options', () => {
- const tests = [
- {
- username: '',
- password: '',
- expect: {
- headers: {},
- },
- err:
- "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string",
- },
- {
- username: 'only-user-provided',
- password: '',
- expect: {
- headers: {},
- },
- err:
- "Invalid type in config for key 'catalog.processors.bitbucketApi.appPassword' in '', got empty-string, wanted string",
- },
- {
- username: '',
- password: 'only-password-provided',
- expect: {
- headers: {},
- },
- err:
- "Invalid type in config for key 'catalog.processors.bitbucketApi.username' in '', got empty-string, wanted string",
- },
- {
- username: 'some-user',
- password: 'my-secret',
- expect: {
- headers: {
- Authorization: 'Basic c29tZS11c2VyOm15LXNlY3JldA==',
- },
- },
- },
- {
- username: undefined,
- password: undefined,
- expect: {
- headers: {},
- },
- },
- {
- username: 'only-user-provided',
- password: undefined,
- expect: {
- headers: {},
- },
- },
- {
- username: undefined,
- password: 'only-password-provided',
- expect: {
- headers: {},
- },
- },
- ];
-
- for (const test of tests) {
- if (test.err) {
- expect(
- () =>
- new BitbucketApiReaderProcessor(
- createConfig(test.username, test.password),
- ),
- ).toThrowError(test.err);
- } else {
- const processor = new BitbucketApiReaderProcessor(
- createConfig(test.username, test.password),
- );
- expect(processor.getRequestOptions()).toEqual(test.expect);
- }
- }
- });
-});
diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts
index c46fbd1737..b3720a5b76 100644
--- a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts
@@ -20,6 +20,11 @@ import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { Config } from '@backstage/config';
+// ***********************************************************************
+// * NOTE: This has been replaced by packages/backend-common/src/reading *
+// * Don't implement new functionality here as this file will be removed *
+// ***********************************************************************
+
export class BitbucketApiReaderProcessor implements LocationProcessor {
private username: string;
private password: string;
diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts
index 29f94dd0d7..c1c83b7c6c 100644
--- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts
@@ -17,7 +17,7 @@
import { LocationSpec } from '@backstage/catalog-model';
import { CodeOwnersEntry } from 'codeowners-utils';
import {
- buildCodeOwnerLocation,
+ buildCodeOwnerUrl,
buildUrl,
CodeOwnersProcessor,
findPrimaryCodeOwner,
@@ -28,18 +28,18 @@ import {
} from './CodeOwnersProcessor';
describe(CodeOwnersProcessor, () => {
+ const mockUrl = ({ basePath = '' } = {}): string =>
+ `https://github.com/spotify/backstage/blob/master/${basePath}catalog-info.yaml`;
const mockLocation = ({
basePath = '',
type = 'github',
} = {}): LocationSpec => ({
type,
- target: `https://github.com/spotify/backstage/blob/master/${basePath}catalog-info.yaml`,
+ target: mockUrl({ basePath }),
});
- const mockReadLocation = (basePath = '') => ({
- type: 'github',
- target: `https://github.com/spotify/backstage/blob/master/${basePath}CODEOWNERS`,
- });
+ const mockReadUrl = (basePath = '') =>
+ `https://github.com/spotify/backstage/blob/master/${basePath}CODEOWNERS`;
const mockGitUri = (codeOwnersPath: string = '') => {
return {
@@ -99,27 +99,20 @@ describe(CodeOwnersProcessor, () => {
});
});
- describe(buildCodeOwnerLocation, () => {
- it('should builds a location spec to the codeowners', () => {
- expect(
- buildCodeOwnerLocation(mockLocation(), '/docs/CODEOWNERS'),
- ).toEqual({
- type: 'github',
- target:
- 'https://github.com/spotify/backstage/blob/master/docs/CODEOWNERS',
- });
+ describe(buildCodeOwnerUrl, () => {
+ it('should build a location spec to the codeowners', () => {
+ expect(buildCodeOwnerUrl(mockUrl(), '/docs/CODEOWNERS')).toEqual(
+ 'https://github.com/spotify/backstage/blob/master/docs/CODEOWNERS',
+ );
});
it('should handle nested paths from original location spec', () => {
expect(
- buildCodeOwnerLocation(
- mockLocation({ basePath: 'packages/foo/' }),
+ buildCodeOwnerUrl(
+ mockUrl({ basePath: 'packages/foo/' }),
'/CODEOWNERS',
),
- ).toEqual({
- type: 'github',
- target: 'https://github.com/spotify/backstage/blob/master/CODEOWNERS',
- });
+ ).toEqual('https://github.com/spotify/backstage/blob/master/CODEOWNERS');
});
});
@@ -158,15 +151,17 @@ describe(CodeOwnersProcessor, () => {
const read = jest
.fn()
.mockResolvedValue(mockReadResult({ data: ownersText }));
- const result = await findRawCodeOwners(mockLocation(), read);
+ const reader = { read };
+ const result = await findRawCodeOwners(mockLocation(), reader);
expect(result).toEqual(ownersText);
});
it('should raise error when no codeowner', async () => {
const read = jest.fn().mockRejectedValue(mockReadResult());
+ const reader = { read };
await expect(
- findRawCodeOwners(mockLocation(), read),
+ findRawCodeOwners(mockLocation(), reader),
).rejects.toBeInstanceOf(Error);
});
@@ -177,13 +172,14 @@ describe(CodeOwnersProcessor, () => {
.mockImplementationOnce(() => mockReadResult({ error: 'foo' }))
.mockImplementationOnce(() => mockReadResult({ error: 'bar' }))
.mockResolvedValue(mockReadResult({ data: ownersText }));
+ const reader = { read };
- const result = await findRawCodeOwners(mockLocation(), read);
+ const result = await findRawCodeOwners(mockLocation(), reader);
expect(read.mock.calls.length).toBe(3);
- expect(read.mock.calls[0]).toEqual([mockReadLocation('.github/')]);
- expect(read.mock.calls[1]).toEqual([mockReadLocation('')]);
- expect(read.mock.calls[2]).toEqual([mockReadLocation('docs/')]);
+ expect(read.mock.calls[0]).toEqual([mockReadUrl('.github/')]);
+ expect(read.mock.calls[1]).toEqual([mockReadUrl('')]);
+ expect(read.mock.calls[2]).toEqual([mockReadUrl('docs/')]);
expect(result).toEqual(ownersText);
});
});
@@ -193,7 +189,9 @@ describe(CodeOwnersProcessor, () => {
const read = jest
.fn()
.mockResolvedValue(mockReadResult({ data: mockCodeOwnersText() }));
- const owner = await resolveCodeOwner(mockLocation(), read);
+ const reader = { read };
+
+ const owner = await resolveCodeOwner(mockLocation(), reader);
expect(owner).toBe('backstage-core');
});
@@ -201,9 +199,10 @@ describe(CodeOwnersProcessor, () => {
const read = jest
.fn()
.mockImplementation(() => mockReadResult({ error: 'error: foo' }));
+ const reader = { read };
await expect(
- resolveCodeOwner(mockLocation(), read),
+ resolveCodeOwner(mockLocation(), reader),
).rejects.toBeInstanceOf(Error);
});
});
@@ -211,10 +210,11 @@ describe(CodeOwnersProcessor, () => {
describe(CodeOwnersProcessor, () => {
const setupTest = ({ kind = 'Component', spec = {} } = {}) => {
const entity = { kind, spec };
- const processor = new CodeOwnersProcessor();
const read = jest
.fn()
.mockResolvedValue(mockReadResult({ data: mockCodeOwnersText() }));
+ const reader = { read };
+ const processor = new CodeOwnersProcessor({ reader });
return { entity, processor, read };
};
@@ -227,8 +227,6 @@ describe(CodeOwnersProcessor, () => {
const result = await processor.processEntity(
entity as any,
mockLocation(),
- null as any,
- null as any,
);
expect(result).toEqual(entity);
@@ -240,8 +238,6 @@ describe(CodeOwnersProcessor, () => {
const result = await processor.processEntity(
entity as any,
mockLocation({ type: 'url' }),
- null as any,
- null as any,
);
expect(result).toEqual(entity);
@@ -253,21 +249,17 @@ describe(CodeOwnersProcessor, () => {
const result = await processor.processEntity(
entity as any,
mockLocation(),
- null as any,
- null as any,
);
expect(result).toEqual(entity);
});
it('should set owner from codeowner', async () => {
- const { entity, processor, read } = setupTest();
+ const { entity, processor } = setupTest();
const result = await processor.processEntity(
entity as any,
mockLocation(),
- null as any,
- read,
);
expect(result).toEqual({
diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
index c875bdc799..e624af91ee 100644
--- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
@@ -14,12 +14,9 @@
* limitations under the License.
*/
+import { UrlReader } from '@backstage/backend-common';
import { Entity, LocationSpec } from '@backstage/catalog-model';
-import {
- LocationProcessor,
- LocationProcessorEmit,
- LocationProcessorRead,
-} from './types';
+import { LocationProcessor } from './types';
import * as codeowners from 'codeowners-utils';
import { CodeOwnersEntry } from 'codeowners-utils';
import parseGitUri from 'git-url-parse';
@@ -37,13 +34,14 @@ const ALLOWED_LOCATION_TYPES = [
'gitlab/api',
];
+type Options = {
+ reader: UrlReader;
+};
+
export class CodeOwnersProcessor implements LocationProcessor {
- async processEntity(
- entity: Entity,
- location: LocationSpec,
- _emit: LocationProcessorEmit,
- read: LocationProcessorRead,
- ): Promise {
+ constructor(private readonly options: Options) {}
+
+ async processEntity(entity: Entity, location: LocationSpec): Promise {
// Only continue if the owner is not set
if (
!entity ||
@@ -54,7 +52,7 @@ export class CodeOwnersProcessor implements LocationProcessor {
return entity;
}
- const owner = await resolveCodeOwner(location, read);
+ const owner = await resolveCodeOwner(location, this.options.reader);
return {
...entity,
@@ -65,9 +63,9 @@ export class CodeOwnersProcessor implements LocationProcessor {
export async function resolveCodeOwner(
location: LocationSpec,
- read: LocationProcessorRead,
+ reader: UrlReader,
): Promise {
- const ownersText = await findRawCodeOwners(location, read);
+ const ownersText = await findRawCodeOwners(location, reader);
if (!ownersText) {
throw Error(`Unable to find codeowners file for: ${location.target}`);
@@ -80,15 +78,14 @@ export async function resolveCodeOwner(
export async function findRawCodeOwners(
location: LocationSpec,
- read: LocationProcessorRead,
+ reader: UrlReader,
): Promise {
const readOwnerLocation = async (basePath: string): Promise => {
- const ownerLocation = buildCodeOwnerLocation(
- location,
+ const ownerUrl = buildCodeOwnerUrl(
+ location.target,
`${basePath}/CODEOWNERS`,
);
-
- const data = await read(ownerLocation);
+ const data = await reader.read(ownerUrl);
return data.toString();
};
@@ -101,6 +98,13 @@ export async function findRawCodeOwners(
]);
}
+export function buildCodeOwnerUrl(
+ basePath: string,
+ codeOwnersPath: string,
+): string {
+ return buildUrl({ ...parseGitUri(basePath), codeOwnersPath });
+}
+
export function parseCodeOwners(ownersText: string) {
return codeowners.parse(ownersText);
}
@@ -128,15 +132,6 @@ export function normalizeCodeOwner(owner: string) {
return owner;
}
-export function buildCodeOwnerLocation(
- location: LocationSpec,
- codeOwnersPath: string,
-): LocationSpec {
- const { type, target } = location;
-
- return { type, target: buildUrl({ ...parseGitUri(target), codeOwnersPath }) };
-}
-
export function buildUrl({
protocol = 'https',
source = 'github.com',
diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts
index e0d35f78aa..fdba32ef7e 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import {
@@ -94,9 +95,15 @@ describe('GithubOrgReaderProcessor', () => {
describe('implementation', () => {
it('rejects unknown types', async () => {
- const processor = new GithubOrgReaderProcessor([
- { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
- ]);
+ const processor = new GithubOrgReaderProcessor({
+ providers: [
+ {
+ target: 'https://github.com',
+ apiBaseUrl: 'https://api.github.com',
+ },
+ ],
+ logger: getVoidLogger(),
+ });
const location: LocationSpec = {
type: 'not-github-org',
target: 'https://github.com',
@@ -107,9 +114,15 @@ describe('GithubOrgReaderProcessor', () => {
});
it('rejects unknown targets', async () => {
- const processor = new GithubOrgReaderProcessor([
- { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' },
- ]);
+ const processor = new GithubOrgReaderProcessor({
+ providers: [
+ {
+ target: 'https://github.com',
+ apiBaseUrl: 'https://api.github.com',
+ },
+ ],
+ logger: getVoidLogger(),
+ });
const location: LocationSpec = {
type: 'github-org',
target: 'https://not.github.com/apa',
diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts
index 5084d4c16f..b8a6f6c43f 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts
@@ -17,6 +17,7 @@
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { graphql } from '@octokit/graphql';
+import { Logger } from 'winston';
import * as results from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { getOrganizationTeams, getOrganizationUsers } from './util/github';
@@ -26,11 +27,20 @@ import { buildOrgHierarchy } from './util/org';
* Extracts teams and users out of a GitHub org.
*/
export class GithubOrgReaderProcessor implements LocationProcessor {
- static fromConfig(config: Config) {
- return new GithubOrgReaderProcessor(readConfig(config));
+ private readonly providers: ProviderConfig[];
+ private readonly logger: Logger;
+
+ static fromConfig(config: Config, options: { logger: Logger }) {
+ return new GithubOrgReaderProcessor({
+ ...options,
+ providers: readConfig(config),
+ });
}
- constructor(private readonly providers: ProviderConfig[]) {}
+ constructor(options: { providers: ProviderConfig[]; logger: Logger }) {
+ this.providers = options.providers;
+ this.logger = options.logger;
+ }
async readLocation(
location: LocationSpec,
@@ -59,13 +69,34 @@ export class GithubOrgReaderProcessor implements LocationProcessor {
},
});
+ // Read out all of the raw data
+ const startTimestamp = Date.now();
+ this.logger.info('Reading GitHub users and groups');
+
const { users } = await getOrganizationUsers(client, org);
const { groups, groupMemberUsers } = await getOrganizationTeams(
client,
org,
);
- buildOrgHierarchy(groups, users, groupMemberUsers);
+ const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
+ this.logger.debug(
+ `Read ${users.length} GitHub users and ${groups.length} GitHub groups in ${duration} seconds`,
+ );
+
+ // Fill out the hierarchy
+ const usersByName = new Map(users.map(u => [u.metadata.name, u]));
+ for (const [groupName, userNames] of groupMemberUsers.entries()) {
+ for (const userName of userNames) {
+ const user = usersByName.get(userName);
+ if (user && !user.spec.memberOf.includes(groupName)) {
+ user.spec.memberOf.push(groupName);
+ }
+ }
+ }
+ buildOrgHierarchy(groups);
+
+ // Done!
for (const group of groups) {
emit(results.entity(location, group));
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts
index cc50e53240..89d1f69860 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts
@@ -22,6 +22,11 @@ import { Logger } from 'winston';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
+// ***********************************************************************
+// * NOTE: This has been replaced by packages/backend-common/src/reading *
+// * Don't implement new functionality here as this file will be removed *
+// ***********************************************************************
+
/**
* The configuration parameters for a single GitHub API provider.
*/
diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts
deleted file mode 100644
index 7e55fece1b..0000000000
--- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts
+++ /dev/null
@@ -1,123 +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 { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor';
-import { ConfigReader } from '@backstage/config';
-
-describe('GitlabApiReaderProcessor', () => {
- const createConfig = (token: string | undefined) =>
- ConfigReader.fromConfigs([
- {
- context: '',
- data: {
- catalog: {
- processors: {
- gitlabApi: {
- privateToken: token,
- },
- },
- },
- },
- },
- ]);
-
- it('should build raw api', () => {
- const processor = new GitlabApiReaderProcessor(createConfig(undefined));
-
- const tests = [
- {
- target:
- 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
- url: new URL(
- 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
- ),
- err: undefined,
- },
- {
- target:
- 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
- url: new URL(
- 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
- ),
- err: undefined,
- },
- {
- target:
- 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
- url: new URL(
- 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
- ),
- err: undefined,
- },
- ];
-
- for (const test of tests) {
- if (test.url) {
- expect(processor.buildRawUrl(test.target, 12345).toString()).toEqual(
- test.url.toString(),
- );
- } else {
- throw new Error(
- 'This should not have happened. Either err or url should have matched.',
- );
- }
- }
- });
-
- it('should return request options', () => {
- const tests = [
- {
- token: '0123456789',
- expect: {
- headers: {
- 'PRIVATE-TOKEN': '0123456789',
- },
- },
- },
- {
- token: '',
- err:
- "Invalid type in config for key 'catalog.processors.gitlabApi.privateToken' in '', got empty-string, wanted string",
- expect: {
- headers: {
- 'PRIVATE-TOKEN': '',
- },
- },
- },
- {
- token: undefined,
- expect: {
- headers: {
- 'PRIVATE-TOKEN': '',
- },
- },
- },
- ];
-
- for (const test of tests) {
- if (test.err) {
- expect(
- () => new GitlabApiReaderProcessor(createConfig(test.token)),
- ).toThrowError(test.err);
- } else {
- const processor = new GitlabApiReaderProcessor(
- createConfig(test.token),
- );
- expect(processor.getRequestOptions()).toEqual(test.expect);
- }
- }
- });
-});
diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts
index a51c306777..aeba7a7ebe 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts
@@ -20,6 +20,11 @@ import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
import { Config } from '@backstage/config';
+// ***********************************************************************
+// * NOTE: This has been replaced by packages/backend-common/src/reading *
+// * Don't implement new functionality here as this file will be removed *
+// ***********************************************************************
+
export class GitlabApiReaderProcessor implements LocationProcessor {
private privateToken: string;
diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts
index c33b388e1a..b51bf85a8e 100644
--- a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts
@@ -19,6 +19,11 @@ import fetch from 'node-fetch';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
+// ***********************************************************************
+// * NOTE: This has been replaced by packages/backend-common/src/reading *
+// * Don't implement new functionality here as this file will be removed *
+// ***********************************************************************
+
export class GitlabReaderProcessor implements LocationProcessor {
async readLocation(
location: LocationSpec,
diff --git a/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts
new file mode 100644
index 0000000000..a32c96480f
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { LocationSpec } from '@backstage/catalog-model';
+import { Config } from '@backstage/config';
+import { Logger } from 'winston';
+import {
+ LdapClient,
+ LdapProviderConfig,
+ readLdapConfig,
+ readLdapOrg,
+} from './ldap';
+import * as results from './results';
+import { LocationProcessor, LocationProcessorEmit } from './types';
+
+/**
+ * Extracts teams and users out of an LDAP server.
+ */
+export class LdapOrgReaderProcessor implements LocationProcessor {
+ private readonly providers: LdapProviderConfig[];
+ private readonly logger: Logger;
+
+ static fromConfig(config: Config, options: { logger: Logger }) {
+ const c = config.getOptionalConfig('catalog.processors.ldapOrg');
+ return new LdapOrgReaderProcessor({
+ ...options,
+ providers: c ? readLdapConfig(c) : [],
+ });
+ }
+
+ constructor(options: { providers: LdapProviderConfig[]; logger: Logger }) {
+ this.providers = options.providers;
+ this.logger = options.logger;
+ }
+
+ async readLocation(
+ location: LocationSpec,
+ _optional: boolean,
+ emit: LocationProcessorEmit,
+ ): Promise {
+ if (location.type !== 'ldap-org') {
+ return false;
+ }
+
+ const provider = this.providers.find(p => location.target === p.target);
+ if (!provider) {
+ throw new Error(
+ `There is no LDAP Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.ldapOrg.providers.`,
+ );
+ }
+
+ // Read out all of the raw data
+ const startTimestamp = Date.now();
+ this.logger.info('Reading LDAP users and groups');
+
+ // Be lazy and create the client each time; even though it's pretty
+ // inefficient, we usually only do this once per entire refresh loop and
+ // don't have to worry about timeouts and reconnects etc.
+ const client = await LdapClient.create(provider.target, provider.bind);
+ const { users, groups } = await readLdapOrg(
+ client,
+ provider.users,
+ provider.groups,
+ );
+
+ const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
+ this.logger.debug(
+ `Read ${users.length} LDAP users and ${groups.length} LDAP groups in ${duration} seconds`,
+ );
+
+ // Done!
+ for (const group of groups) {
+ emit(results.entity(location, group));
+ }
+ for (const user of users) {
+ emit(results.entity(location, user));
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts
index 6893cae0e7..c196458e2a 100644
--- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts
@@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { UrlReader } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import {
jsonPlaceholderResolver,
PlaceholderProcessor,
PlaceholderResolver,
ResolverParams,
+ ResolverRead,
yamlPlaceholderResolver,
} from './PlaceholderProcessor';
-import { LocationProcessorEmit, LocationProcessorRead } from './types';
describe('PlaceholderProcessor', () => {
- const emit: LocationProcessorEmit = jest.fn();
- const read: jest.MockedFunction = jest.fn();
+ const read: jest.MockedFunction = jest.fn();
+ const reader: UrlReader = { read };
beforeEach(() => {
jest.resetAllMocks();
@@ -39,10 +39,13 @@ describe('PlaceholderProcessor', () => {
metadata: { name: 'n' },
};
const processor = new PlaceholderProcessor({
- foo: async () => 'replaced',
+ resolvers: {
+ foo: async () => 'replaced',
+ },
+ reader,
});
await expect(
- processor.processEntity(input, { type: 't', target: 'l' }, emit, read),
+ processor.processEntity(input, { type: 't', target: 'l' }),
).resolves.toBe(input);
});
@@ -51,7 +54,10 @@ describe('PlaceholderProcessor', () => {
value!.toString().toUpperCase(),
);
const processor = new PlaceholderProcessor({
- upper: upperResolver,
+ resolvers: {
+ upper: upperResolver,
+ },
+ reader,
});
await expect(
@@ -63,8 +69,6 @@ describe('PlaceholderProcessor', () => {
spec: { a: [{ b: { $upper: 'text' } }] },
},
{ type: 'fake', target: 'http://example.com' },
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -73,20 +77,23 @@ describe('PlaceholderProcessor', () => {
spec: { a: [{ b: 'TEXT' }] },
});
- expect(emit).not.toBeCalled();
expect(read).not.toBeCalled();
- expect(upperResolver).toBeCalledWith({
- key: 'upper',
- value: 'text',
- location: { type: 'fake', target: 'http://example.com' },
- read,
- });
+ expect(upperResolver).toBeCalledWith(
+ expect.objectContaining({
+ key: 'upper',
+ value: 'text',
+ baseUrl: 'http://example.com',
+ }),
+ );
});
it('rejects multiple placeholders', async () => {
const processor = new PlaceholderProcessor({
- foo: jest.fn(),
- bar: jest.fn(),
+ resolvers: {
+ foo: jest.fn(),
+ bar: jest.fn(),
+ },
+ reader,
});
await expect(
@@ -97,20 +104,20 @@ describe('PlaceholderProcessor', () => {
metadata: { name: 'n', x: { $foo: 'a', $bar: 'b' } },
},
{ type: 'a', target: 'b' },
- emit,
- read,
),
).rejects.toThrow(
'Placeholders have to be on the form of a single $-prefixed key in an object',
);
- expect(emit).not.toBeCalled();
expect(read).not.toBeCalled();
});
it('rejects unknown placeholders', async () => {
const processor = new PlaceholderProcessor({
- bar: jest.fn(),
+ resolvers: {
+ bar: jest.fn(),
+ },
+ reader,
});
await expect(
@@ -121,18 +128,15 @@ describe('PlaceholderProcessor', () => {
metadata: { name: 'n', x: { $foo: 'a' } },
},
{ type: 'a', target: 'b' },
- emit,
- read,
),
).rejects.toThrow('Encountered unknown placeholder $foo');
- expect(emit).not.toBeCalled();
expect(read).not.toBeCalled();
});
it('has builtin text support', async () => {
read.mockResolvedValue(Buffer.from('TEXT', 'utf-8'));
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -146,8 +150,6 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
},
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -156,18 +158,16 @@ describe('PlaceholderProcessor', () => {
spec: { data: 'TEXT' },
});
- expect(emit).not.toBeCalled();
- expect(read).toBeCalledWith({
- type: 'github',
- target: 'https://github.com/spotify/backstage/a/file.txt',
- });
+ expect(read).toBeCalledWith(
+ 'https://github.com/spotify/backstage/a/file.txt',
+ );
});
it('has builtin json support', async () => {
read.mockResolvedValue(
Buffer.from(JSON.stringify({ a: ['b', 7] }), 'utf-8'),
);
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -181,8 +181,6 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
},
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -191,16 +189,14 @@ describe('PlaceholderProcessor', () => {
spec: { data: { a: ['b', 7] } },
});
- expect(emit).not.toBeCalled();
- expect(read).toBeCalledWith({
- type: 'github',
- target: 'https://github.com/spotify/backstage/a/b/file.json',
- });
+ expect(read).toBeCalledWith(
+ 'https://github.com/spotify/backstage/a/b/file.json',
+ );
});
it('has builtin yaml support', async () => {
read.mockResolvedValue(Buffer.from('foo:\n - bar: 7', 'utf-8'));
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -214,8 +210,6 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
},
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -224,16 +218,14 @@ describe('PlaceholderProcessor', () => {
spec: { data: { foo: [{ bar: 7 }] } },
});
- expect(emit).not.toBeCalled();
- expect(read).toBeCalledWith({
- type: 'github',
- target: 'https://github.com/spotify/backstage/a/file.yaml',
- });
+ expect(read).toBeCalledWith(
+ 'https://github.com/spotify/backstage/a/file.yaml',
+ );
});
it('resolves absolute path for absolute location', async () => {
read.mockResolvedValue(Buffer.from('TEXT', 'utf-8'));
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -251,8 +243,6 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
},
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -261,16 +251,14 @@ describe('PlaceholderProcessor', () => {
spec: { data: 'TEXT' },
});
- expect(emit).not.toBeCalled();
- expect(read).toBeCalledWith({
- type: 'github',
- target: 'https://github.com/spotify/backstage/catalog-info.yaml',
- });
+ expect(read).toBeCalledWith(
+ 'https://github.com/spotify/backstage/catalog-info.yaml',
+ );
});
it('resolves absolute path for relative file location', async () => {
read.mockResolvedValue(Buffer.from('TEXT', 'utf-8'));
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -288,8 +276,6 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: './a/b/catalog-info.yaml',
},
- emit,
- read,
),
).resolves.toEqual({
apiVersion: 'a',
@@ -298,11 +284,9 @@ describe('PlaceholderProcessor', () => {
spec: { data: 'TEXT' },
});
- expect(emit).not.toBeCalled();
- expect(read).toBeCalledWith({
- type: 'github',
- target: 'https://github.com/spotify/backstage/catalog-info.yaml',
- });
+ expect(read).toBeCalledWith(
+ 'https://github.com/spotify/backstage/catalog-info.yaml',
+ );
});
it('not resolves relative file path for relative file location', async () => {
@@ -310,7 +294,7 @@ describe('PlaceholderProcessor', () => {
// traversel attacks. If we want to implement this, we need to have additional
// security measures in place!
read.mockResolvedValue(Buffer.from('TEXT', 'utf-8'));
- const processor = PlaceholderProcessor.default();
+ const processor = PlaceholderProcessor.default({ reader });
await expect(
processor.processEntity(
@@ -328,27 +312,21 @@ describe('PlaceholderProcessor', () => {
type: 'github',
target: './a/b/catalog-info.yaml',
},
- emit,
- read,
),
).rejects.toThrow(
'Placeholder $text could not form an URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml',
);
- expect(emit).not.toBeCalled();
expect(read).not.toBeCalled();
});
});
describe('yamlPlaceholderResolver', () => {
- const read: jest.MockedFunction = jest.fn();
+ const read: jest.MockedFunction = jest.fn();
const params: ResolverParams = {
key: 'a',
value: './file.yaml',
- location: {
- type: 'github',
- target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
- },
+ baseUrl: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
read,
};
@@ -388,14 +366,11 @@ describe('yamlPlaceholderResolver', () => {
});
describe('jsonPlaceholderResolver', () => {
- const read: jest.MockedFunction = jest.fn();
+ const read: jest.MockedFunction = jest.fn();
const params: ResolverParams = {
key: 'a',
value: './file.json',
- location: {
- type: 'github',
- target: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
- },
+ baseUrl: 'https://github.com/spotify/backstage/a/b/catalog-info.yaml',
read,
};
diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts
index b811d217d5..91257b68a4 100644
--- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts
@@ -14,49 +14,49 @@
* limitations under the License.
*/
+import { UrlReader } from '@backstage/backend-common';
import { Entity, LocationSpec } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/config';
import yaml from 'yaml';
-import {
- LocationProcessor,
- LocationProcessorEmit,
- LocationProcessorRead,
-} from './types';
+import { LocationProcessor } from './types';
+
+export type ResolverRead = (url: string) => Promise;
export type ResolverParams = {
key: string;
value: JsonValue;
- location: LocationSpec;
- read: LocationProcessorRead;
+ baseUrl: string;
+ read: ResolverRead;
};
export type PlaceholderResolver = (
params: ResolverParams,
) => Promise;
+type Options = {
+ resolvers: Record;
+ reader: UrlReader;
+};
+
/**
* Traverses raw entity JSON looking for occurrences of $-prefixed placeholders
* that it then fills in with actual data.
*/
export class PlaceholderProcessor implements LocationProcessor {
- static default() {
+ static default({ reader }: { reader: UrlReader }) {
return new PlaceholderProcessor({
- json: jsonPlaceholderResolver,
- yaml: yamlPlaceholderResolver,
- text: textPlaceholderResolver,
+ resolvers: {
+ json: jsonPlaceholderResolver,
+ yaml: yamlPlaceholderResolver,
+ text: textPlaceholderResolver,
+ },
+ reader,
});
}
- constructor(
- private readonly resolvers: Record,
- ) {}
+ constructor(private readonly options: Options) {}
- async processEntity(
- entity: Entity,
- location: LocationSpec,
- _emit: LocationProcessorEmit,
- read: LocationProcessorRead,
- ): Promise {
+ async processEntity(entity: Entity, location: LocationSpec): Promise {
const process = async (data: any): Promise<[any, boolean]> => {
if (!data || !(data instanceof Object)) {
// Scalars can't have placeholders
@@ -90,7 +90,7 @@ export class PlaceholderProcessor implements LocationProcessor {
}
const resolverKey = keys[0].substr(1);
- const resolver = this.resolvers[resolverKey];
+ const resolver = this.options.resolvers[resolverKey];
if (!resolver) {
throw new Error(`Encountered unknown placeholder \$${resolverKey}`);
}
@@ -99,8 +99,8 @@ export class PlaceholderProcessor implements LocationProcessor {
await resolver({
key: resolverKey,
value: data[keys[0]],
- location,
- read,
+ baseUrl: location.target,
+ read: this.options.reader.read.bind(this.options.reader),
}),
true,
];
@@ -171,10 +171,10 @@ export async function textPlaceholderResolver(
*/
async function readTextLocation(params: ResolverParams): Promise {
- const newLocation = relativeLocation(params);
+ const newUrl = relativeUrl(params);
try {
- const data = await params.read(newLocation);
+ const data = await params.read(newUrl);
return data.toString('utf-8');
} catch (e) {
throw new Error(
@@ -183,11 +183,7 @@ async function readTextLocation(params: ResolverParams): Promise {
}
}
-function relativeLocation({
- key,
- value,
- location,
-}: ResolverParams): LocationSpec {
+function relativeUrl({ key, value, baseUrl }: ResolverParams): string {
if (typeof value !== 'string') {
throw new Error(
`Placeholder \$${key} expected a string value parameter, in the form of an absolute URL or a relative path`,
@@ -197,7 +193,7 @@ function relativeLocation({
let url: URL;
try {
// The two-value form of the URL constructor handles relative paths for us
- url = new URL(value, location.target);
+ url = new URL(value, baseUrl);
} catch {
try {
// Check whether value is a valid absolute URL on it's own, if not fail.
@@ -208,13 +204,10 @@ function relativeLocation({
// path traversal attacks and access to any file on the host system. Implementing this
// would require additional security measures.
throw new Error(
- `Placeholder \$${key} could not form an URL out of ${location.target} and ${value}`,
+ `Placeholder \$${key} could not form an URL out of ${baseUrl} and ${value}`,
);
}
}
- return {
- type: location.type,
- target: url.toString(),
- };
+ return url.toString();
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts
index 1ab4cb09b8..009872cda3 100644
--- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts
@@ -22,6 +22,8 @@ import {
} from './types';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
+import { getVoidLogger, UrlReaders } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
describe('UrlReaderProcessor', () => {
const mockApiOrigin = 'http://localhost:23000';
@@ -32,7 +34,9 @@ describe('UrlReaderProcessor', () => {
afterAll(() => server.close());
it('should load from url', async () => {
- const processor = new UrlReaderProcessor();
+ const logger = getVoidLogger();
+ const reader = UrlReaders.default({ logger, config: new ConfigReader({}) });
+ const processor = new UrlReaderProcessor({ reader, logger });
const spec = {
type: 'url',
target: `${mockApiOrigin}/component.yaml`,
@@ -54,7 +58,9 @@ describe('UrlReaderProcessor', () => {
});
it('should fail load from url with error', async () => {
- const processor = new UrlReaderProcessor();
+ const logger = getVoidLogger();
+ const reader = UrlReaders.default({ logger, config: new ConfigReader({}) });
+ const processor = new UrlReaderProcessor({ reader, logger });
const spec = {
type: 'url',
target: `${mockApiOrigin}/component-notfound.yaml`,
@@ -74,7 +80,7 @@ describe('UrlReaderProcessor', () => {
expect(generated.location).toBe(spec);
expect(generated.error.name).toBe('NotFoundError');
expect(generated.error.message).toBe(
- `${mockApiOrigin}/component-notfound.yaml could not be read, 404 Not Found`,
+ `Unable to read url, NotFoundError: could not read ${mockApiOrigin}/component-notfound.yaml, 404 Not Found`,
);
});
});
diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts
index 0c879ea70c..2bc05bf1cb 100644
--- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts
@@ -14,40 +14,58 @@
* limitations under the License.
*/
+import { Logger } from 'winston';
+import { UrlReader } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
-import fetch from 'node-fetch';
import * as result from './results';
import { LocationProcessor, LocationProcessorEmit } from './types';
+// TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this
+// can be removed in a bit
+const deprecatedTypes = [
+ 'github',
+ 'github/api',
+ 'bitbucket/api',
+ 'gitlab/api',
+ 'azure/api',
+];
+
+type Options = {
+ reader: UrlReader;
+ logger: Logger;
+};
+
export class UrlReaderProcessor implements LocationProcessor {
+ constructor(private readonly options: Options) {}
+
async readLocation(
location: LocationSpec,
optional: boolean,
emit: LocationProcessorEmit,
): Promise {
- if (location.type !== 'url') {
+ if (deprecatedTypes.includes(location.type)) {
+ // TODO(Rugvip): Let's not enable this warning yet, as we want to move over the example YAMLs
+ // in this repo to use the 'url' type first.
+ // this.options.logger.warn(
+ // `Using deprecated location type '${location.type}' for '${location.target}', use 'url' instead`,
+ // );
+ } else if (location.type !== 'url') {
return false;
}
try {
- const response = await fetch(location.target);
+ const data = await this.options.reader.read(location.target);
+ emit(result.data(location, data));
+ } catch (error) {
+ const message = `Unable to read ${location.type}, ${error}`;
- if (response.ok) {
- const data = await response.buffer();
- emit(result.data(location, data));
- } else {
- const message = `${location.target} could not be read, ${response.status} ${response.statusText}`;
- if (response.status === 404) {
- if (!optional) {
- emit(result.notFoundError(location, message));
- }
- } else {
- emit(result.generalError(location, message));
+ if (error.name === 'NotFoundError') {
+ if (!optional) {
+ emit(result.notFoundError(location, message));
}
+ } else {
+ emit(result.generalError(location, message));
}
- } catch (e) {
- const message = `Unable to read ${location.type} ${location.target}, ${e}`;
- emit(result.generalError(location, message));
}
return true;
diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts
index 0ce7f11a5f..c9453d0247 100644
--- a/plugins/catalog-backend/src/ingestion/processors/index.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/index.ts
@@ -20,7 +20,6 @@ export { results };
export * from './types';
export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor';
-export { ApiDefinitionAtLocationProcessor } from './ApiDefinitionAtLocationProcessor';
export { AzureApiReaderProcessor } from './AzureApiReaderProcessor';
export { BitbucketApiReaderProcessor } from './BitbucketApiReaderProcessor';
export { CodeOwnersProcessor } from './CodeOwnersProcessor';
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/client.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/client.ts
new file mode 100644
index 0000000000..1ef2af5f2f
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/client.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs';
+import { BindConfig } from './config';
+import { errorString } from './util';
+
+/**
+ * Basic wrapper for the ldapjs library.
+ *
+ * Helps out with promisifying calls, paging, binding etc.
+ */
+export class LdapClient {
+ static async create(target: string, bind?: BindConfig): Promise {
+ const client = ldap.createClient({ url: target });
+ if (!bind) {
+ return new LdapClient(client);
+ }
+
+ return new Promise((resolve, reject) => {
+ const { dn, secret } = bind;
+ client.bind(dn, secret, err => {
+ if (err) {
+ reject(`LDAP bind failed for ${dn}, ${errorString(err)}`);
+ } else {
+ resolve(new LdapClient(client));
+ }
+ });
+ });
+ }
+
+ constructor(private readonly client: Client) {}
+
+ /**
+ * Performs an LDAP search operation.
+ *
+ * @param dn The fully qualified base DN to search within
+ * @param options The search options
+ */
+ async search(dn: string, options: SearchOptions): Promise {
+ try {
+ return await new Promise((resolve, reject) => {
+ const output: SearchEntry[] = [];
+
+ this.client.search(dn, options, (err, res) => {
+ if (err) {
+ reject(new Error(errorString(err)));
+ return;
+ }
+
+ res.on('searchReference', () => {
+ reject(new Error('Unable to handle referral'));
+ });
+
+ res.on('searchEntry', entry => {
+ output.push(entry);
+ });
+
+ res.on('error', e => {
+ reject(new Error(errorString(e)));
+ });
+
+ res.on('end', r => {
+ if (!r) {
+ reject(new Error('Null response'));
+ } else if (r.status !== 0) {
+ reject(new Error(`Got status ${r.status}: ${r.errorMessage}`));
+ } else {
+ resolve(output);
+ }
+ });
+ });
+ });
+ } catch (e) {
+ throw new Error(`LDAP search at ${dn} failed, ${e.message}`);
+ }
+ }
+}
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts
new file mode 100644
index 0000000000..7e6036668f
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.test.ts
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ConfigReader } from '@backstage/config';
+import { readLdapConfig } from './config';
+
+describe('readLdapConfig', () => {
+ it('applies all of the defaults', () => {
+ const config = {
+ providers: [
+ {
+ target: 'target',
+ users: {
+ dn: 'udn',
+ },
+ groups: {
+ dn: 'gdn',
+ },
+ },
+ ],
+ };
+ const actual = readLdapConfig(
+ ConfigReader.fromConfigs([{ context: '', data: config }]),
+ );
+ const expected = [
+ {
+ target: 'target',
+ bind: undefined,
+ users: {
+ dn: 'udn',
+ options: {
+ scope: 'one',
+ attributes: ['*', '+'],
+ },
+ set: undefined,
+ map: {
+ rdn: 'uid',
+ name: 'uid',
+ displayName: 'cn',
+ email: 'mail',
+ memberOf: 'memberOf',
+ },
+ },
+ groups: {
+ dn: 'gdn',
+ options: {
+ scope: 'one',
+ attributes: ['*', '+'],
+ },
+ set: undefined,
+ map: {
+ rdn: 'cn',
+ name: 'cn',
+ description: 'description',
+ type: 'groupType',
+ memberOf: 'memberOf',
+ members: 'member',
+ },
+ },
+ },
+ ];
+ expect(actual).toEqual(expected);
+ });
+
+ it('reads all the values', () => {
+ const config = {
+ providers: [
+ {
+ target: 'target',
+ bind: { dn: 'bdn', secret: 's' },
+ users: {
+ dn: 'udn',
+ options: {
+ scope: 'base',
+ attributes: ['*'],
+ filter: 'f',
+ paged: true,
+ },
+ set: [{ path: 'p', value: 'v' }],
+ map: {
+ rdn: 'u',
+ name: 'v',
+ description: 'd',
+ displayName: 'c',
+ email: 'm',
+ picture: 'p',
+ memberOf: 'm',
+ },
+ },
+ groups: {
+ dn: 'gdn',
+ options: {
+ scope: 'base',
+ attributes: ['*'],
+ filter: 'f',
+ paged: true,
+ },
+ set: [{ path: 'p', value: 'v' }],
+ map: {
+ rdn: 'u',
+ name: 'v',
+ description: 'd',
+ type: 't',
+ memberOf: 'm',
+ members: 'n',
+ },
+ },
+ },
+ ],
+ };
+ const actual = readLdapConfig(
+ ConfigReader.fromConfigs([{ context: '', data: config }]),
+ );
+ const expected = [
+ {
+ target: 'target',
+ bind: { dn: 'bdn', secret: 's' },
+ users: {
+ dn: 'udn',
+ options: {
+ scope: 'base',
+ attributes: ['*'],
+ filter: 'f',
+ paged: true,
+ },
+ set: [{ path: 'p', value: 'v' }],
+ map: {
+ rdn: 'u',
+ name: 'v',
+ description: 'd',
+ displayName: 'c',
+ email: 'm',
+ picture: 'p',
+ memberOf: 'm',
+ },
+ },
+ groups: {
+ dn: 'gdn',
+ options: {
+ scope: 'base',
+ attributes: ['*'],
+ filter: 'f',
+ paged: true,
+ },
+ set: [{ path: 'p', value: 'v' }],
+ map: {
+ rdn: 'u',
+ name: 'v',
+ description: 'd',
+ type: 't',
+ memberOf: 'm',
+ members: 'n',
+ },
+ },
+ },
+ ];
+ expect(actual).toEqual(expected);
+ });
+});
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts
new file mode 100644
index 0000000000..74c02e3f84
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/config.ts
@@ -0,0 +1,264 @@
+/*
+ * 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 { Config, JsonValue } from '@backstage/config';
+import { SearchOptions } from 'ldapjs';
+import mergeWith from 'lodash/mergeWith';
+import { RecursivePartial } from './util';
+
+/**
+ * The configuration parameters for a single LDAP provider.
+ */
+export type LdapProviderConfig = {
+ // The prefix of the target that this matches on, e.g.
+ // "ldaps://ds.example.net", with no trailing slash.
+ target: string;
+ // The settings to use for the bind command. If none are specified, the bind
+ // command is not issued.
+ bind?: BindConfig;
+ // The settings that govern the reading and interpretation of users
+ users: UserConfig;
+ // The settings that govern the reading and interpretation of groups
+ groups: GroupConfig;
+};
+
+/**
+ * The settings to use for the a command.
+ */
+export type BindConfig = {
+ // The DN of the user to auth as, e.g.
+ // uid=ldap-robot,ou=robots,ou=example,dc=example,dc=net
+ dn: string;
+ // The secret of the user to auth as (its password)
+ secret: string;
+};
+
+/**
+ * The settings that govern the reading and interpretation of users.
+ */
+export type UserConfig = {
+ // The DN under which users are stored.
+ dn: string;
+ // The search options to use.
+ // Only the scope, filter, attributes, and paged fields are supported. The
+ // default is scope "one" and attributes "*" and "+".
+ options: SearchOptions;
+ // JSON paths (on a.b.c form) and hard coded values to set on those paths
+ set?: { path: string; value: JsonValue }[];
+ // Mappings from well known entity fields, to LDAP attribute names
+ map: {
+ // The name of the attribute that holds the relative distinguished name of
+ // each entry. Defaults to "uid".
+ rdn: string;
+ // The name of the attribute that shall be used for the value of the
+ // metadata.name field of the entity. Defaults to "uid".
+ name: string;
+ // The name of the attribute that shall be used for the value of the
+ // metadata.description field of the entity.
+ description?: string;
+ // The name of the attribute that shall be used for the value of the
+ // spec.profile.displayName field of the entity. Defaults to "cn".
+ displayName: string;
+ // The name of the attribute that shall be used for the value of the
+ // spec.profile.email field of the entity. Defaults to "mail".
+ email: string;
+ // The name of the attribute that shall be used for the value of the
+ // spec.profile.picture field of the entity.
+ picture?: string;
+ // The name of the attribute that shall be used for the values of the
+ // spec.memberOf field of the entity. Defaults to "memberOf".
+ memberOf: string;
+ };
+};
+
+/**
+ * The settings that govern the reading and interpretation of groups.
+ */
+export type GroupConfig = {
+ // The DN under which groups are stored.
+ dn: string;
+ // The search options to use.
+ // Only the scope, filter, attributes, and paged fields are supported.
+ options: SearchOptions;
+ // JSON paths (on a.b.c form) and hard coded values to set on those paths
+ set?: { path: string; value: JsonValue }[];
+ // Mappings from well known entity fields, to LDAP attribute names
+ map: {
+ // The name of the attribute that holds the relative distinguished name of
+ // each entry. Defaults to "cn".
+ rdn: string;
+ // The name of the attribute that shall be used for the value of the
+ // metadata.name field of the entity. Defaults to "cn".
+ name: string;
+ // The name of the attribute that shall be used for the value of the
+ // metadata.description field of the entity. Defaults to "description".
+ description: string;
+ // The name of the attribute that shall be used for the value of the
+ // spec.type field of the entity. Defaults to "groupType".
+ type: string;
+ // The name of the attribute that shall be used for the values of the
+ // spec.parent field of the entity. Defaults to "memberOf".
+ memberOf: string;
+ // The name of the attribute that shall be used for the values of the
+ // spec.children field of the entity. Defaults to "member".
+ members: string;
+ };
+};
+
+const defaultConfig = {
+ users: {
+ options: {
+ scope: 'one',
+ attributes: ['*', '+'],
+ },
+ map: {
+ rdn: 'uid',
+ name: 'uid',
+ displayName: 'cn',
+ email: 'mail',
+ memberOf: 'memberOf',
+ },
+ },
+ groups: {
+ options: {
+ scope: 'one',
+ attributes: ['*', '+'],
+ },
+ map: {
+ rdn: 'cn',
+ name: 'cn',
+ description: 'description',
+ type: 'groupType',
+ memberOf: 'memberOf',
+ members: 'member',
+ },
+ },
+};
+
+/**
+ * Parses configuration.
+ *
+ * @param config The root of the LDAP config hierarchy
+ */
+export function readLdapConfig(config: Config): LdapProviderConfig[] {
+ function readBindConfig(
+ c: Config | undefined,
+ ): LdapProviderConfig['bind'] | undefined {
+ if (!c) {
+ return undefined;
+ }
+ return {
+ dn: c.getString('dn'),
+ secret: c.getString('secret'),
+ };
+ }
+
+ function readOptionsConfig(c: Config | undefined): SearchOptions {
+ if (!c) {
+ return {};
+ }
+ return {
+ scope: c.getOptionalString('scope') as SearchOptions['scope'],
+ filter: c.getOptionalString('filter'),
+ attributes: c.getOptionalStringArray('attributes'),
+ paged: c.getOptionalBoolean('paged'),
+ };
+ }
+
+ function readSetConfig(
+ c: Config[] | undefined,
+ ): { path: string; value: JsonValue }[] | undefined {
+ if (!c) {
+ return undefined;
+ }
+ return c.map(entry => ({
+ path: entry.getString('path'),
+ value: entry.get('value'),
+ }));
+ }
+
+ function readUserMapConfig(
+ c: Config | undefined,
+ ): Partial {
+ if (!c) {
+ return {};
+ }
+
+ return {
+ rdn: c.getOptionalString('rdn'),
+ name: c.getOptionalString('name'),
+ description: c.getOptionalString('description'),
+ displayName: c.getOptionalString('displayName'),
+ email: c.getOptionalString('email'),
+ picture: c.getOptionalString('picture'),
+ memberOf: c.getOptionalString('memberOf'),
+ };
+ }
+
+ function readGroupMapConfig(
+ c: Config | undefined,
+ ): Partial {
+ if (!c) {
+ return {};
+ }
+
+ return {
+ rdn: c.getOptionalString('rdn'),
+ name: c.getOptionalString('name'),
+ description: c.getOptionalString('description'),
+ type: c.getOptionalString('type'),
+ memberOf: c.getOptionalString('memberOf'),
+ members: c.getOptionalString('members'),
+ };
+ }
+
+ function readUserConfig(
+ c: Config,
+ ): RecursivePartial {
+ return {
+ dn: c.getString('dn'),
+ options: readOptionsConfig(c.getOptionalConfig('options')),
+ set: readSetConfig(c.getOptionalConfigArray('set')),
+ map: readUserMapConfig(c.getOptionalConfig('map')),
+ };
+ }
+
+ function readGroupConfig(
+ c: Config,
+ ): RecursivePartial {
+ return {
+ dn: c.getString('dn'),
+ options: readOptionsConfig(c.getOptionalConfig('options')),
+ set: readSetConfig(c.getOptionalConfigArray('set')),
+ map: readGroupMapConfig(c.getOptionalConfig('map')),
+ };
+ }
+
+ const providerConfigs = config.getOptionalConfigArray('providers') ?? [];
+ return providerConfigs.map(c => {
+ const newConfig = {
+ target: c.getString('target').replace(/\/+$/, ''),
+ bind: readBindConfig(c.getOptionalConfig('bind')),
+ users: readUserConfig(c.getConfig('users')),
+ groups: readGroupConfig(c.getConfig('groups')),
+ };
+ const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => {
+ // Replace arrays instead of merging, otherwise default behavior
+ return Array.isArray(from) ? from : undefined;
+ });
+ return merged as LdapProviderConfig;
+ });
+}
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/constants.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/constants.ts
new file mode 100644
index 0000000000..507da37b39
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/constants.ts
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+/**
+ * The name of an entity annotation, that references the RDN of the LDAP object
+ * it was ingested from.
+ *
+ * The RDN is the name of the leftmost attribute that identifies the item; for
+ * example, for an item with the fully qualified DN
+ * uid=john,ou=people,ou=spotify,dc=spotify,dc=net the generated entity would
+ * have this annotation, with the value "john".
+ */
+export const LDAP_RDN_ANNOTATION = 'backstage.io/ldap-rdn';
+
+/**
+ * The name of an entity annotation, that references the DN of the LDAP object
+ * it was ingested from.
+ *
+ * The DN is the fully qualified name that identifies the item; for example,
+ * for an item with the DN uid=john,ou=people,ou=spotify,dc=spotify,dc=net the
+ * generated entity would have this annotation, with that full string as its
+ * value.
+ */
+export const LDAP_DN_ANNOTATION = 'backstage.io/ldap-dn';
+
+/**
+ * The name of an entity annotation, that references the UUID of the LDAP
+ * object it was ingested from.
+ *
+ * The UUID is the globally unique ID that identifies the item; for example,
+ * for an item with the UUID 76ef928a-b251-1037-9840-d78227f36a7e, the
+ * generated entity would have this annotation, with that full string as its
+ * value.
+ */
+export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid';
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/index.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/index.ts
new file mode 100644
index 0000000000..2f8dae7486
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { LdapClient } from './client';
+export { readLdapConfig } from './config';
+export type { LdapProviderConfig } from './config';
+export {
+ LDAP_DN_ANNOTATION,
+ LDAP_RDN_ANNOTATION,
+ LDAP_UUID_ANNOTATION,
+} from './constants';
+export { readLdapOrg } from './read';
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/read.test.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/read.test.ts
new file mode 100644
index 0000000000..e7a7d1c567
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/read.test.ts
@@ -0,0 +1,393 @@
+/*
+ * 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 { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { SearchEntry } from 'ldapjs';
+import merge from 'lodash/merge';
+import { LdapClient } from './client';
+import { GroupConfig, UserConfig } from './config';
+import {
+ LDAP_DN_ANNOTATION,
+ LDAP_RDN_ANNOTATION,
+ LDAP_UUID_ANNOTATION,
+} from './constants';
+import { readLdapGroups, readLdapUsers, resolveRelations } from './read';
+import { RecursivePartial } from './util';
+
+function user(data: RecursivePartial): UserEntity {
+ return merge(
+ {},
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: { name: 'name' },
+ spec: { profile: {}, memberOf: [] },
+ } as UserEntity,
+ data,
+ );
+}
+
+function group(data: RecursivePartial): GroupEntity {
+ return merge(
+ {},
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: { name: 'name' },
+ spec: { type: 'type', ancestors: [], children: [], descendants: [] },
+ } as GroupEntity,
+ data,
+ );
+}
+
+function searchEntry(attributes: Record): SearchEntry {
+ return {
+ attributes: Object.entries(attributes).map(([k, vs]) => ({
+ json: {
+ type: k,
+ vals: vs,
+ },
+ })),
+ } as any;
+}
+
+describe('readLdapUsers', () => {
+ const client: jest.Mocked = {
+ search: jest.fn(),
+ } as any;
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('transfers all attributes', async () => {
+ client.search.mockResolvedValue([
+ searchEntry({
+ uid: ['uid-value'],
+ description: ['description-value'],
+ cn: ['cn-value'],
+ mail: ['mail-value'],
+ avatarUrl: ['avatarUrl-value'],
+ memberOf: ['x', 'y', 'z'],
+ entryDN: ['dn-value'],
+ entryUUID: ['uuid-value'],
+ }),
+ ]);
+ const config: UserConfig = {
+ dn: 'ddd',
+ options: {},
+ map: {
+ rdn: 'uid',
+ name: 'uid',
+ description: 'description',
+ displayName: 'cn',
+ email: 'mail',
+ picture: 'avatarUrl',
+ memberOf: 'memberOf',
+ },
+ };
+ const { users, userMemberOf } = await readLdapUsers(client, config);
+ expect(users).toEqual([
+ expect.objectContaining({
+ metadata: {
+ name: 'uid-value',
+ description: 'description-value',
+ annotations: {
+ [LDAP_DN_ANNOTATION]: 'dn-value',
+ [LDAP_RDN_ANNOTATION]: 'uid-value',
+ [LDAP_UUID_ANNOTATION]: 'uuid-value',
+ },
+ },
+ spec: {
+ profile: {
+ displayName: 'cn-value',
+ email: 'mail-value',
+ picture: 'avatarUrl-value',
+ },
+ memberOf: [],
+ },
+ }),
+ ]);
+ expect(userMemberOf).toEqual(
+ new Map([['dn-value', new Set(['x', 'y', 'z'])]]),
+ );
+ });
+});
+
+describe('readLdapGroups', () => {
+ const client: jest.Mocked = {
+ search: jest.fn(),
+ } as any;
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('transfers all attributes', async () => {
+ client.search.mockResolvedValue([
+ searchEntry({
+ cn: ['cn-value'],
+ description: ['description-value'],
+ tt: ['type-value'],
+ memberOf: ['x', 'y', 'z'],
+ member: ['e', 'f', 'g'],
+ entryDN: ['dn-value'],
+ entryUUID: ['uuid-value'],
+ }),
+ ]);
+ const config: GroupConfig = {
+ dn: 'ddd',
+ options: {},
+ map: {
+ rdn: 'cn',
+ name: 'cn',
+ description: 'description',
+ type: 'tt',
+ memberOf: 'memberOf',
+ members: 'member',
+ },
+ };
+ const { groups, groupMember, groupMemberOf } = await readLdapGroups(
+ client,
+ config,
+ );
+ expect(groups).toEqual([
+ expect.objectContaining({
+ metadata: {
+ name: 'cn-value',
+ description: 'description-value',
+ annotations: {
+ [LDAP_DN_ANNOTATION]: 'dn-value',
+ [LDAP_RDN_ANNOTATION]: 'cn-value',
+ [LDAP_UUID_ANNOTATION]: 'uuid-value',
+ },
+ },
+ spec: {
+ type: 'type-value',
+ ancestors: [],
+ children: [],
+ descendants: [],
+ },
+ }),
+ ]);
+ expect(groupMember).toEqual(
+ new Map([['dn-value', new Set(['e', 'f', 'g'])]]),
+ );
+ expect(groupMemberOf).toEqual(
+ new Map([['dn-value', new Set(['x', 'y', 'z'])]]),
+ );
+ });
+});
+
+describe('resolveRelations', () => {
+ describe('lookup', () => {
+ it('matches by DN', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_DN_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ca' },
+ },
+ });
+ const groupMember = new Map>([
+ ['pa', new Set(['ca'])],
+ ]);
+ resolveRelations([parent, child], [], new Map(), new Map(), groupMember);
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ });
+ it('matches by UUID', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'ca' },
+ },
+ });
+ const groupMember = new Map>([
+ ['pa', new Set(['ca'])],
+ ]);
+ resolveRelations([parent, child], [], new Map(), new Map(), groupMember);
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ });
+ });
+
+ describe('userMemberOf', () => {
+ it('populates relations by dn', () => {
+ const host = group({
+ metadata: { name: 'host', annotations: { [LDAP_DN_ANNOTATION]: 'ha' } },
+ });
+ const member = user({
+ metadata: {
+ name: 'member',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ma' },
+ },
+ });
+ const userMemberOf = new Map>([
+ ['ma', new Set(['ha'])],
+ ]);
+ resolveRelations([host], [member], userMemberOf, new Map(), new Map());
+ expect(member.spec.memberOf).toEqual(['host']);
+ });
+
+ it('populates relations by uuid', () => {
+ const host = group({
+ metadata: {
+ name: 'host',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'ha' },
+ },
+ });
+ const member = user({
+ metadata: {
+ name: 'member',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ma' },
+ },
+ });
+ const userMemberOf = new Map>([
+ ['ma', new Set(['ha'])],
+ ]);
+ resolveRelations([host], [member], userMemberOf, new Map(), new Map());
+ expect(member.spec.memberOf).toEqual(['host']);
+ });
+ });
+
+ describe('groupMemberOf', () => {
+ it('populates relations by dn', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_DN_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ca' },
+ },
+ });
+ const groupMemberOf = new Map>([
+ ['ca', new Set(['pa'])],
+ ]);
+ resolveRelations(
+ [parent, child],
+ [],
+ new Map(),
+ groupMemberOf,
+ new Map(),
+ );
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ });
+ });
+
+ it('populates relations by uuid', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'ca' },
+ },
+ });
+ const groupMemberOf = new Map>([
+ ['ca', new Set(['pa'])],
+ ]);
+ resolveRelations([parent, child], [], new Map(), groupMemberOf, new Map());
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ });
+
+ describe('groupMember', () => {
+ it('populates relations by dn', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_DN_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ca' },
+ },
+ });
+ const member = user({
+ metadata: {
+ name: 'member',
+ annotations: { [LDAP_DN_ANNOTATION]: 'ma' },
+ },
+ });
+ const groupMember = new Map>([
+ ['pa', new Set(['ca', 'ma'])],
+ ]);
+ resolveRelations(
+ [parent, child],
+ [member],
+ new Map(),
+ new Map(),
+ groupMember,
+ );
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ expect(member.spec.memberOf).toEqual(['parent']);
+ });
+
+ it('populates relations by uuid', () => {
+ const parent = group({
+ metadata: {
+ name: 'parent',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'pa' },
+ },
+ });
+ const child = group({
+ metadata: {
+ name: 'child',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'ca' },
+ },
+ });
+ const member = user({
+ metadata: {
+ name: 'member',
+ annotations: { [LDAP_UUID_ANNOTATION]: 'ma' },
+ },
+ });
+ const groupMember = new Map>([
+ ['pa', new Set(['ca', 'ma'])],
+ ]);
+ resolveRelations(
+ [parent, child],
+ [member],
+ new Map(),
+ new Map(),
+ groupMember,
+ );
+ expect(parent.spec.children).toEqual(['child']);
+ expect(child.spec.parent).toEqual('parent');
+ expect(member.spec.memberOf).toEqual(['parent']);
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts
new file mode 100644
index 0000000000..63fce00bd8
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/read.ts
@@ -0,0 +1,422 @@
+/*
+ * 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 { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import lodashSet from 'lodash/set';
+import { buildOrgHierarchy } from '../util/org';
+import { LdapClient } from './client';
+import { GroupConfig, UserConfig } from './config';
+import {
+ LDAP_DN_ANNOTATION,
+ LDAP_RDN_ANNOTATION,
+ LDAP_UUID_ANNOTATION,
+} from './constants';
+
+// NOTE(freben): These attribute names are assumed to be the same across all
+// compliant LDAP implementations
+const DN_ATTRIBUTE = 'entryDN';
+const UUID_ATTRIBUTE = 'entryUUID';
+
+/**
+ * Reads groups out of an LDAP provider.
+ *
+ * @param client The LDAP client
+ * @param config The user data configuration
+ */
+export async function readLdapUsers(
+ client: LdapClient,
+ config: UserConfig,
+): Promise<{
+ users: UserEntity[]; // With all relations empty
+ userMemberOf: Map>; // DN -> DN or UUID of groups
+}> {
+ const { dn, options, set, map } = config;
+
+ const entries = await client.search(dn, options);
+
+ const entities: UserEntity[] = [];
+ const userMemberOf: Map> = new Map();
+
+ for (const entry of entries) {
+ const attributes = new Map(
+ entry.attributes.map(attr => {
+ const data = attr.json;
+ return [data.type, data.vals];
+ }),
+ );
+
+ const entity: UserEntity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ name: '',
+ annotations: {},
+ },
+ spec: {
+ profile: {},
+ memberOf: [],
+ },
+ };
+
+ if (set) {
+ for (const { path, value } of set) {
+ lodashSet(entity, path, value);
+ }
+ }
+
+ mapStringAttr(attributes, map.name, v => {
+ entity.metadata.name = v;
+ });
+ mapStringAttr(attributes, map.description, v => {
+ entity.metadata.description = v;
+ });
+ mapStringAttr(attributes, map.rdn, v => {
+ entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, UUID_ATTRIBUTE, v => {
+ entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, DN_ATTRIBUTE, v => {
+ entity.metadata.annotations![LDAP_DN_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, map.displayName, v => {
+ entity.spec.profile!.displayName = v;
+ });
+ mapStringAttr(attributes, map.email, v => {
+ entity.spec.profile!.email = v;
+ });
+ mapStringAttr(attributes, map.picture, v => {
+ entity.spec.profile!.picture = v;
+ });
+
+ mapReferencesAttr(attributes, map.memberOf, (myDn, vs) => {
+ ensureItems(userMemberOf, myDn, vs);
+ });
+
+ entities.push(entity);
+ }
+
+ return { users: entities, userMemberOf };
+}
+
+/**
+ * Reads groups out of an LDAP provider.
+ *
+ * @param client The LDAP client
+ * @param config The group data configuration
+ */
+export async function readLdapGroups(
+ client: LdapClient,
+ config: GroupConfig,
+): Promise<{
+ groups: GroupEntity[]; // With all relations empty
+ groupMemberOf: Map>; // DN -> DN or UUID of groups
+ groupMember: Map>; // DN -> DN or UUID of groups & users
+}> {
+ const { dn, options, set, map } = config;
+ const entries = await client.search(dn, options);
+
+ const groups: GroupEntity[] = [];
+ const groupMemberOf: Map> = new Map();
+ const groupMember: Map> = new Map();
+
+ for (const entry of entries) {
+ const attributes = new Map(
+ entry.attributes.map(attr => {
+ const data = attr.json;
+ return [data.type, data.vals];
+ }),
+ );
+
+ const entity: GroupEntity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: '',
+ annotations: {},
+ },
+ spec: {
+ type: 'unknown',
+ ancestors: [],
+ children: [],
+ descendants: [],
+ },
+ };
+
+ if (set) {
+ for (const { path, value } of set) {
+ lodashSet(entity, path, value);
+ }
+ }
+
+ mapStringAttr(attributes, map.name, v => {
+ entity.metadata.name = v;
+ });
+ mapStringAttr(attributes, map.description, v => {
+ entity.metadata.description = v;
+ });
+ mapStringAttr(attributes, map.rdn, v => {
+ entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, UUID_ATTRIBUTE, v => {
+ entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, DN_ATTRIBUTE, v => {
+ entity.metadata.annotations![LDAP_DN_ANNOTATION] = v;
+ });
+ mapStringAttr(attributes, map.type, v => {
+ entity.spec.type = v;
+ });
+
+ mapReferencesAttr(attributes, map.memberOf, (myDn, vs) => {
+ ensureItems(groupMemberOf, myDn, vs);
+ });
+ mapReferencesAttr(attributes, map.members, (myDn, vs) => {
+ ensureItems(groupMember, myDn, vs);
+ });
+
+ groups.push(entity);
+ }
+
+ return {
+ groups,
+ groupMemberOf,
+ groupMember,
+ };
+}
+
+/**
+ * Reads users and groups out of an LDAP provider.
+ *
+ * Invokes the above "raw" read functions and stitches together the results
+ * with all relations etc filled in.
+ *
+ * @param client The LDAP client
+ * @param logger A logger instance
+ * @param userConfig The user data configuration
+ * @param groupConfig The group data configuration
+ */
+export async function readLdapOrg(
+ client: LdapClient,
+ userConfig: UserConfig,
+ groupConfig: GroupConfig,
+): Promise<{
+ users: UserEntity[];
+ groups: GroupEntity[];
+}> {
+ const { users, userMemberOf } = await readLdapUsers(client, userConfig);
+ const { groups, groupMemberOf, groupMember } = await readLdapGroups(
+ client,
+ groupConfig,
+ );
+
+ resolveRelations(groups, users, userMemberOf, groupMemberOf, groupMember);
+ users.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
+ groups.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
+
+ return { users, groups };
+}
+
+//
+// Helpers
+//
+
+// Maps a single-valued attribute to a consumer
+function mapStringAttr(
+ attributes: Map,
+ attributeName: string | undefined,
+ setter: (value: string) => void,
+) {
+ if (attributeName) {
+ const values = attributes.get(attributeName);
+ if (values && values.length === 1) {
+ setter(values[0]);
+ }
+ }
+}
+
+// Maps a multi-valued attribute of references to other objects, to a consumer
+function mapReferencesAttr(
+ attributes: Map,
+ attributeName: string | undefined,
+ setter: (sourceDn: string, targets: string[]) => void,
+) {
+ if (attributeName) {
+ const values = attributes.get(attributeName);
+ const dn = attributes.get(DN_ATTRIBUTE);
+ if (values && dn && dn.length === 1) {
+ setter(dn[0], values);
+ }
+ }
+}
+
+// Inserts a number of values in a key-values mapping
+function ensureItems(
+ target: Map>,
+ key: string,
+ values: string[],
+) {
+ if (key) {
+ let set = target.get(key);
+ if (!set) {
+ set = new Set();
+ target.set(key, set);
+ }
+ for (const value of values) {
+ if (value) {
+ set!.add(value);
+ }
+ }
+ }
+}
+
+/**
+ * Takes groups and entities with empty relations, and fills in the various
+ * relations that were returned by the readers, and forms the org hierarchy.
+ *
+ * @param groups Group entities with empty relations; modified in place
+ * @param users User entities with empty relations; modified in place
+ * @param userMemberOf For a user DN, the set of group DNs or UUIDs that the
+ * user is a member of
+ * @param groupMemberOf For a group DN, the set of group DNs or UUIDs that the
+ * group is a member of (parents in the hierarchy)
+ * @param groupMember For a group DN, the set of group DNs or UUIDs that are
+ * members of the group (children in the hierarchy)
+ */
+export function resolveRelations(
+ groups: GroupEntity[],
+ users: UserEntity[],
+ userMemberOf: Map>,
+ groupMemberOf: Map>,
+ groupMember: Map>,
+) {
+ // Build reference lookup tables - all of the relations that are output from
+ // the above calls can be expressed as either DNs or UUIDs so we need to be
+ // able to find by both, as well as the name. Note that we expect them to not
+ // collide here - this is a reasonable assumption as long as the fields are
+ // the supported forms.
+ const userMap: Map = new Map(); // by name, dn, uuid
+ const groupMap: Map = new Map(); // by name, dn, uuid
+ for (const user of users) {
+ userMap.set(user.metadata.name, user);
+ userMap.set(user.metadata.annotations![LDAP_DN_ANNOTATION], user);
+ userMap.set(user.metadata.annotations![LDAP_UUID_ANNOTATION], user);
+ }
+ for (const group of groups) {
+ groupMap.set(group.metadata.name, group);
+ groupMap.set(group.metadata.annotations![LDAP_DN_ANNOTATION], group);
+ groupMap.set(group.metadata.annotations![LDAP_UUID_ANNOTATION], group);
+ }
+
+ // This can happen e.g. if entryUUID wasn't returned by the server
+ userMap.delete('');
+ groupMap.delete('');
+ userMap.delete(undefined!);
+ groupMap.delete(undefined!);
+
+ // Fill in all of the immediate relations, now keyed on metadata.name. We
+ // keep all parents at this point, whether the target model can support more
+ // than one or not (it gets filtered farther down). And group children are
+ // only groups in here.
+ const newUserMemberOf: Map> = new Map();
+ const newGroupParents: Map> = new Map();
+ const newGroupChildren: Map> = new Map();
+
+ // Resolve and store in the intermediaries. It may seem redundant that the
+ // input data has both parent and children directions, as well as both
+ // user->group and group->user - the reason is that different LDAP schemas
+ // express relations in different directions. Some may have a user memberOf
+ // overlay, some don't, for example.
+ for (const [userN, groupsN] of userMemberOf.entries()) {
+ const user = userMap.get(userN);
+ if (user) {
+ for (const groupN of groupsN) {
+ const group = groupMap.get(groupN);
+ if (group) {
+ ensureItems(newUserMemberOf, user.metadata.name, [
+ group.metadata.name,
+ ]);
+ }
+ }
+ }
+ }
+ for (const [groupN, parentsN] of groupMemberOf.entries()) {
+ const group = groupMap.get(groupN);
+ if (group) {
+ for (const parentN of parentsN) {
+ const parentGroup = groupMap.get(parentN);
+ if (parentGroup) {
+ ensureItems(newGroupParents, group.metadata.name, [
+ parentGroup.metadata.name,
+ ]);
+ ensureItems(newGroupChildren, parentGroup.metadata.name, [
+ group.metadata.name,
+ ]);
+ }
+ }
+ }
+ }
+ for (const [groupN, membersN] of groupMember.entries()) {
+ const group = groupMap.get(groupN);
+ if (group) {
+ for (const memberN of membersN) {
+ // Group members can be both users and groups in the input model, so
+ // try both
+ const memberUser = userMap.get(memberN);
+ if (memberUser) {
+ ensureItems(newUserMemberOf, memberUser.metadata.name, [
+ group.metadata.name,
+ ]);
+ } else {
+ const memberGroup = groupMap.get(memberN);
+ if (memberGroup) {
+ ensureItems(newGroupChildren, group.metadata.name, [
+ memberGroup.metadata.name,
+ ]);
+ ensureItems(newGroupParents, memberGroup.metadata.name, [
+ group.metadata.name,
+ ]);
+ }
+ }
+ }
+ }
+ }
+
+ // Write down the relations again into the actual entities
+ for (const [userN, groupsN] of newUserMemberOf.entries()) {
+ const user = userMap.get(userN);
+ if (user) {
+ user.spec.memberOf = Array.from(groupsN).sort();
+ }
+ }
+ for (const [groupN, parentsN] of newGroupParents.entries()) {
+ if (parentsN.size === 1) {
+ const group = groupMap.get(groupN);
+ if (group) {
+ group.spec.parent = parentsN.values().next().value;
+ }
+ }
+ }
+ for (const [groupN, childrenN] of newGroupChildren.entries()) {
+ const group = groupMap.get(groupN);
+ if (group) {
+ group.spec.children = Array.from(childrenN).sort();
+ }
+ }
+
+ // Fill out the rest of the hierarchy
+ buildOrgHierarchy(groups);
+}
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/util.test.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/util.test.ts
new file mode 100644
index 0000000000..2def6ad960
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/util.test.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { errorString, RecursivePartial } from './util';
+
+describe('errorString', () => {
+ it('formats', () => {
+ const e = { code: 1, name: 'n', message: 'm' };
+ expect(errorString(e)).toEqual('1 n: m');
+ });
+});
+
+describe('RecursivePartial', () => {
+ it('is recursive', () => {
+ type X = {
+ required: {
+ required: string;
+ };
+ };
+ const x: RecursivePartial = {
+ required: {},
+ };
+ expect(x).toEqual({ required: {} });
+ });
+});
diff --git a/plugins/catalog-backend/src/ingestion/processors/ldap/util.ts b/plugins/catalog-backend/src/ingestion/processors/ldap/util.ts
new file mode 100644
index 0000000000..29159dc219
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/processors/ldap/util.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { Error as LDAPError } from 'ldapjs';
+
+/**
+ * Builds a string form of an LDAP Error structure.
+ *
+ * @param error The error
+ */
+export function errorString(error: LDAPError) {
+ return `${error.code} ${error.name}: ${error.message}`;
+}
+
+/**
+ * Makes all keys of an entire hierarchy optional.
+ */
+export type RecursivePartial = {
+ [P in keyof T]?: T[P] extends (infer U)[]
+ ? RecursivePartial[]
+ : T[P] extends object
+ ? RecursivePartial
+ : T[P];
+};
diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts
index 66359b328b..cd2c423423 100644
--- a/plugins/catalog-backend/src/ingestion/processors/types.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/types.ts
@@ -58,7 +58,6 @@ export type LocationProcessor = {
entity: Entity,
location: LocationSpec,
emit: LocationProcessorEmit,
- read: LocationProcessorRead,
): Promise;
/**
@@ -109,5 +108,3 @@ export type LocationProcessorResult =
| LocationProcessorDataResult
| LocationProcessorEntityResult
| LocationProcessorErrorResult;
-
-export type LocationProcessorRead = (location: LocationSpec) => Promise;
diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts b/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts
index e0120735fe..e9dddc8226 100644
--- a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts
@@ -14,18 +14,9 @@
* limitations under the License.
*/
-import { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { GroupEntity } from '@backstage/catalog-model';
import { buildOrgHierarchy } from './org';
-function u(name: string): UserEntity {
- return {
- apiVersion: 'backstage.io/v1alpha1',
- kind: 'User',
- metadata: { name },
- spec: { memberOf: [] },
- };
-}
-
function g(
name: string,
parent: string | undefined,
@@ -40,26 +31,12 @@ function g(
}
describe('buildOrgHierarchy', () => {
- it('puts users in the respective groups', () => {
- const a = g('a', undefined, []);
- const b = g('b', undefined, []);
- const x = u('x');
- const y = u('y');
- const groupMemberUsers: Map = new Map([
- ['a', ['x', 'y']],
- ['b', ['y']],
- ]);
- buildOrgHierarchy([a, b], [x, y], groupMemberUsers);
- expect(x.spec.memberOf).toEqual(['a']);
- expect(y.spec.memberOf).toEqual(['a', 'b']);
- });
-
it('adds groups to their parent.children', () => {
const a = g('a', undefined, []);
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const d = g('d', 'a', []);
- buildOrgHierarchy([a, b, c, d], [], new Map());
+ buildOrgHierarchy([a, b, c, d]);
expect(a.spec.children).toEqual(expect.arrayContaining(['b', 'd']));
expect(b.spec.children).toEqual(expect.arrayContaining(['c']));
expect(c.spec.children).toEqual([]);
@@ -71,7 +48,7 @@ describe('buildOrgHierarchy', () => {
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const d = g('d', 'a', []);
- buildOrgHierarchy([a, b, c, d], [], new Map());
+ buildOrgHierarchy([a, b, c, d]);
expect(a.spec.descendants).toEqual(expect.arrayContaining(['b', 'c', 'd']));
expect(b.spec.descendants).toEqual(expect.arrayContaining(['c']));
expect(c.spec.descendants).toEqual([]);
@@ -83,7 +60,7 @@ describe('buildOrgHierarchy', () => {
const b = g('b', 'a', []);
const c = g('c', 'b', []);
const d = g('d', 'a', []);
- buildOrgHierarchy([a, b, c, d], [], new Map());
+ buildOrgHierarchy([a, b, c, d]);
expect(a.spec.ancestors).toEqual([]);
expect(b.spec.ancestors).toEqual(expect.arrayContaining(['a']));
expect(c.spec.ancestors).toEqual(expect.arrayContaining(['a', 'b']));
diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.ts b/plugins/catalog-backend/src/ingestion/processors/util/org.ts
index bcc4c7b5e6..a280a265a8 100644
--- a/plugins/catalog-backend/src/ingestion/processors/util/org.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/util/org.ts
@@ -14,28 +14,10 @@
* limitations under the License.
*/
-import { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { GroupEntity } from '@backstage/catalog-model';
-export function buildOrgHierarchy(
- groups: GroupEntity[],
- users: UserEntity[],
- groupMemberUsers: Map,
-) {
+export function buildOrgHierarchy(groups: GroupEntity[]) {
const groupsByName = new Map(groups.map(g => [g.metadata.name, g]));
- const usersByName = new Map(users.map(u => [u.metadata.name, u]));
-
- //
- // Make sure that u.memberOf contain all g
- //
-
- for (const [groupName, userNames] of groupMemberUsers.entries()) {
- for (const userName of userNames) {
- const user = usersByName.get(userName);
- if (user && !user.spec.memberOf.includes(groupName)) {
- user.spec.memberOf.push(groupName);
- }
- }
- }
//
// Make sure that g.parent.children contain g
diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts
index 0b532d1664..5939c7eb58 100644
--- a/plugins/catalog-backend/src/service/standaloneServer.ts
+++ b/plugins/catalog-backend/src/service/standaloneServer.ts
@@ -17,6 +17,7 @@
import {
createServiceBuilder,
loadBackendConfig,
+ UrlReaders,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Server } from 'http';
@@ -39,12 +40,13 @@ export async function startStandaloneServer(
): Promise {
const logger = options.logger.child({ service: 'catalog-backend' });
const config = ConfigReader.fromConfigs(await loadBackendConfig());
+ const reader = UrlReaders.default({ logger, config });
logger.debug('Creating application...');
const db = await DatabaseManager.createInMemoryDatabase({ logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
- const locationReader = new LocationReaders({ logger, config });
+ const locationReader = new LocationReaders({ logger, config, reader });
const higherOrderOperation = new HigherOrderOperations(
entitiesCatalog,
locationsCatalog,
diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json
index 8597f3aa3c..0c18624e0f 100644
--- a/plugins/catalog-graphql/package.json
+++ b/plugins/catalog-graphql/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-graphql",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@graphql-modules/core": "^0.7.17",
"apollo-server": "^2.16.1",
"graphql": "^15.3.0",
@@ -32,7 +32,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@graphql-codegen/cli": "^1.17.7",
"@graphql-codegen/typescript": "^1.17.7",
"@graphql-codegen/typescript-resolvers": "^1.17.7",
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index d3241ec10d..ea270b6fe9 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,11 +21,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-scaffolder": "^0.1.1-alpha.23",
- "@backstage/plugin-techdocs": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-scaffolder": "^0.1.1-alpha.24",
+ "@backstage/plugin-techdocs": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -41,9 +41,9 @@
"swr": "^0.3.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
@@ -52,9 +52,9 @@
"@types/node": "^12.0.0",
"jest-fetch-mock": "^3.0.3",
"msw": "^0.20.5",
+ "node-fetch": "^2.6.1",
"react-test-renderer": "^16.13.1",
- "whatwg-fetch": "^3.4.0",
- "node-fetch": "^2.6.1"
+ "whatwg-fetch": "^3.4.0"
},
"files": [
"dist"
diff --git a/plugins/catalog/src/components/EntityNotFound/illo.svg b/plugins/catalog/src/components/EntityNotFound/illo.svg
index df97dfe063..849ec2fed8 100644
--- a/plugins/catalog/src/components/EntityNotFound/illo.svg
+++ b/plugins/catalog/src/components/EntityNotFound/illo.svg
@@ -1,37 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index a6d86e8102..762f4924c0 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -20,6 +20,6 @@ export * from './api/types';
export * from './routes';
export { useEntityCompoundName } from './components/useEntityCompoundName';
export { Router } from './components/Router';
-export { useEntity } from './hooks/useEntity';
+export { useEntity, EntityContext } from './hooks/useEntity';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md
index 19c2c35899..d329768514 100644
--- a/plugins/circleci/README.md
+++ b/plugins/circleci/README.md
@@ -57,8 +57,7 @@ proxy:
target: https://circleci.com/api/v1.1
headers:
Circle-Token:
- $secret:
- env: CIRCLECI_AUTH_TOKEN
+ $env: CIRCLECI_AUTH_TOKEN
```
5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token)
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index a661c2562f..24dc8a40e1 100644
--- a/plugins/circleci/package.json
+++ b/plugins/circleci/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"postpack": "backstage-cli postpack"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -38,8 +38,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/circleci/src/assets/screenshot-1.png b/plugins/circleci/src/assets/screenshot-1.png
index db99230a65..bef8a5a06f 100644
Binary files a/plugins/circleci/src/assets/screenshot-1.png and b/plugins/circleci/src/assets/screenshot-1.png differ
diff --git a/plugins/circleci/src/assets/screenshot-2.png b/plugins/circleci/src/assets/screenshot-2.png
index 4f9ddcaec6..e4b8299013 100644
Binary files a/plugins/circleci/src/assets/screenshot-2.png and b/plugins/circleci/src/assets/screenshot-2.png differ
diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json
index 3367bb6cca..1caac1ecc5 100644
--- a/plugins/cloudbuild/package.json
+++ b/plugins/cloudbuild/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cloudbuild",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md
index 221f9f37d8..01dd415ec2 100644
--- a/plugins/cost-insights/README.md
+++ b/plugins/cost-insights/README.md
@@ -9,7 +9,7 @@ At Spotify, we find that cloud costs are optimized organically when:
- The data is shown in software terms familiar to them.
- Alerts and recommendations are targeted and actionable.
-Cost Insights shows trends over time, at the granularity of your software deployments - rather than the cloud provider's concepts. It can be used to troubleshoot cost anomalies, and promote cost-saving infrastructure migrations.
+Cost Insights shows trends over time, at the granularity of Backstage catalog entities - rather than the cloud provider's concepts. It can be used to troubleshoot cost anomalies, and promote cost-saving infrastructure migrations.
## Install
diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json
index 96a74cb746..a9b6a646c4 100644
--- a/plugins/cost-insights/package.json
+++ b/plugins/cost-insights/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cost-insights",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -41,8 +41,8 @@
"recharts": "^1.8.5"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
index 2f25b4f137..5bd78ab088 100644
--- a/plugins/explore/package.json
+++ b/plugins/explore/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-explore",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -33,9 +33,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json
index 406b2428df..18a957d701 100644
--- a/plugins/gcp-projects/package.json
+++ b/plugins/gcp-projects/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gcp-projects",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -31,8 +31,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/github-actions/README.md b/plugins/github-actions/README.md
index 24bc96937f..07a69cdfad 100644
--- a/plugins/github-actions/README.md
+++ b/plugins/github-actions/README.md
@@ -11,7 +11,7 @@ TBD
### Generic Requirements
1. Provide OAuth credentials:
- 1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `https://localhost:3000/auth/github`.
+ 1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `http://localhost:7000/api/auth/github`.
2. Take Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` env variables.
2. Annotate your component with a correct GitHub Actions repository and owner:
diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json
index 5d85d8b3fa..a43e351d47 100644
--- a/plugins/github-actions/package.json
+++ b/plugins/github-actions/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-actions",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,11 +21,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/core-api": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/core-api": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx
index 28783efdea..7e4d5f5fb6 100644
--- a/plugins/github-actions/src/components/Cards/Cards.tsx
+++ b/plugins/github-actions/src/components/Cards/Cards.tsx
@@ -60,7 +60,10 @@ const WidgetContent = ({
metadata={{
status: (
<>
-
+
>
),
message: lastRun.message,
diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx
index 014aed1633..e5bba7fad9 100644
--- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx
+++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx
@@ -18,9 +18,9 @@ import { errorApiRef, useApi } from '@backstage/core-api';
import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName';
import { useWorkflowRuns } from '../useWorkflowRuns';
import React, { useEffect } from 'react';
-import { Table } from '@backstage/core';
+import { EmptyState, Table } from '@backstage/core';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
-import { Card, Link, TableContainer } from '@material-ui/core';
+import { Button, Card, Link, TableContainer } from '@material-ui/core';
import { generatePath, Link as RouterLink } from 'react-router-dom';
const firstLine = (message: string): string => message.split('\n')[0];
@@ -54,7 +54,22 @@ export const RecentWorkflowRunsCard = ({
}
}, [error, errorApi]);
- return (
+ return !runs.length ? (
+
+ Create new Workflow
+
+ }
+ />
+ ) : (
Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]);
export const Router = ({ entity }: { entity: Entity }) =>
- // TODO(shmidt-i): move warning to a separate standardized component
!isPluginApplicableToEntity(entity) ? (
-
- {GITHUB_ACTIONS_ANNOTATION} annotation is missing on the
- entity.
-
+
) : (
{
/>
-
+
);
@@ -204,7 +207,10 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
Status
-
+
diff --git a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
index 089c319802..b1a71ea3ed 100644
--- a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
+++ b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
@@ -14,13 +14,22 @@
* limitations under the License.
*/
-import { StatusPending, StatusRunning, StatusOK } from '@backstage/core';
+import {
+ StatusPending,
+ StatusRunning,
+ StatusOK,
+ StatusWarning,
+ StatusAborted,
+ StatusError,
+} from '@backstage/core';
import React from 'react';
export const WorkflowRunStatus = ({
status,
+ conclusion,
}: {
status: string | undefined;
+ conclusion: string | undefined;
}) => {
if (status === undefined) return null;
switch (status.toLowerCase()) {
@@ -37,11 +46,32 @@ export const WorkflowRunStatus = ({
>
);
case 'completed':
- return (
- <>
- Completed
- >
- );
+ switch (conclusion?.toLowerCase()) {
+ case 'skipped' || 'canceled':
+ return (
+ <>
+ Aborted
+ >
+ );
+ case 'timed_out':
+ return (
+ <>
+ Timed out
+ >
+ );
+ case 'failure':
+ return (
+ <>
+ Error
+ >
+ );
+ default:
+ return (
+ <>
+ Completed
+ >
+ );
+ }
default:
return (
<>
diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
index cc834f09ee..fcde5fefbb 100644
--- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
+++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
@@ -14,11 +14,18 @@
* limitations under the License.
*/
import React, { FC } from 'react';
-import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core';
+import {
+ Link,
+ Typography,
+ Box,
+ IconButton,
+ Tooltip,
+ Button,
+} from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
import GitHubIcon from '@material-ui/icons/GitHub';
import { Link as RouterLink, generatePath } from 'react-router-dom';
-import { Table, TableColumn } from '@backstage/core';
+import { EmptyState, Table, TableColumn } from '@backstage/core';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import SyncIcon from '@material-ui/icons/Sync';
@@ -39,6 +46,7 @@ export type WorkflowRun = {
};
};
status: string;
+ conclusion: string;
onReRunClick: () => void;
};
@@ -77,7 +85,7 @@ const generatedColumns: TableColumn[] = [
render: (row: Partial) => (
-
+
),
},
@@ -156,15 +164,34 @@ export const WorkflowRunsTable = ({
}) => {
const { value: projectName, loading } = useProjectName(entity);
const [owner, repo] = (projectName ?? '/').split('/');
- const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
+ const [
+ { runs, ...tableProps },
+ { retry, setPage, setPageSize },
+ ] = useWorkflowRuns({
owner,
repo,
branch,
});
- return (
+ return !runs ? (
+
+ Create new Workflow
+
+ }
+ />
+ ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json
index f008e6f51e..edd3f2782e 100644
--- a/plugins/graphql/package.json
+++ b/plugins/graphql/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-graphql-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -19,9 +19,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog-graphql": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog-graphql": "^0.1.1-alpha.24",
"@graphql-modules/core": "^0.7.17",
"@types/express": "^4.17.6",
"apollo-server": "^2.16.1",
@@ -36,7 +36,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/supertest": "^2.0.8",
"eslint-plugin-graphql": "^4.0.0",
"msw": "^0.20.5",
diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md
index 3f5334b94f..565906ca85 100644
--- a/plugins/jenkins/README.md
+++ b/plugins/jenkins/README.md
@@ -30,8 +30,7 @@ proxy:
changeOrigin: true
headers:
Authorization:
- $secret:
- env: JENKINS_BASIC_AUTH_HEADER
+ $env: JENKINS_BASIC_AUTH_HEADER
```
4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password)
diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json
index 84e8a47664..186fdbcc33 100644
--- a/plugins/jenkins/package.json
+++ b/plugins/jenkins/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-jenkins",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/jenkins/src/assets/build-details.png b/plugins/jenkins/src/assets/build-details.png
index ee463fea43..a333fb6611 100644
Binary files a/plugins/jenkins/src/assets/build-details.png and b/plugins/jenkins/src/assets/build-details.png differ
diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png
index c01e4cf437..2c14d5d927 100644
Binary files a/plugins/jenkins/src/assets/folder-results.png and b/plugins/jenkins/src/assets/folder-results.png differ
diff --git a/plugins/jenkins/src/assets/last-master-build.png b/plugins/jenkins/src/assets/last-master-build.png
index 517e7a4410..4174f08997 100644
Binary files a/plugins/jenkins/src/assets/last-master-build.png and b/plugins/jenkins/src/assets/last-master-build.png differ
diff --git a/plugins/kubernetes-backend/README.md b/plugins/kubernetes-backend/README.md
index 0246da24ed..fbbde1fe65 100644
--- a/plugins/kubernetes-backend/README.md
+++ b/plugins/kubernetes-backend/README.md
@@ -6,6 +6,66 @@ This is the backend part of the Kubernetes plugin.
It responds to Kubernetes requests from the frontend.
-## Links
+## Configuration
-- [The Backstage homepage](https://backstage.io)
+### clusterLocatorMethod
+
+This configures how to determine which clusters a component is running in.
+
+Currently, the only valid locator method is:
+
+#### configMultiTenant
+
+This configuration assumes that all components run on all the provided clusters.
+
+Example:
+
+```yaml
+kubernetes:
+ clusterLocatorMethod: 'configMultiTenant'
+ clusters:
+ - url: http://127.0.0.1:9999
+ name: minikube
+ serviceAccountToken:
+ authProvider: 'serviceAccount'
+ - url: http://127.0.0.2:9999
+ name: gke-cluster-1
+ authProvider: 'google'
+```
+
+##### clusters
+
+Used by the `configMultiTenant` `clusterLocatorMethod` to construct Kubernetes clients.
+
+###### url
+
+The base url to the Kubernetes control plane. Can be found by using the `Kubernetes master` result from running the `kubectl cluster-info` command.
+
+###### name
+
+A name to represent this cluster, this must be unique within the `clusters` array. Users will see this value in the Service Catalog Kubernetes plugin.
+
+###### authProvider
+
+This determines how the Kubernetes client authenticate with the Kubernetes cluster. Valid values are:
+
+| Value | Description |
+| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
+| `google` | This will use a user's google auth token from the [google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
+
+###### serviceAccount (optional)
+
+The service account token to be used when using the `authProvider`, `serviceAccount`.
+
+## RBAC
+
+The current RBAC permissions required are read-only cluster wide, for the following objects:
+
+- pods
+- services
+- configmaps
+- deployments
+- replicasets
+- horizontalpodautoscalers
+- ingresses
diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md
index 47fc345c73..f97f760c9f 100644
--- a/plugins/kubernetes-backend/examples/dice-roller/README.md
+++ b/plugins/kubernetes-backend/examples/dice-roller/README.md
@@ -1,6 +1,6 @@
# Dice roller
-An app to roll dice (it doesn't actually do that).
+This can be used to run the kubernetes plugin locally against a mock service.
# Viewing in local Minikube running Backstage locally
@@ -23,22 +23,24 @@ An app to roll dice (it doesn't actually do that).
6. Register existing component in Backstage
- https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml
-Update `app-config.yaml` as follows.
+Update `app-config.development.yaml` as follows.
```yaml
----
kubernetes:
clusterLocatorMethod: 'configMultiTenant'
clusters:
- url:
name: minikube
serviceAccountToken:
+ authProvider: 'serviceAccount'
```
### Getting the service account token
+Mac copy to clipboard:
+
```
-kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
+kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r .secrets[0].name) -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
```
-Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken`
+Paste into `app-config.development.yaml` `kubernetes.clusters[0].serviceAccountToken`
diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json
index 4ff8149322..a44199ec8b 100644
--- a/plugins/kubernetes-backend/package.json
+++ b/plugins/kubernetes-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-kubernetes-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
- "@types/express": "^4.17.6",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@kubernetes/client-node": "^0.12.1",
+ "@types/express": "^4.17.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
@@ -37,9 +37,9 @@
"yn": "^4.0.0"
},
"devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.24",
"jest-fetch-mock": "^3.0.3",
- "supertest": "^4.0.2",
- "@backstage/cli": "^0.1.1-alpha.23"
+ "supertest": "^4.0.2"
},
"files": [
"dist"
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
index 34788d16cb..dd77a11bae 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
@@ -43,6 +43,7 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
+ authProvider: 'serviceAccount',
},
],
},
@@ -60,6 +61,7 @@ describe('MultiTenantConfigClusterLocator', () => {
name: 'cluster1',
serviceAccountToken: undefined,
url: 'http://localhost:8080',
+ authProvider: 'serviceAccount',
},
]);
});
@@ -70,13 +72,14 @@ describe('MultiTenantConfigClusterLocator', () => {
clusters: [
{
name: 'cluster1',
- serviceAccountToken: undefined,
+ serviceAccountToken: 'token',
url: 'http://localhost:8080',
+ authProvider: 'serviceAccount',
},
{
name: 'cluster2',
- serviceAccountToken: undefined,
url: 'http://localhost:8081',
+ authProvider: 'google',
},
],
},
@@ -92,13 +95,15 @@ describe('MultiTenantConfigClusterLocator', () => {
expect(result).toStrictEqual([
{
name: 'cluster1',
- serviceAccountToken: undefined,
+ serviceAccountToken: 'token',
url: 'http://localhost:8080',
+ authProvider: 'serviceAccount',
},
{
name: 'cluster2',
serviceAccountToken: undefined,
url: 'http://localhost:8081',
+ authProvider: 'google',
},
]);
});
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
index 3d9e2dd8b4..5a1f0f3839 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
@@ -28,12 +28,15 @@ export class MultiTenantConfigClusterLocator
}
static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
+ // TODO: Add validation that authProvider is required and serviceAccountToken
+ // is required if authProvider is serviceAccount
return new MultiTenantConfigClusterLocator(
config.map(c => {
return {
name: c.getString('name'),
url: c.getString('url'),
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
+ authProvider: c.getString('authProvider'),
};
}),
);
diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts
index 9b793da558..dae8eb6d60 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/types.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/types.ts
@@ -15,3 +15,4 @@
*/
export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
+export type AuthProviderType = 'google' | 'serviceAccount';
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts
new file mode 100644
index 0000000000..7f9b2f0bae
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { KubernetesAuthTranslator } from './types';
+import { AuthRequestBody, ClusterDetails } from '../types/types';
+
+export class GoogleKubernetesAuthTranslator
+ implements KubernetesAuthTranslator {
+ async decorateClusterDetailsWithAuth(
+ clusterDetails: ClusterDetails,
+ requestBody: AuthRequestBody,
+ ): Promise {
+ const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
+ {},
+ clusterDetails,
+ );
+ const authToken: string | undefined = requestBody.auth?.google;
+
+ if (authToken) {
+ clusterDetailsWithAuthToken.serviceAccountToken = authToken;
+ } else {
+ throw new Error(
+ 'Google token not found under auth.google in request body',
+ );
+ }
+ return clusterDetailsWithAuthToken;
+ }
+}
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts
new file mode 100644
index 0000000000..3b27eb012a
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { KubernetesAuthTranslator } from './types';
+import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
+import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
+import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
+
+describe('getKubernetesAuthTranslatorInstance', () => {
+ const sut = KubernetesAuthTranslatorGenerator;
+
+ it('can return an auth translator for google auth', () => {
+ const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
+ 'google',
+ );
+ expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
+ });
+
+ it('can return an auth translator for serviceAccount auth', () => {
+ const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
+ 'serviceAccount',
+ );
+ expect(
+ authTranslator instanceof ServiceAccountKubernetesAuthTranslator,
+ ).toBe(true);
+ });
+
+ it('throws an error when asked for an auth translator for an unsupported auth type', () => {
+ expect(() => sut.getKubernetesAuthTranslatorInstance('linode')).toThrow(
+ 'authProvider "linode" has no KubernetesAuthTranslator associated with it',
+ );
+ });
+});
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
new file mode 100644
index 0000000000..f7cfc92112
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { KubernetesAuthTranslator } from './types';
+import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
+import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
+
+export class KubernetesAuthTranslatorGenerator {
+ static getKubernetesAuthTranslatorInstance(
+ authProvider: String,
+ ): KubernetesAuthTranslator {
+ switch (authProvider) {
+ case 'google': {
+ return new GoogleKubernetesAuthTranslator();
+ }
+ case 'serviceAccount': {
+ return new ServiceAccountKubernetesAuthTranslator();
+ }
+ default: {
+ throw new Error(
+ `authProvider "${authProvider}" has no KubernetesAuthTranslator associated with it`,
+ );
+ }
+ }
+ }
+}
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts
new file mode 100644
index 0000000000..ecf2f12b72
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { KubernetesAuthTranslator } from './types';
+import { AuthRequestBody, ClusterDetails } from '../types/types';
+
+export class ServiceAccountKubernetesAuthTranslator
+ implements KubernetesAuthTranslator {
+ async decorateClusterDetailsWithAuth(
+ clusterDetails: ClusterDetails,
+ // To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
+ // @ts-ignore-start
+ requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
+ // @ts-ignore-end
+ ): Promise {
+ return clusterDetails;
+ }
+}
diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts
similarity index 61%
rename from packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx
rename to plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts
index 34cf6fb837..f89e04456f 100644
--- a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts
@@ -14,16 +14,11 @@
* limitations under the License.
*/
-import React from 'react';
-import List from '@material-ui/core/List';
-import ListSubheader from '@material-ui/core/ListSubheader';
+import { AuthRequestBody, ClusterDetails } from '../types/types';
-type Props = {
- providerSettings: React.ReactNode;
-};
-
-export const AuthProvidersList = ({ providerSettings }: Props) => (
- Available Auth Providers}>
- {providerSettings}
-
-);
+export interface KubernetesAuthTranslator {
+ decorateClusterDetailsWithAuth(
+ clusterDetails: ClusterDetails,
+ requestBody: AuthRequestBody,
+ ): Promise;
+}
diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
index a3dc67716a..409d41b783 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
@@ -33,6 +33,7 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
expect(result.basePath).toBe('http://localhost:9999');
@@ -55,12 +56,14 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
const result2 = sut.getCoreClientByClusterDetails({
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
expect(result1.basePath).toBe('http://localhost:9999');
@@ -89,6 +92,7 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
expect(result.basePath).toBe('http://localhost:9999');
@@ -111,12 +115,14 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
const result2 = sut.getAppsClientByClusterDetails({
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
+ authProvider: 'serviceAccount',
});
expect(result1.basePath).toBe('http://localhost:9999');
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
index 8f390f309b..6294b3a7ed 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
@@ -62,7 +62,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
- serviceAccountToken: undefined,
+ serviceAccountToken: 'token',
+ authProvider: 'serviceAccount',
},
new Set(['pods', 'services']),
);
@@ -124,7 +125,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
- serviceAccountToken: undefined,
+ serviceAccountToken: 'token',
+ authProvider: 'serviceAccount',
},
new Set(['pods', 'services']),
);
@@ -172,7 +174,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
- serviceAccountToken: undefined,
+ serviceAccountToken: 'token',
+ authProvider: 'serviceAccount',
},
new Set(['foo']),
),
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
index 56da3235cc..4218c64e80 100644
--- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
@@ -74,6 +74,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
Promise.resolve([
{
name: 'test-cluster',
+ authProvider: 'serviceAccount',
},
]),
);
@@ -89,6 +90,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
getClusterByServiceId,
},
getVoidLogger(),
+ {},
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
@@ -142,9 +144,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
Promise.resolve([
{
name: 'test-cluster',
+ authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
+ authProvider: 'google',
},
]),
);
@@ -160,6 +164,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
getClusterByServiceId,
},
getVoidLogger(),
+ {
+ auth: {
+ google: 'google_token_123',
+ },
+ },
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
index 1690b8be40..f0b21cbc0c 100644
--- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
@@ -16,17 +16,22 @@
import { Logger } from 'winston';
import {
+ AuthRequestBody,
+ ClusterDetails,
KubernetesClusterLocator,
KubernetesFetcher,
KubernetesObjectTypes,
ObjectsByServiceIdResponse,
-} from '..';
+} from '../types/types';
+import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
+import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
export type GetKubernetesObjectsByServiceIdHandler = (
serviceId: string,
fetcher: KubernetesFetcher,
clusterLocator: KubernetesClusterLocator,
logger: Logger,
+ requestBody: AuthRequestBody,
objectsToFetch?: Set,
) => Promise;
@@ -46,18 +51,35 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic
fetcher,
clusterLocator,
logger,
+ requestBody,
objectsToFetch = DEFAULT_OBJECTS,
) => {
- const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId);
+ const clusterDetails: ClusterDetails[] = await clusterLocator.getClusterByServiceId(
+ serviceId,
+ );
+
+ // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
+ const promises: Promise[] = clusterDetails.map(cd => {
+ const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
+ cd.authProvider,
+ );
+ return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
+ cd,
+ requestBody,
+ );
+ });
+ const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all(
+ promises,
+ );
logger.info(
- `serviceId=${serviceId} clusterDetails=[${clusterDetails
+ `serviceId=${serviceId} clusterDetails=[${clusterDetailsDecoratedForAuth
.map(c => c.name)
.join(', ')}]`,
);
return Promise.all(
- clusterDetails.map(cd => {
+ clusterDetailsDecoratedForAuth.map(cd => {
return fetcher
.fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
.then(result => {
diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts
index 7731fe9abb..4e9a206eb4 100644
--- a/plugins/kubernetes-backend/src/service/router.test.ts
+++ b/plugins/kubernetes-backend/src/service/router.test.ts
@@ -54,8 +54,8 @@ describe('router', () => {
jest.resetAllMocks();
});
- describe('GET /services/:serviceId', () => {
- it('happy path: lists kubernetes objects', async () => {
+ describe('post /services/:serviceId', () => {
+ it('happy path: lists kubernetes objects without auth in request body', async () => {
const result = {
clusterOne: {
pods: [
@@ -69,7 +69,34 @@ describe('router', () => {
} as any;
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
- const response = await request(app).get('/services/test-service');
+ const response = await request(app).post('/services/test-service');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(result);
+ });
+
+ it('happy path: lists kubernetes objects with auth in request body', async () => {
+ const result = {
+ clusterOne: {
+ pods: [
+ {
+ metadata: {
+ name: 'pod1',
+ },
+ },
+ ],
+ },
+ } as any;
+ handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
+
+ const response = await request(app)
+ .post('/services/test-service')
+ .send({
+ auth: {
+ google: 'google_token_123',
+ },
+ })
+ .set('Content-Type', 'application/json');
expect(response.status).toEqual(200);
expect(response.body).toEqual(result);
@@ -78,7 +105,7 @@ describe('router', () => {
it('internal error: lists kubernetes objects', async () => {
handleGetByServiceId.mockRejectedValue(Error('some internal error'));
- const response = await request(app).get('/services/test-service');
+ const response = await request(app).post('/services/test-service');
expect(response.status).toEqual(500);
expect(response.body).toEqual({ error: 'some internal error' });
diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts
index bdb14bb74d..9e9b4ab2f5 100644
--- a/plugins/kubernetes-backend/src/service/router.ts
+++ b/plugins/kubernetes-backend/src/service/router.ts
@@ -26,7 +26,11 @@ import {
GetKubernetesObjectsByServiceIdHandler,
handleGetKubernetesObjectsByServiceId,
} from './getKubernetesObjectsByServiceIdHandler';
-import { KubernetesClusterLocator, KubernetesFetcher } from '..';
+import {
+ AuthRequestBody,
+ KubernetesClusterLocator,
+ KubernetesFetcher,
+} from '../types/types';
export interface RouterOptions {
logger: Logger;
@@ -62,15 +66,16 @@ export const makeRouter = (
router.use(express.json());
// TODO error handling
- router.get('/services/:serviceId', async (req, res) => {
+ router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
-
+ const requestBody: AuthRequestBody = req.body;
try {
const response = await handleGetByServiceId(
serviceId,
fetcher,
clusterLocator,
logger,
+ requestBody,
);
res.send(response);
} catch (e) {
diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts
index 0cb42eec1a..eb2817f268 100644
--- a/plugins/kubernetes-backend/src/types/types.ts
+++ b/plugins/kubernetes-backend/src/types/types.ts
@@ -27,8 +27,14 @@ import {
export interface ClusterDetails {
name: string;
url: string;
- // TODO this will eventually be configured by the auth translation work
- serviceAccountToken: string | undefined;
+ authProvider: string;
+ serviceAccountToken?: string | undefined;
+}
+
+export interface AuthRequestBody {
+ auth?: {
+ google?: string;
+ };
}
export interface ClusterObjects {
diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md
index 678c9a96a6..1ad478bd89 100644
--- a/plugins/kubernetes/README.md
+++ b/plugins/kubernetes/README.md
@@ -11,3 +11,24 @@ Your plugin has been added to the example app in this repository, meaning you'll
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+
+## Surfacing your Kubernetes components as part of an entity
+
+### Adding the entity annotation
+
+In order for Backstage to detect that an entity has Kubernetes components,
+the following annotation should be added to the entity.
+
+```yaml
+annotations:
+ 'backstage.io/kubernetes-id': dice-roller
+```
+
+### Labeling Kubernetes components
+
+In order for Kubernetes components to show up in the service catalog
+as a part of an entity, Kubernetes components must be labeled with the following label:
+
+```yaml
+'backstage.io/kubernetes-id':
+```
diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json
index 6d177c8326..2adefebbd6 100644
--- a/plugins/kubernetes/package.json
+++ b/plugins/kubernetes/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-kubernetes",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,11 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@kubernetes/client-node": "^0.12.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -34,9 +35,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
index cbb3b03a38..86bb830046 100644
--- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts
+++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
@@ -16,7 +16,10 @@
import { DiscoveryApi } from '@backstage/core';
import { KubernetesApi } from './types';
-import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
+import {
+ AuthRequestBody,
+ ObjectsByServiceIdResponse,
+} from '@backstage/plugin-kubernetes-backend';
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
@@ -25,9 +28,18 @@ export class KubernetesBackendClient implements KubernetesApi {
this.discoveryApi = options.discoveryApi;
}
- private async getRequired(path: string): Promise {
+ private async getRequired(
+ path: string,
+ requestBody: AuthRequestBody,
+ ): Promise {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
- const response = await fetch(url);
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestBody),
+ });
if (!response.ok) {
const payload = await response.text();
@@ -40,7 +52,8 @@ export class KubernetesBackendClient implements KubernetesApi {
async getObjectsByServiceId(
serviceId: String,
+ requestBody: AuthRequestBody,
): Promise {
- return await this.getRequired(`/services/${serviceId}`);
+ return await this.getRequired(`/services/${serviceId}`, requestBody);
}
}
diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts
index 5ec3cda6f8..8e44d58e6a 100644
--- a/plugins/kubernetes/src/api/types.ts
+++ b/plugins/kubernetes/src/api/types.ts
@@ -15,7 +15,10 @@
*/
import { createApiRef } from '@backstage/core';
-import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
+import {
+ AuthRequestBody,
+ ObjectsByServiceIdResponse,
+} from '@backstage/plugin-kubernetes-backend';
export const kubernetesApiRef = createApiRef({
id: 'plugin.kubernetes.service',
@@ -24,5 +27,8 @@ export const kubernetesApiRef = createApiRef({
});
export interface KubernetesApi {
- getObjectsByServiceId(serviceId: String): Promise;
+ getObjectsByServiceId(
+ serviceId: String,
+ requestBody: AuthRequestBody,
+ ): Promise;
}
diff --git a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx
index 6985423a10..6fa6b3114c 100644
--- a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx
+++ b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx
@@ -76,7 +76,7 @@ describe('ErrorPanel', () => {
expect(getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
expect(
getByText(
- "Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR",
+ "Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR, status code: 500",
),
).toBeInTheDocument();
});
diff --git a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx
index 1fe5714b09..1b8336dd72 100644
--- a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx
+++ b/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx
@@ -29,7 +29,7 @@ const clustersWithErrorsToErrorMessage = (
{c.errors.map((e, j) => {
return (
- {`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}`}
+ {`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}, status code: ${e.statusCode}`}
);
})}
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
index 6119884c43..013c7ad002 100644
--- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
+++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
@@ -16,8 +16,10 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { Grid, TabProps } from '@material-ui/core';
+import { Config } from '@backstage/config';
import {
CardTab,
+ configApiRef,
Content,
Page,
pageTheme,
@@ -28,10 +30,12 @@ import {
import { Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../../api/types';
import {
+ AuthRequestBody,
ClusterObjects,
FetchResponse,
ObjectsByServiceIdResponse,
} from '@backstage/plugin-kubernetes-backend';
+import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
import { DeploymentTables } from '../DeploymentTables';
import { DeploymentTriple } from '../../types/types';
import {
@@ -105,16 +109,40 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
>(undefined);
const [error, setError] = useState(undefined);
+ const configApi = useApi(configApiRef);
+ const clusters: Config[] = configApi.getConfigArray('kubernetes.clusters');
+ const allAuthProviders: string[] = clusters.map(c =>
+ c.getString('authProvider'),
+ );
+ const authProviders: string[] = [...new Set(allAuthProviders)];
+
+ const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
+
useEffect(() => {
- kubernetesApi
- .getObjectsByServiceId(entity.metadata.name)
- .then(result => {
- setKubernetesObjects(result);
- })
- .catch(e => {
- setError(e.message);
- });
- }, [entity.metadata.name, kubernetesApi]);
+ (async () => {
+ // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
+ let requestBody: AuthRequestBody = {};
+ for (const authProviderStr of authProviders) {
+ // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
+ requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
+ authProviderStr,
+ requestBody,
+ );
+ }
+
+ // TODO: Add validation on contents/format of requestBody
+ kubernetesApi
+ .getObjectsByServiceId(entity.metadata.name, requestBody)
+ .then(result => {
+ setKubernetesObjects(result);
+ })
+ .catch(e => {
+ setError(e.message);
+ });
+ })();
+ /* eslint-disable react-hooks/exhaustive-deps */
+ }, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
+ /* eslint-enable react-hooks/exhaustive-deps */
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts
new file mode 100644
index 0000000000..7de5c7c0a9
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { OAuthApi } from '@backstage/core';
+import { KubernetesAuthProvider } from './types';
+import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
+
+export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
+ authProvider: OAuthApi;
+
+ constructor(authProvider: OAuthApi) {
+ this.authProvider = authProvider;
+ }
+
+ async decorateRequestBodyForAuth(
+ requestBody: AuthRequestBody,
+ ): Promise {
+ const googleAuthToken: string = await this.authProvider.getAccessToken(
+ 'https://www.googleapis.com/auth/cloud-platform',
+ );
+ if ('auth' in requestBody) {
+ requestBody.auth!.google = googleAuthToken;
+ } else {
+ requestBody.auth = { google: googleAuthToken };
+ }
+ return requestBody;
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts
new file mode 100644
index 0000000000..ac909d6695
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { OAuthApi } from '@backstage/core';
+import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
+import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
+import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
+import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
+
+export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
+ private readonly kubernetesAuthProviderMap: Map<
+ string,
+ KubernetesAuthProvider
+ >;
+
+ constructor(options: { googleAuthApi: OAuthApi }) {
+ this.kubernetesAuthProviderMap = new Map();
+ this.kubernetesAuthProviderMap.set(
+ 'google',
+ new GoogleKubernetesAuthProvider(options.googleAuthApi),
+ );
+ this.kubernetesAuthProviderMap.set(
+ 'serviceAccount',
+ new ServiceAccountKubernetesAuthProvider(),
+ );
+ }
+
+ async decorateRequestBodyForAuth(
+ authProvider: string,
+ requestBody: AuthRequestBody,
+ ): Promise {
+ const kubernetesAuthProvider:
+ | KubernetesAuthProvider
+ | undefined = this.kubernetesAuthProviderMap.get(authProvider);
+ if (kubernetesAuthProvider) {
+ return await kubernetesAuthProvider.decorateRequestBodyForAuth(
+ requestBody,
+ );
+ }
+ throw new Error(
+ `authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`,
+ );
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts
new file mode 100644
index 0000000000..3ac5a9494b
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { KubernetesAuthProvider } from './types';
+import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
+
+export class ServiceAccountKubernetesAuthProvider
+ implements KubernetesAuthProvider {
+ async decorateRequestBodyForAuth(
+ requestBody: AuthRequestBody,
+ ): Promise {
+ // No-op, with service account for auth, cluster config/details should already have serviceAccountToken
+ return requestBody;
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts
new file mode 100644
index 0000000000..24fee0f94c
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { createApiRef } from '@backstage/core';
+import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
+
+export interface KubernetesAuthProvider {
+ decorateRequestBodyForAuth(
+ requestBody: AuthRequestBody,
+ ): Promise;
+}
+
+export const kubernetesAuthProvidersApiRef = createApiRef<
+ KubernetesAuthProvidersApi
+>({
+ id: 'plugin.kubernetes-auth-providers.service',
+ description: 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders',
+});
+
+export interface KubernetesAuthProvidersApi {
+ decorateRequestBodyForAuth(
+ authProvider: string,
+ requestBody: AuthRequestBody,
+ ): Promise;
+}
diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts
index b7ad9615f5..cff9e1468b 100644
--- a/plugins/kubernetes/src/plugin.ts
+++ b/plugins/kubernetes/src/plugin.ts
@@ -18,9 +18,12 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
+ googleAuthApiRef,
} from '@backstage/core';
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
import { kubernetesApiRef } from './api/types';
+import { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types';
+import { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders';
export const rootCatalogKubernetesRouteRef = createRouteRef({
path: '*',
@@ -36,5 +39,12 @@ export const plugin = createPlugin({
factory: ({ discoveryApi }) =>
new KubernetesBackendClient({ discoveryApi }),
}),
+ createApiFactory({
+ api: kubernetesAuthProvidersApiRef,
+ deps: { googleAuthApi: googleAuthApiRef },
+ factory: ({ googleAuthApi }) => {
+ return new KubernetesAuthProviders({ googleAuthApi });
+ },
+ }),
],
});
diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md
index b5fecb85f8..19244f3cf0 100644
--- a/plugins/lighthouse/README.md
+++ b/plugins/lighthouse/README.md
@@ -57,3 +57,66 @@ Then configure the lighthouse service url in your [`app-config.yaml`](https://gi
lighthouse:
baseUrl: http://your-service-url
```
+
+### Integration with the Catalog
+
+The lighthouse plugin can be integrated into the catalog so that lighthouse audit information relating to a component
+can be displayed within that component's entity page. In order to link an Entity to its lighthouse audits the entity
+must be annotated as follows:
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ # ...
+ annotations:
+ # ...
+ lighthouse.com/website-url: # A single website url e.g. https://backstage.io/
+```
+
+> NOTE: The lighthouse plugin only supports one website url per component at this time.
+
+Add a lighthouse tab to the EntityPage:
+
+```tsx
+// packages/app/src/components/catalog/EntityPage.tsx
+import { EmbeddedRouter as LighthouseRouter } from '@backstage/plugin-lighthouse';
+
+// ...
+const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
+
+ // ...
+ }
+ />
+
+);
+```
+
+> NOTE: The embedded router renders page content without a header section allowing it to be rendered within a
+> catalog plugin page.
+
+Add a Lighthouse card to the overview tab on the EntityPage:
+
+```tsx
+// packages/app/src/components/catalog/EntityPage.tsx
+import {
+ LastLighthouseAuditCard,
+ isPluginApplicableToEntity as isLighthouseAvailable,
+} from '@backstage/plugin-lighthouse';
+
+// ...
+
+const OverviewContent = ({ entity }: { entity: Entity }) => (
+
+ // ...
+ {isLighthouseAvailable(entity) && (
+
+
+
+ )}
+
+);
+```
diff --git a/plugins/lighthouse/constants.ts b/plugins/lighthouse/constants.ts
new file mode 100644
index 0000000000..7a60e2be67
--- /dev/null
+++ b/plugins/lighthouse/constants.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const LIGHTHOUSE_WEBSITE_URL_ANNOTATION = 'lighthouse.com/website-url';
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json
index 6ca1495a05..7446f09ed4 100644
--- a/plugins/lighthouse/package.json
+++ b/plugins/lighthouse/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-lighthouse",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,22 +21,27 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/core-api": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
+ "@testing-library/react-hooks": "^3.4.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-markdown": "^4.3.1",
"react-router-dom": "6.0.0-beta.0",
- "react-use": "^15.3.3"
+ "react-use": "^15.3.3",
+ "@types/react": "^16.9"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx
index 46843504fd..df481483c3 100644
--- a/plugins/lighthouse/src/Router.tsx
+++ b/plugins/lighthouse/src/Router.tsx
@@ -15,11 +15,18 @@
*/
import React from 'react';
-import { Routes, Route } from 'react-router-dom';
-import { rootRouteRef, viewAuditRouteRef, createAuditRouteRef } from './plugin';
+import { Route, Routes } from 'react-router-dom';
+import { createAuditRouteRef, rootRouteRef, viewAuditRouteRef } from './plugin';
import AuditList from './components/AuditList';
-import AuditView from './components/AuditView';
-import CreateAudit from './components/CreateAudit';
+import AuditView, { AuditViewContent } from './components/AuditView';
+import CreateAudit, { CreateAuditContent } from './components/CreateAudit';
+import { Entity } from '@backstage/catalog-model';
+import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../constants';
+import { AuditListForEntity } from './components/AuditList/AuditListForEntity';
+import { EmptyState } from '@backstage/core';
+
+export const isPluginApplicableToEntity = (entity: Entity) =>
+ Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]);
export const Router = () => (
@@ -28,3 +35,24 @@ export const Router = () => (
} />
);
+
+export const EmbeddedRouter = ({ entity }: { entity: Entity }) =>
+ !isPluginApplicableToEntity(entity) ? (
+
+ ) : (
+
+ } />
+ }
+ />
+ }
+ />
+
+ );
diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts
index 5b6cd9aae7..03b42a38b9 100644
--- a/plugins/lighthouse/src/api.ts
+++ b/plugins/lighthouse/src/api.ts
@@ -104,6 +104,7 @@ export type LighthouseApi = {
getWebsiteList: (listOptions: LASListRequest) => Promise;
getWebsiteForAuditId: (auditId: string) => Promise;
triggerAudit: (payload: TriggerAuditPayload) => Promise;
+ getWebsiteByUrl: (websiteUrl: string) => Promise;
};
export const lighthouseApiRef = createApiRef({
@@ -150,4 +151,10 @@ export class LighthouseRestApi implements LighthouseApi {
},
});
}
+
+ async getWebsiteByUrl(websiteUrl: string): Promise {
+ return this.fetch(
+ `/v1/websites/${encodeURIComponent(websiteUrl)}`,
+ );
+ }
}
diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx
new file mode 100644
index 0000000000..7f10914d9c
--- /dev/null
+++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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 { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
+import {
+ lighthouseApiRef,
+ LighthouseRestApi,
+ WebsiteListResponse,
+} from '../../api';
+import mockFetch from 'jest-fetch-mock';
+
+import * as data from '../../__fixtures__/website-list-response.json';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import { AuditListForEntity } from './AuditListForEntity';
+import { lightTheme } from '@backstage/theme';
+import { ThemeProvider } from '@material-ui/core';
+import { MemoryRouter } from 'react-router-dom';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+
+jest.mock('../../hooks/useWebsiteForEntity', () => ({
+ useWebsiteForEntity: jest.fn(),
+}));
+
+const websiteListResponse = data as WebsiteListResponse;
+const entityWebsite = websiteListResponse.items[0];
+
+describe(' ', () => {
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ apis = ApiRegistry.from([
+ [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')],
+ [errorApiRef, mockErrorApi],
+ ]);
+ mockFetch.mockResponse(JSON.stringify(entityWebsite));
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: entityWebsite,
+ loading: false,
+ error: null,
+ });
+ });
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': entityWebsite.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const subject = (value = {}) =>
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ it('renders the audit list for the entity', async () => {
+ const { findByText } = subject();
+ expect(await findByText(entityWebsite.url)).toBeInTheDocument();
+ });
+
+ describe('where the data is loading', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: true,
+ error: null,
+ });
+ });
+
+ it('renders a Progress element', async () => {
+ const { findByTestId } = subject();
+ expect(await findByTestId('progress')).toBeInTheDocument();
+ });
+ });
+
+ describe('where there is an error loading data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: 'error',
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+
+ describe('where there is not data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx
new file mode 100644
index 0000000000..616fd0fa8a
--- /dev/null
+++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { AuditListTable } from './AuditListTable';
+import { Progress } from '@backstage/core';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+
+export const AuditListForEntity = () => {
+ const { value, loading, error } = useWebsiteForEntity();
+ if (loading) {
+ return ;
+ }
+ if (error || !value) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx
index d0e743a77f..54bd179418 100644
--- a/plugins/lighthouse/src/components/AuditView/index.tsx
+++ b/plugins/lighthouse/src/components/AuditView/index.tsx
@@ -124,7 +124,7 @@ const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => {
);
};
-const ConnectedAuditView: FC<{}> = () => {
+export const AuditViewContent: FC<{}> = () => {
const lighthouseApi = useApi(lighthouseApiRef);
const params = useParams() as { id: string };
const classes = useStyles();
@@ -173,32 +173,35 @@ const ConnectedAuditView: FC<{}> = () => {
}
return (
-
-
-
- navigate(`../../${createAuditButtonUrl}`)}
>
- navigate(`../../${createAuditButtonUrl}`)}
- >
- Create New Audit
-
-
-
- {content}
-
-
+ Create New Audit
+
+
+
+ {content}
+ >
);
};
+const ConnectedAuditView = () => (
+
+
+
+
+
+
+);
+
export default ConnectedAuditView;
diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx
new file mode 100644
index 0000000000..20b3b9c072
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import {
+ AuditCompleted,
+ LighthouseCategoryId,
+ WebsiteListResponse,
+} from '../../api';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
+import { lightTheme } from '@backstage/theme';
+import { ThemeProvider } from '@material-ui/core';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+import { MemoryRouter } from 'react-router-dom';
+import * as data from '../../__fixtures__/website-list-response.json';
+
+jest.mock('../../hooks/useWebsiteForEntity', () => ({
+ useWebsiteForEntity: jest.fn(),
+}));
+
+const websiteListResponse = data as WebsiteListResponse;
+let entityWebsite = websiteListResponse.items[2];
+
+describe(' ', () => {
+ const asPercentage = (fraction: number) => `${fraction * 100}%`;
+
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: entityWebsite,
+ loading: false,
+ error: null,
+ });
+ });
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': entityWebsite.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const subject = (value = {}) =>
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ describe('where the last audit completed successfully', () => {
+ const audit = entityWebsite.lastAudit as AuditCompleted;
+
+ it('renders the performance data for the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText(audit.url)).toBeInTheDocument();
+ expect(await findByText(audit.status)).toBeInTheDocument();
+ for (const category of Object.keys(audit.categories)) {
+ const { score } = audit.categories[category as LighthouseCategoryId];
+ expect(await findByText(asPercentage(score))).toBeInTheDocument();
+ }
+ });
+
+ describe('where a category score is not a number', () => {
+ beforeEach(() => {
+ entityWebsite = { ...entityWebsite };
+ (entityWebsite.lastAudit as AuditCompleted).categories.accessibility.score = NaN;
+ });
+
+ afterEach(() => {
+ entityWebsite = websiteListResponse.items[2];
+ });
+
+ it('renders the performance data for the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText('N/A')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('where the last audit is in running', () => {
+ const audit = websiteListResponse.items[0].lastAudit as AuditCompleted;
+
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: websiteListResponse.items[0],
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders the url and status of the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText(audit.url)).toBeInTheDocument();
+ expect(await findByText(audit.status)).toBeInTheDocument();
+ });
+ });
+
+ describe('where the data is loading', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: true,
+ error: null,
+ });
+ });
+
+ it('renders a Progress element', async () => {
+ const { findByTestId } = subject();
+ expect(await findByTestId('progress')).toBeInTheDocument();
+ });
+ });
+
+ describe('where there is an error loading data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: 'error',
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+ //
+ describe('where there is no data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx
new file mode 100644
index 0000000000..d3bc362877
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx
@@ -0,0 +1,109 @@
+/*
+ * 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 { Audit, AuditCompleted, LighthouseCategoryId } from '../../api';
+import {
+ InfoCard,
+ Progress,
+ StatusError,
+ StatusOK,
+ StatusWarning,
+ StructuredMetadataTable,
+} from '@backstage/core';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+import AuditStatusIcon from '../AuditStatusIcon';
+
+const LighthouseCategoryScoreStatus: FC<{ score: number }> = ({ score }) => {
+ const scoreAsPercentage = score * 100;
+ switch (true) {
+ case scoreAsPercentage >= 90:
+ return (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ case scoreAsPercentage >= 50 && scoreAsPercentage < 90:
+ return (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ case scoreAsPercentage < 50:
+ return (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ default:
+ return N/A ;
+ }
+};
+
+const LighthouseAuditStatus: FC<{ audit: Audit }> = ({ audit }) => (
+ <>
+
+ {audit.status.toUpperCase()}
+ >
+);
+
+const LighthouseAuditSummary: FC<{ audit: Audit; dense?: boolean }> = ({
+ audit,
+ dense = false,
+}) => {
+ const { url } = audit;
+ const flattenedCategoryData: Record = {};
+ if (audit.status === 'COMPLETED') {
+ const categories = (audit as AuditCompleted).categories;
+ const categoryIds = Object.keys(categories) as LighthouseCategoryId[];
+ categoryIds.forEach((id: LighthouseCategoryId) => {
+ const { title, score } = categories[id];
+
+ flattenedCategoryData[title] = (
+
+ );
+ });
+ }
+ const tableData = {
+ url,
+ status: ,
+ ...flattenedCategoryData,
+ };
+
+ return ;
+};
+
+export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({
+ dense = false,
+}) => {
+ const { value: website, loading, error } = useWebsiteForEntity();
+
+ let content;
+ if (loading) {
+ content = ;
+ }
+ if (error) {
+ content = null;
+ }
+ if (website) {
+ content = (
+
+ );
+ }
+ return {content} ;
+};
diff --git a/plugins/lighthouse/src/components/Cards/index.ts b/plugins/lighthouse/src/components/Cards/index.ts
new file mode 100644
index 0000000000..7595619808
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx
index 36bd97485a..ad46b06020 100644
--- a/plugins/lighthouse/src/components/CreateAudit/index.tsx
+++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx
@@ -53,7 +53,7 @@ const useStyles = makeStyles(theme => ({
},
}));
-const CreateAudit: FC<{}> = () => {
+export const CreateAuditContent: FC<{}> = () => {
const errorApi = useApi(errorApiRef);
const lighthouseApi = useApi(lighthouseApiRef);
const classes = useStyles();
@@ -94,88 +94,91 @@ const CreateAudit: FC<{}> = () => {
]);
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+ >
);
};
+const CreateAudit = () => (
+
+
+
+
+
+
+);
+
export default CreateAudit;
diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx
new file mode 100644
index 0000000000..868e26f04c
--- /dev/null
+++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core-api';
+import { lighthouseApiRef, WebsiteListResponse } from '../api';
+import { useWebsiteForEntity } from './useWebsiteForEntity';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import * as data from '../__fixtures__/website-list-response.json';
+
+const websiteListResponse = data as WebsiteListResponse;
+const website = websiteListResponse.items[0];
+
+const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+};
+
+const mockLighthouseApi: jest.Mocked> = {
+ getWebsiteByUrl: jest.fn(),
+};
+
+describe('useWebsiteForEntity', () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': website.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const wrapper: React.FC<{}> = ({ children }) => {
+ return (
+
+
+ {children}
+
+
+ );
+ };
+
+ const subject = () =>
+ renderHook(useWebsiteForEntity, {
+ wrapper,
+ });
+
+ beforeEach(() => {
+ (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockResolvedValue(website);
+ });
+
+ it('returns the lighthouse information for the website url in annotations ', async () => {
+ const { result, waitForNextUpdate } = subject();
+ await waitForNextUpdate();
+ expect(result.current?.value).toBe(website);
+ });
+
+ describe('where there is an error', () => {
+ const error = new Error('useWebsiteForEntity unit test');
+
+ beforeEach(() => {
+ (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockRejectedValueOnce(
+ error,
+ );
+ });
+
+ it('posts the error to the error api and returns the error to the caller', async () => {
+ const { result, waitForNextUpdate } = subject();
+ await waitForNextUpdate();
+ expect(result.current?.error).toBe(error);
+ expect(mockErrorApi.post).toHaveBeenCalledWith(error);
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts
new file mode 100644
index 0000000000..c52e38f473
--- /dev/null
+++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { useEntity } from '@backstage/plugin-catalog';
+import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants';
+import { errorApiRef, useApi } from '@backstage/core-api';
+import { lighthouseApiRef } from '../api';
+import { useAsync } from 'react-use';
+
+// For the sake of simplicity we assume that an entity has only one website url. This is to avoid encoding a list
+// type in an annotation which is a plain string.
+export const useWebsiteForEntity = () => {
+ const { entity } = useEntity();
+ const websiteUrl =
+ entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION] ?? '';
+ const lighthouseApi = useApi(lighthouseApiRef);
+ const errorApi = useApi(errorApiRef);
+ const response = useAsync(() => lighthouseApi.getWebsiteByUrl(websiteUrl), [
+ websiteUrl,
+ ]);
+ if (response.error) {
+ errorApi.post(response.error);
+ }
+ return response;
+};
diff --git a/plugins/lighthouse/src/index.ts b/plugins/lighthouse/src/index.ts
index bebdaaf713..64fe2f8cc0 100644
--- a/plugins/lighthouse/src/index.ts
+++ b/plugins/lighthouse/src/index.ts
@@ -15,5 +15,6 @@
*/
export { plugin } from './plugin';
-export { Router } from './Router';
+export { Router, isPluginApplicableToEntity, EmbeddedRouter } from './Router';
export * from './api';
+export * from './components/Cards';
diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json
index 38a5c437a4..603d01d62b 100644
--- a/plugins/newrelic/package.json
+++ b/plugins/newrelic/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-newrelic",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -31,8 +31,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png b/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png
index cc63306bba..88661830d0 100644
Binary files a/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png and b/plugins/newrelic/src/assets/img/newrelic-plugin-apm.png differ
diff --git a/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png b/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png
index e42013f14c..dc35d05dca 100644
Binary files a/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png and b/plugins/newrelic/src/assets/img/newrelic-plugin-tools.png differ
diff --git a/plugins/newrelic/src/assets/img/newrelic.jpg b/plugins/newrelic/src/assets/img/newrelic.jpg
index d4b3a0e196..914ad02280 100644
Binary files a/plugins/newrelic/src/assets/img/newrelic.jpg and b/plugins/newrelic/src/assets/img/newrelic.jpg differ
diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json
index 10024fcd42..11f18287f0 100644
--- a/plugins/proxy-backend/package.json
+++ b/plugins/proxy-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-proxy-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -19,10 +19,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
- "@types/http-proxy-middleware": "^0.19.3",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"http-proxy-middleware": "^0.19.1",
@@ -35,7 +34,8 @@
"yup": "^0.29.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@types/http-proxy-middleware": "^0.19.3",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
"@types/uuid": "^8.0.0",
diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts
index 7612c392a2..de2be19b6d 100644
--- a/plugins/proxy-backend/src/index.ts
+++ b/plugins/proxy-backend/src/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export * from './service/router';
+export * from './service';
diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/plugins/proxy-backend/src/service/index.ts
similarity index 83%
rename from packages/core/src/layout/Sidebar/Settings/index.ts
rename to plugins/proxy-backend/src/service/index.ts
index 15042dc85d..38fbb697c4 100644
--- a/packages/core/src/layout/Sidebar/Settings/index.ts
+++ b/plugins/proxy-backend/src/service/index.ts
@@ -14,5 +14,4 @@
* limitations under the License.
*/
-export { ProviderSettingsItem } from './ProviderSettingsItem';
-export { SidebarUserSettings } from './UserSettings';
+export { createRouter } from './router';
diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts
index 07737e9ea8..012adaed93 100644
--- a/plugins/proxy-backend/src/service/router.test.ts
+++ b/plugins/proxy-backend/src/service/router.test.ts
@@ -14,13 +14,30 @@
* limitations under the License.
*/
-import { createRouter } from './router';
+import { buildMiddleware, createRouter } from './router';
import * as winston from 'winston';
import { ConfigReader } from '@backstage/config';
import {
loadBackendConfig,
SingleHostDiscovery,
} from '@backstage/backend-common';
+import createProxyMiddleware, {
+ Config as ProxyMiddlewareConfig,
+ Proxy,
+} from 'http-proxy-middleware';
+import * as http from 'http';
+
+jest.mock('http-proxy-middleware', () => {
+ return jest.fn().mockImplementation(
+ (): Proxy => {
+ return () => undefined;
+ },
+ );
+});
+
+const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction<
+ typeof createProxyMiddleware
+>;
describe('createRouter', () => {
it('works', async () => {
@@ -35,3 +52,151 @@ describe('createRouter', () => {
expect(router).toBeDefined();
});
});
+
+describe('buildMiddleware', () => {
+ const logger = winston.createLogger();
+
+ beforeEach(() => {
+ mockCreateProxyMiddleware.mockClear();
+ });
+
+ it('accepts strings', async () => {
+ buildMiddleware('/api/', logger, 'test', 'http://mocked');
+
+ expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
+
+ const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
+ (pathname: string, req: Partial) => boolean,
+ ProxyMiddlewareConfig,
+ ];
+ expect(filter('', { method: 'GET' })).toBe(true);
+ expect(filter('', { method: 'POST' })).toBe(true);
+ expect(filter('', { method: 'PUT' })).toBe(true);
+ expect(filter('', { method: 'PATCH' })).toBe(true);
+ expect(filter('', { method: 'DELETE' })).toBe(true);
+
+ expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' });
+ expect(fullConfig.changeOrigin).toBe(true);
+ expect(fullConfig.logProvider!(logger)).toBe(logger);
+ });
+
+ it('limits allowedMethods', async () => {
+ buildMiddleware('/api/', logger, 'test', {
+ target: 'http://mocked',
+ allowedMethods: ['GET', 'DELETE'],
+ });
+
+ expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
+
+ const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
+ (pathname: string, req: Partial) => boolean,
+ ProxyMiddlewareConfig,
+ ];
+ expect(filter('', { method: 'GET' })).toBe(true);
+ expect(filter('', { method: 'POST' })).toBe(false);
+ expect(filter('', { method: 'PUT' })).toBe(false);
+ expect(filter('', { method: 'PATCH' })).toBe(false);
+ expect(filter('', { method: 'DELETE' })).toBe(true);
+
+ expect(fullConfig.pathRewrite).toEqual({ '^/api/test/': '/' });
+ expect(fullConfig.changeOrigin).toBe(true);
+ expect(fullConfig.logProvider!(logger)).toBe(logger);
+ });
+
+ it('permits default headers', async () => {
+ buildMiddleware('/api/', logger, 'test', {
+ target: 'http://mocked',
+ });
+
+ expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
+
+ const config = mockCreateProxyMiddleware.mock
+ .calls[0][1] as ProxyMiddlewareConfig;
+
+ const testClientRequest = {
+ getHeaderNames: () => [
+ 'cache-control',
+ 'content-language',
+ 'content-length',
+ 'content-type',
+ 'expires',
+ 'last-modified',
+ 'pragma',
+ 'host',
+ 'accept',
+ 'accept-language',
+ 'user-agent',
+ 'cookie',
+ ],
+ removeHeader: jest.fn(),
+ } as Partial;
+
+ expect(config).toBeDefined();
+ expect(config.onProxyReq).toBeDefined();
+
+ config.onProxyReq!(
+ testClientRequest as http.ClientRequest,
+ {} as http.IncomingMessage,
+ {} as http.ServerResponse,
+ );
+
+ expect(testClientRequest.removeHeader).toHaveBeenCalledTimes(1);
+ expect(testClientRequest.removeHeader).toHaveBeenCalledWith('cookie');
+ });
+
+ it('permits default and configured headers', async () => {
+ buildMiddleware('/api/', logger, 'test', {
+ target: 'http://mocked',
+ headers: {
+ Authorization: 'my-token',
+ },
+ });
+
+ expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
+
+ const config = mockCreateProxyMiddleware.mock
+ .calls[0][1] as ProxyMiddlewareConfig;
+
+ const testClientRequest = {
+ getHeaderNames: () => ['authorization', 'Cookie'],
+ removeHeader: jest.fn(),
+ } as Partial;
+
+ config.onProxyReq!(
+ testClientRequest as http.ClientRequest,
+ {} as http.IncomingMessage,
+ {} as http.ServerResponse,
+ );
+
+ expect(testClientRequest.removeHeader).toHaveBeenCalledTimes(1);
+ expect(testClientRequest.removeHeader).toHaveBeenCalledWith('Cookie');
+ });
+
+ it('permits configured headers', async () => {
+ buildMiddleware('/api/', logger, 'test', {
+ target: 'http://mocked',
+ allowedHeaders: ['authorization', 'cookie'],
+ });
+
+ expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
+
+ const config = mockCreateProxyMiddleware.mock
+ .calls[0][1] as ProxyMiddlewareConfig;
+
+ const testClientRequest = {
+ getHeaderNames: () => ['authorization', 'Cookie', 'X-Auth-Request-User'],
+ removeHeader: jest.fn(),
+ } as Partial;
+
+ config.onProxyReq!(
+ testClientRequest as http.ClientRequest,
+ {} as http.IncomingMessage,
+ {} as http.ServerResponse,
+ );
+
+ expect(testClientRequest.removeHeader).toHaveBeenCalledTimes(1);
+ expect(testClientRequest.removeHeader).toHaveBeenCalledWith(
+ 'X-Auth-Request-User',
+ );
+ });
+});
diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts
index 8cec31719c..75eab029b5 100644
--- a/plugins/proxy-backend/src/service/router.ts
+++ b/plugins/proxy-backend/src/service/router.ts
@@ -25,6 +25,27 @@ import { Logger } from 'winston';
import http from 'http';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
+// A list of headers that are always forwarded to the proxy targets.
+const safeForwardHeaders = [
+ // https://fetch.spec.whatwg.org/#cors-safelisted-request-header
+ 'cache-control',
+ 'content-language',
+ 'content-length',
+ 'content-type',
+ 'expires',
+ 'last-modified',
+ 'pragma',
+
+ // host is overridden by default. if changeOrigin is configured to false,
+ // we assume this is a intentional and should also be forwarded.
+ 'host',
+
+ // other headers that we assume to be ok
+ 'accept',
+ 'accept-language',
+ 'user-agent',
+];
+
export interface RouterOptions {
logger: Logger;
config: Config;
@@ -33,11 +54,12 @@ export interface RouterOptions {
export interface ProxyConfig extends ProxyMiddlewareConfig {
allowedMethods?: string[];
+ allowedHeaders?: string[];
}
// Creates a proxy middleware, possibly with defaults added on top of the
// given config.
-function buildMiddleware(
+export function buildMiddleware(
pathPrefix: string,
logger: Logger,
route: string,
@@ -68,6 +90,30 @@ function buildMiddleware(
return fullConfig?.allowedMethods?.includes(req.method!) ?? true;
};
+ // Only forward the allowed HTTP headers to not forward unwanted secret headers
+ const headerAllowList = new Set(
+ [
+ // allow all safe headers
+ ...safeForwardHeaders,
+
+ // allow all headers that are set by the proxy
+ ...((fullConfig.headers && Object.keys(fullConfig.headers)) || []),
+
+ // allow all configured headers
+ ...(fullConfig.allowedHeaders || []),
+ ].map(h => h.toLocaleLowerCase()),
+ );
+
+ fullConfig.onProxyReq = (proxyReq: http.ClientRequest) => {
+ const headerNames = proxyReq.getHeaderNames();
+
+ headerNames.forEach(h => {
+ if (!headerAllowList.has(h.toLocaleLowerCase())) {
+ proxyReq.removeHeader(h);
+ }
+ });
+ };
+
return createProxyMiddleware(filter, fullConfig);
}
diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json
index 9e3ef7e307..9dbd598dee 100644
--- a/plugins/register-component/package.json
+++ b/plugins/register-component/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-register-component",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md
index c221dfd23b..f441efa416 100644
--- a/plugins/rollbar-backend/README.md
+++ b/plugins/rollbar-backend/README.md
@@ -10,8 +10,7 @@ The following values are read from the configuration file.
rollbar:
organization: spotify
accountToken:
- $secret:
- env: ROLLBAR_ACCOUNT_TOKEN
+ $env: ROLLBAR_ACCOUNT_TOKEN
```
_NOTE: The `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read
diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json
index 2ed15b7aff..cda105a73f 100644
--- a/plugins/rollbar-backend/package.json
+++ b/plugins/rollbar-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-rollbar-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
"axios": "^0.20.0",
"camelcase-keys": "^6.2.2",
@@ -37,7 +37,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/supertest": "^2.0.8",
"jest-fetch-mock": "^3.0.3",
"supertest": "^4.0.2"
diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md
index b2d2100dd9..98f2272508 100644
--- a/plugins/rollbar/README.md
+++ b/plugins/rollbar/README.md
@@ -55,8 +55,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
rollbar:
organization: spotify
accountToken:
- $secret:
- env: ROLLBAR_ACCOUNT_TOKEN
+ $env: ROLLBAR_ACCOUNT_TOKEN
```
7. Annotate entities with the rollbar project slug
diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json
index 18f13c3969..ba690fde64 100644
--- a/plugins/rollbar/package.json
+++ b/plugins/rollbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-rollbar",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -37,9 +37,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json
index 7a7938a483..ff584e7374 100644
--- a/plugins/scaffolder-backend/package.json
+++ b/plugins/scaffolder-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@gitbeaker/core": "^23.5.0",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
@@ -47,7 +47,7 @@
"yaml": "^1.10.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@octokit/types": "^5.4.1",
"@types/fs-extra": "^9.0.1",
"@types/git-url-parse": "^9.0.0",
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts
index 4409d815b5..babdd65c04 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts
@@ -90,7 +90,6 @@ describe('GitHubPreparer', () => {
{},
);
});
-
it('return the temp directory with the path to the folder if it is specified', async () => {
const preparer = new GithubPreparer();
mockEntity.spec.path = './template/test/1/2/3';
@@ -100,4 +99,20 @@ describe('GitHubPreparer', () => {
/\/template\/test\/1\/2\/3$/,
);
});
+ it('calls the clone command with the token when provided', async () => {
+ const preparer = new GithubPreparer({ token: 'abc' });
+ await preparer.prepare(mockEntity);
+ expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
+ 1,
+ 'https://github.com/benjdlambert/backstage-graphql-template',
+ expect.any(String),
+ {
+ fetchOpts: {
+ callbacks: {
+ credentials: expect.any(Function),
+ },
+ },
+ },
+ );
+ });
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
index ba4adcabd9..49703c5616 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts
@@ -21,11 +21,18 @@ import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { PreparerBase } from './types';
import GitUriParser from 'git-url-parse';
-import { Clone } from 'nodegit';
+import { Clone, Cred } from 'nodegit';
export class GithubPreparer implements PreparerBase {
+ token?: string;
+
+ constructor(params: { token?: string } = {}) {
+ this.token = params.token;
+ }
+
async prepare(template: TemplateEntityV1alpha1): Promise {
const { protocol, location } = parseLocationAnnotation(template);
+ const { token } = this;
if (protocol !== 'github') {
throw new InputError(
@@ -45,9 +52,19 @@ export class GithubPreparer implements PreparerBase {
template.spec.path ?? '.',
);
- await Clone.clone(repositoryCheckoutUrl, tempDir, {
- // TODO(blam): Maybe need some auth here?
- });
+ const cloneOptions = token
+ ? {
+ fetchOpts: {
+ callbacks: {
+ credentials() {
+ return Cred.userpassPlaintextNew(token, 'x-oauth-basic');
+ },
+ },
+ },
+ }
+ : {};
+
+ await Clone.clone(repositoryCheckoutUrl, tempDir, cloneOptions);
return path.resolve(tempDir, templateDirectory);
}
diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json
index 2abd3feb8f..1e59019ad7 100644
--- a/plugins/scaffolder/package.json
+++ b/plugins/scaffolder/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -41,9 +41,9 @@
"swr": "^0.3.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json
index 68b94ce7d8..c52f7176e3 100644
--- a/plugins/sentry-backend/package.json
+++ b/plugins/sentry-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-sentry-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
"@types/express": "^4.17.6",
"axios": "^0.20.0",
"compression": "^1.7.4",
@@ -34,7 +34,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"jest-fetch-mock": "^3.0.3"
},
"files": [
diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json
index 143a2dfc12..6972b87823 100644
--- a/plugins/sentry/package.json
+++ b/plugins/sentry/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-sentry",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"timeago.js": "^4.0.2"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/tech-radar/docs/screenshot.png b/plugins/tech-radar/docs/screenshot.png
index 657268fc09..8c1261e57a 100644
Binary files a/plugins/tech-radar/docs/screenshot.png and b/plugins/tech-radar/docs/screenshot.png differ
diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json
index c9950cdc95..d6067ba3e2 100644
--- a/plugins/tech-radar/package.json
+++ b/plugins/tech-radar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-tech-radar",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -35,8 +35,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json
index 0ff9722718..75f6071464 100644
--- a/plugins/techdocs-backend/package.json
+++ b/plugins/techdocs-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-techdocs-backend",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.23",
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/config": "^0.1.1-alpha.23",
+ "@backstage/backend-common": "^0.1.1-alpha.24",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/config": "^0.1.1-alpha.24",
"@types/dockerode": "^2.5.34",
"@types/express": "^4.17.6",
"command-exists-promise": "^2.0.2",
@@ -38,7 +38,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
"@types/node-fetch": "^2.5.7",
"supertest": "^4.0.2"
},
diff --git a/plugins/techdocs-backend/src/default-branch.ts b/plugins/techdocs-backend/src/default-branch.ts
index 05a8f666ce..a903b780d8 100644
--- a/plugins/techdocs-backend/src/default-branch.ts
+++ b/plugins/techdocs-backend/src/default-branch.ts
@@ -69,7 +69,7 @@ function getGithubRequestOptions(config: Config): RequestInit {
const token =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken') ??
- process.env.GITHUB_PRIVATE_TOKEN;
+ process.env.GITHUB_TOKEN;
if (token) {
headers.Authorization = `token ${token}`;
@@ -88,7 +88,7 @@ function getGitlabRequestOptions(config: Config): RequestInit {
const token =
config.getOptionalString('catalog.processors.gitlab.privateToken') ??
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
- process.env.GITLAB_ACCESS_TOKEN;
+ process.env.GITLAB_TOKEN;
if (token) {
headers['PRIVATE-TOKEN'] = token;
diff --git a/plugins/techdocs-backend/src/helpers.ts b/plugins/techdocs-backend/src/helpers.ts
index d6a275f183..e79df12365 100644
--- a/plugins/techdocs-backend/src/helpers.ts
+++ b/plugins/techdocs-backend/src/helpers.ts
@@ -126,9 +126,7 @@ export const checkoutGitRepository = async (
process.env.GITLAB_PRIVATE_TOKEN_USER ||
'';
const token =
- process.env.GITHUB_PRIVATE_TOKEN ||
- process.env.GITLAB_PRIVATE_TOKEN_USER ||
- '';
+ process.env.GITHUB_TOKEN || process.env.GITLAB_PRIVATE_TOKEN_USER || '';
if (fs.existsSync(repositoryTmpPath)) {
try {
diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json
index e376202a41..27b0b0b881 100644
--- a/plugins/techdocs/package.json
+++ b/plugins/techdocs/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-techdocs",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -22,12 +22,12 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/catalog-model": "^0.1.1-alpha.23",
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/core-api": "^0.1.1-alpha.23",
- "@backstage/plugin-catalog": "^0.1.1-alpha.23",
- "@backstage/test-utils": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/catalog-model": "^0.1.1-alpha.24",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/core-api": "^0.1.1-alpha.24",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -40,8 +40,8 @@
"sanitize-html": "^1.27.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx
index 550b8df923..01299b8fe6 100644
--- a/plugins/techdocs/src/reader/components/Reader.tsx
+++ b/plugins/techdocs/src/reader/components/Reader.tsx
@@ -116,6 +116,7 @@ export const Reader = ({ entityId, onReady }: Props) => {
return dom;
},
addLinkClickListener({
+ baseUrl: window.location.origin,
onClick: (_: MouseEvent, url: string) => {
const parsedUrl = new URL(url);
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx
index 70388e81c2..0d8fe9cabb 100644
--- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx
@@ -54,7 +54,7 @@ describe(' ', () => {
),
);
expect(rendered.container.innerHTML).toContain('header');
- expect(rendered.getByText('test-site-name')).toBeDefined();
+ expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
expect(rendered.getByText('test-site-desc')).toBeDefined();
});
});
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
index b01ace4994..c59cce2b11 100644
--- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
@@ -48,12 +48,14 @@ export const TechDocsPageHeader = ({
spec: { owner, lifecycle },
} = entityMetadataValues || { spec: {} };
+ const componentLink = `/catalog/${kind}/${name}`;
+
const labels = (
<>
+
{name}
}
@@ -66,7 +68,11 @@ export const TechDocsPageHeader = ({
+
}
@@ -81,6 +87,8 @@ export const TechDocsPageHeader = ({
subtitle={
siteDescription && siteDescription !== 'None' ? siteDescription : ''
}
+ type={name}
+ typeLink={componentLink}
>
{labels}
diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
index 8e942b8be1..775780fe4d 100644
--- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
+++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts
@@ -14,23 +14,61 @@
* limitations under the License.
*/
-import { createTestShadowDom, FIXTURES } from '../../test-utils';
+import { createTestShadowDom } from '../../test-utils';
import { addLinkClickListener } from '.';
describe('addLinkClickListener', () => {
it('calls onClick when a link has been clicked', () => {
const fn = jest.fn();
- const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
- preTransformers: [],
- postTransformers: [
- addLinkClickListener({
- onClick: fn,
- }),
- ],
- });
+ const shadowDom = createTestShadowDom(
+ `
+
+
+
+ Link
+
+
+ `,
+ {
+ preTransformers: [],
+ postTransformers: [
+ addLinkClickListener({
+ baseUrl: 'http://localhost:3000',
+ onClick: fn,
+ }),
+ ],
+ },
+ );
shadowDom.querySelector('a')?.click();
expect(fn).toHaveBeenCalledTimes(1);
});
+
+ it('does not call onClick when a link links to another baseUrl', () => {
+ const fn = jest.fn();
+ const shadowDom = createTestShadowDom(
+ `
+
+
+
+ Link
+
+
+ `,
+ {
+ preTransformers: [],
+ postTransformers: [
+ addLinkClickListener({
+ baseUrl: 'http://localhost:3000',
+ onClick: fn,
+ }),
+ ],
+ },
+ );
+
+ shadowDom.querySelector('a')?.click();
+
+ expect(fn).toHaveBeenCalledTimes(0);
+ });
});
diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts
index 3566b403a2..b6a6509550 100644
--- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts
+++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts
@@ -17,10 +17,12 @@
import type { Transformer } from './index';
type AddLinkClickListenerOptions = {
+ baseUrl: string;
onClick: (e: MouseEvent, newUrl: string) => void;
};
export const addLinkClickListener = ({
+ baseUrl,
onClick,
}: AddLinkClickListenerOptions): Transformer => {
return dom => {
@@ -28,8 +30,9 @@ export const addLinkClickListener = ({
elem.addEventListener('click', (e: MouseEvent) => {
const target = e.target as HTMLAnchorElement;
const href = target?.getAttribute('href');
+
if (!href) return;
- if (!href.match(/^https?:\/\//i)) {
+ if (href.startsWith(baseUrl)) {
e.preventDefault();
onClick(e, target.getAttribute('href')!);
}
diff --git a/plugins/user-settings/.eslintrc.js b/plugins/user-settings/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/user-settings/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md
new file mode 100644
index 0000000000..db0b01681f
--- /dev/null
+++ b/plugins/user-settings/README.md
@@ -0,0 +1,65 @@
+# user-settings
+
+Welcome to the user-settings plugin!
+
+_This plugin was created through the Backstage CLI_
+
+## About the plugin
+
+This plugin provides two components, ` ` is intended to be used within the [``](https://backstage.io/storybook/?path=/story/sidebar--sample-sidebar) and displays the signed-in users profile picture and name.
+
+The second component is a settings page where the user can control different settings across the App.
+
+## Usage
+
+Add the item to the Sidebar:
+
+```ts
+import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
+
+
+
+
+
+ ;
+```
+
+Add the page to the App routing:
+
+```ts
+import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
+
+const AppRoutes = () => (
+
+ } />
+
+);
+```
+
+### Props
+
+**Auth Providers**
+
+By default, the plugin provides a list of configured authentication providers fetched from `app-config.yaml` and displayed in the "Authentication Providers" tab.
+
+If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop:
+
+```ts
+const MyAuthProviders = () => (
+
+
+ {someAction}
+
+);
+
+const AppRoutes = () => (
+
+ } />}
+ />
+
+);
+```
+
+> **Note that the list of providers expects to be rendered within a MUI [``](https://material-ui.com/components/lists/)**
diff --git a/plugins/user-settings/dev/index.tsx b/plugins/user-settings/dev/index.tsx
new file mode 100644
index 0000000000..264d6f801f
--- /dev/null
+++ b/plugins/user-settings/dev/index.tsx
@@ -0,0 +1,19 @@
+/*
+ * 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 { createDevApp } from '@backstage/dev-utils';
+import { plugin } from '../src/plugin';
+
+createDevApp().registerPlugin(plugin).render();
diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json
new file mode 100644
index 0000000000..29f86c774b
--- /dev/null
+++ b/plugins/user-settings/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@backstage/plugin-user-settings",
+ "version": "0.1.1-alpha.24",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "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": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
+ "@material-ui/core": "^4.11.0",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.45",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "react-router": "6.0.0-beta.0",
+ "react-use": "^15.3.3"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
+ "@backstage/test-utils": "^0.1.1-alpha.24",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^12.0.0",
+ "msw": "^0.20.5",
+ "node-fetch": "^2.6.1"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx
new file mode 100644
index 0000000000..4399b6bd3e
--- /dev/null
+++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 {
+ ApiProvider,
+ ApiRegistry,
+ configApiRef,
+ ConfigReader,
+ googleAuthApiRef,
+} from '@backstage/core';
+import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
+import { fireEvent } from '@testing-library/react';
+import React from 'react';
+import { AuthProviders } from './AuthProviders';
+
+const mockSignInHandler = jest.fn().mockReturnValue('');
+const mockGoogleAuth = {
+ sessionState$: () => ({
+ subscribe: () => ({
+ unsubscribe: () => null,
+ }),
+ }),
+ signIn: mockSignInHandler,
+};
+
+const createConfig = () =>
+ ConfigReader.fromConfigs([
+ {
+ context: '',
+ data: {
+ auth: {
+ providers: {
+ google: { development: {} },
+ },
+ },
+ },
+ },
+ ]);
+
+const config = createConfig();
+
+const apiRegistry = ApiRegistry.from([
+ [configApiRef, config],
+ [googleAuthApiRef, mockGoogleAuth],
+]);
+
+describe(' ', () => {
+ it('displays a provider and calls its sign-in handler on click', async () => {
+ const rendered = await renderWithEffects(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ expect(rendered.getByText('Google')).toBeInTheDocument();
+ expect(
+ rendered.getByText(googleAuthApiRef.description),
+ ).toBeInTheDocument();
+
+ const button = rendered.getByTitle('Sign in to Google');
+ fireEvent.click(button);
+ expect(mockSignInHandler).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx
new file mode 100644
index 0000000000..563bf44150
--- /dev/null
+++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { List } from '@material-ui/core';
+import { configApiRef, InfoCard, useApi } from '@backstage/core';
+import { EmptyProviders } from './EmptyProviders';
+import { DefaultProviderSettings } from './DefaultProviderSettings';
+
+type Props = {
+ providerSettings?: JSX.Element;
+};
+
+export const AuthProviders = ({ providerSettings }: Props) => {
+ const configApi = useApi(configApiRef);
+ const providersConfig = configApi.getOptionalConfig('auth.providers');
+ const configuredProviders = providersConfig?.keys() || [];
+ const providers = providerSettings ?? (
+
+ );
+
+ if (!providerSettings && !configuredProviders?.length) {
+ return ;
+ }
+
+ return (
+
+ {providers}
+
+ );
+};
diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx
new file mode 100644
index 0000000000..a1dab1e7d4
--- /dev/null
+++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 {
+ githubAuthApiRef,
+ gitlabAuthApiRef,
+ googleAuthApiRef,
+ oauth2ApiRef,
+ oktaAuthApiRef,
+ microsoftAuthApiRef,
+} from '@backstage/core';
+import Star from '@material-ui/icons/Star';
+import React from 'react';
+import { ProviderSettingsItem } from './ProviderSettingsItem';
+
+type Props = {
+ configuredProviders: string[];
+};
+
+export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
+ <>
+ {configuredProviders.includes('google') && (
+
+ )}
+ {configuredProviders.includes('microsoft') && (
+
+ )}
+ {configuredProviders.includes('github') && (
+
+ )}
+ {configuredProviders.includes('gitlab') && (
+
+ )}
+ {configuredProviders.includes('okta') && (
+
+ )}
+ {configuredProviders.includes('oauth2') && (
+
+ )}
+ >
+);
diff --git a/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx b/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx
new file mode 100644
index 0000000000..653f449e92
--- /dev/null
+++ b/plugins/user-settings/src/components/AuthProviders/EmptyProviders.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 { CodeSnippet, EmptyState } from '@backstage/core';
+import { Button, Typography } from '@material-ui/core';
+
+const EXAMPLE = `auth:
+ providers:
+ google:
+ development:
+ clientId:
+ $env: AUTH_GOOGLE_CLIENT_ID
+ clientSecret:
+ $env: AUTH_GOOGLE_CLIENT_SECRET
+`;
+
+export const EmptyProviders = () => (
+
+
+ Open app-config.yaml and make the changes as highlighted
+ below:
+
+
+
+ Read More
+
+ >
+ }
+ />
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx
similarity index 65%
rename from packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx
rename to plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx
index 4db7ea0823..7213ee2b83 100644
--- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx
+++ b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx
@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import React, { FC, useState, useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
+import {
+ ApiRef,
+ SessionApi,
+ useApi,
+ IconComponent,
+ SessionState,
+} from '@backstage/core';
import {
ListItem,
ListItemIcon,
@@ -24,25 +30,20 @@ import {
} from '@material-ui/core';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
import { ToggleButton } from '@material-ui/lab';
-import {
- ApiRef,
- SessionApi,
- useApi,
- IconComponent,
- SessionState,
-} from '@backstage/core-api';
-type OAuthProviderSidebarProps = {
+type Props = {
title: string;
+ description: string;
icon: IconComponent;
apiRef: ApiRef;
};
-export const ProviderSettingsItem: FC = ({
+export const ProviderSettingsItem = ({
title,
+ description,
icon: Icon,
apiRef,
-}) => {
+}: Props) => {
const api = useApi(apiRef);
const [signedIn, setSignedIn] = useState(false);
@@ -68,22 +69,30 @@ export const ProviderSettingsItem: FC = ({
-
-
- (signedIn ? api.signOut() : api.signIn())}
- >
-
-
+
+ {description}
-
+ }
+ secondaryTypographyProps={{ noWrap: true, style: { width: '80%' } }}
+ />
+
+
+ (signedIn ? api.signOut() : api.signIn())}
+ >
+
+
+
);
diff --git a/plugins/user-settings/src/components/AuthProviders/index.ts b/plugins/user-settings/src/components/AuthProviders/index.ts
new file mode 100644
index 0000000000..2f6a2b5ab1
--- /dev/null
+++ b/plugins/user-settings/src/components/AuthProviders/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { AuthProviders } from './AuthProviders';
+export { DefaultProviderSettings } from './DefaultProviderSettings';
diff --git a/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx
new file mode 100644
index 0000000000..5df4df4450
--- /dev/null
+++ b/plugins/user-settings/src/components/FeatureFlags/EmptyFlags.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 { CodeSnippet, EmptyState } from '@backstage/core';
+import { Button, Typography } from '@material-ui/core';
+
+const EXAMPLE = `import { createPlugin } from '@backstage/core';
+
+export default createPlugin({
+ id: 'welcome',
+ register({ router, featureFlags }) {
+ featureFlags.register('enable-example-feature');
+ },
+});
+`;
+
+export const EmptyFlags = () => (
+
+
+ An example how how to add a feature flags is highlighted below:
+
+
+
+ Read More
+
+ >
+ }
+ />
+);
diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx
new file mode 100644
index 0000000000..71f59c52b8
--- /dev/null
+++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx
@@ -0,0 +1,82 @@
+/*
+ * 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, { useCallback, useState } from 'react';
+import {
+ FeatureFlagName,
+ featureFlagsApiRef,
+ FeatureFlagsRegistryItem,
+ FeatureFlagState,
+ InfoCard,
+ useApi,
+} from '@backstage/core';
+import { List } from '@material-ui/core';
+import { EmptyFlags } from './EmptyFlags';
+import { FlagItem } from './FeatureFlagsItem';
+
+export const FeatureFlags = () => {
+ const featureFlagsApi = useApi(featureFlagsApiRef);
+ const featureFlags = featureFlagsApi.getRegisteredFlags();
+ const initialFlagState = featureFlags.reduce(
+ (result, featureFlag: FeatureFlagsRegistryItem) => {
+ const state = featureFlagsApi.getFlags().get(featureFlag.name);
+
+ result[featureFlag.name] = state;
+ return result;
+ },
+ {} as Record,
+ );
+
+ const [state, setState] = useState>(
+ initialFlagState,
+ );
+
+ const toggleFlag = useCallback(
+ (flagName: FeatureFlagName) => {
+ const newState = featureFlagsApi.getFlags().toggle(flagName);
+
+ setState(prevState => ({
+ ...prevState,
+ [flagName]: newState,
+ }));
+ featureFlagsApi.getFlags().save();
+ },
+ [featureFlagsApi],
+ );
+
+ if (!featureFlags.length) {
+ return ;
+ }
+
+ return (
+
+
+ {featureFlags.map(featureFlag => {
+ const enabled = Boolean(state[featureFlag.name]);
+
+ return (
+
+ );
+ })}
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx
similarity index 51%
rename from packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx
rename to plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx
index 3e40ce69cc..92065f2ed6 100644
--- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx
+++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx
@@ -15,11 +15,6 @@
*/
import React from 'react';
-import {
- FeatureFlagName,
- useApi,
- featureFlagsApiRef,
-} from '@backstage/core-api';
import {
ListItem,
ListItemSecondaryAction,
@@ -28,46 +23,31 @@ import {
} from '@material-ui/core';
import CheckIcon from '@material-ui/icons/CheckCircle';
import { ToggleButton } from '@material-ui/lab';
-
-export type Item = {
- name: FeatureFlagName;
- pluginId: string;
-};
+import { FeatureFlagsRegistryItem } from '@backstage/core';
type Props = {
- featureFlag: Item;
+ flag: FeatureFlagsRegistryItem;
+ enabled: boolean;
+ toggleHandler: Function;
};
-export const FlagItem = ({ featureFlag }: Props) => {
- const api = useApi(featureFlagsApiRef);
-
- const [enabled, setEnabled] = React.useState(
- Boolean(api.getFlags().get(featureFlag.name)),
- );
-
- const toggleFlag = () => {
- const newState = api.getFlags().toggle(featureFlag.name);
- setEnabled(Boolean(newState));
- };
-
- return (
-
-
-
+export const FlagItem = ({ flag, enabled, toggleHandler }: Props) => (
+
+
+
+
toggleHandler(flag.name)}
>
-
-
-
+
-
-
- );
-};
+
+
+
+);
diff --git a/plugins/user-settings/src/components/FeatureFlags/index.ts b/plugins/user-settings/src/components/FeatureFlags/index.ts
new file mode 100644
index 0000000000..37c9fd1fd2
--- /dev/null
+++ b/plugins/user-settings/src/components/FeatureFlags/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { FeatureFlags } from './FeatureFlags';
diff --git a/plugins/user-settings/src/components/General/General.tsx b/plugins/user-settings/src/components/General/General.tsx
new file mode 100644
index 0000000000..00ac17dfe1
--- /dev/null
+++ b/plugins/user-settings/src/components/General/General.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { InfoCard } from '@backstage/core';
+import { Grid, List } from '@material-ui/core';
+import React from 'react';
+import { PinButton } from './PinButton';
+import { Profile } from './Profile';
+import { ThemeToggle } from './ThemeToggle';
+
+export const General = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/plugins/user-settings/src/components/General/PinButton.test.tsx b/plugins/user-settings/src/components/General/PinButton.test.tsx
new file mode 100644
index 0000000000..76af8ce8d4
--- /dev/null
+++ b/plugins/user-settings/src/components/General/PinButton.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SidebarPinStateContext } from '@backstage/core';
+import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
+import { fireEvent } from '@testing-library/react';
+import React from 'react';
+import { PinButton } from './PinButton';
+
+describe(' ', () => {
+ it('toggles the pin sidebar button', async () => {
+ const mockToggleFn = jest.fn();
+ const rendered = await renderWithEffects(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument();
+
+ const pinButton = rendered.getByTitle('Pin Sidebar');
+ fireEvent.click(pinButton);
+ expect(mockToggleFn).toHaveBeenCalled();
+ });
+});
diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/plugins/user-settings/src/components/General/PinButton.tsx
similarity index 65%
rename from packages/core/src/layout/Sidebar/Settings/PinButton.tsx
rename to plugins/user-settings/src/components/General/PinButton.tsx
index 2727313ede..e36fc365f8 100644
--- a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx
+++ b/plugins/user-settings/src/components/General/PinButton.tsx
@@ -24,23 +24,18 @@ import {
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import { ToggleButton } from '@material-ui/lab';
-import { SidebarPinStateContext } from '../Page';
+import { SidebarPinStateContext } from '@backstage/core';
-export const SidebarPinButton = () => {
+type PinIconProps = { isPinned: boolean };
+
+const PinIcon = ({ isPinned }: PinIconProps) =>
+ isPinned ? : ;
+
+export const PinButton = () => {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
- const PinIcon = () => (
-
- {isPinned ? : }
-
- );
-
return (
{
secondary="Prevent the sidebar from collapsing"
/>
- {
- toggleSidebarPinState();
- }}
+
-
-
+ {
+ toggleSidebarPinState();
+ }}
+ >
+
+
+
);
diff --git a/plugins/user-settings/src/components/General/Profile.tsx b/plugins/user-settings/src/components/General/Profile.tsx
new file mode 100644
index 0000000000..b0231d95b6
--- /dev/null
+++ b/plugins/user-settings/src/components/General/Profile.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 { InfoCard } from '@backstage/core';
+import { Grid, Typography } from '@material-ui/core';
+import React from 'react';
+import { SignInAvatar } from './SignInAvatar';
+import { UserSettingsMenu } from './UserSettingsMenu';
+import { useUserProfile } from '../useUserProfileInfo';
+
+export const Profile = () => {
+ const { profile, displayName } = useUserProfile();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {displayName}
+
+
+ {profile.email}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/plugins/user-settings/src/components/General/SignInAvatar.tsx
similarity index 72%
rename from packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx
rename to plugins/user-settings/src/components/General/SignInAvatar.tsx
index f0430edfcf..8a5bbd75bf 100644
--- a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx
+++ b/plugins/user-settings/src/components/General/SignInAvatar.tsx
@@ -17,26 +17,24 @@
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, Avatar } from '@material-ui/core';
-import { useUserProfile } from './useUserProfileInfo';
-import { sidebarConfig } from '../config';
+import { useUserProfile } from '../useUserProfileInfo';
+import { sidebarConfig } from '@backstage/core';
-const useStyles = makeStyles({
+const useStyles = makeStyles(theme => ({
avatar: {
width: ({ size }) => size,
height: ({ size }) => size,
+ fontSize: ({ size }) => size * 0.7,
+ border: `1px solid ${theme.palette.textSubtle}`,
},
-});
+}));
type Props = { size?: number };
export const SignInAvatar = ({ size }: Props) => {
const { iconSize } = sidebarConfig;
const classes = useStyles(size ? { size } : { size: iconSize });
- const { profile, displayName } = useUserProfile();
+ const { profile } = useUserProfile();
- return (
-
- {displayName[0]}
-
- );
+ return ;
};
diff --git a/plugins/user-settings/src/components/General/ThemeToggle.test.tsx b/plugins/user-settings/src/components/General/ThemeToggle.test.tsx
new file mode 100644
index 0000000000..79d9210826
--- /dev/null
+++ b/plugins/user-settings/src/components/General/ThemeToggle.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 {
+ ApiProvider,
+ ApiRegistry,
+ appThemeApiRef,
+ AppThemeSelector,
+} from '@backstage/core';
+import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
+import { lightTheme } from '@backstage/theme';
+import { fireEvent } from '@testing-library/react';
+import React from 'react';
+import { ThemeToggle } from './ThemeToggle';
+
+const mockTheme = {
+ id: 'light',
+ title: 'Mock Theme',
+ variant: 'light' as 'light', // wut?
+ theme: lightTheme,
+};
+
+const apiRegistry = ApiRegistry.from([
+ [appThemeApiRef, AppThemeSelector.createWithStorage([mockTheme])],
+]);
+
+describe(' ', () => {
+ it('toggles the theme select button', async () => {
+ const themeApi = apiRegistry.get(appThemeApiRef);
+ const rendered = await renderWithEffects(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ expect(rendered.getByText('Theme')).toBeInTheDocument();
+
+ const themeButton = rendered.getByTitle('Select Mock Theme');
+ expect(themeApi?.getActiveThemeId()).toBe(undefined);
+ fireEvent.click(themeButton);
+ expect(themeApi?.getActiveThemeId()).toBe('light');
+ });
+});
diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/plugins/user-settings/src/components/General/ThemeToggle.tsx
similarity index 53%
rename from packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx
rename to plugins/user-settings/src/components/General/ThemeToggle.tsx
index a5e703089c..111bd838a8 100644
--- a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx
+++ b/plugins/user-settings/src/components/General/ThemeToggle.tsx
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { cloneElement } from 'react';
import { useObservable } from 'react-use';
-import LightIcon from '@material-ui/icons/WbSunny';
-import DarkIcon from '@material-ui/icons/Brightness2';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
-import { appThemeApiRef, useApi } from '@backstage/core-api';
+import { appThemeApiRef, useApi } from '@backstage/core';
import ToggleButton from '@material-ui/lab/ToggleButton';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import {
@@ -29,7 +27,43 @@ import {
Tooltip,
} from '@material-ui/core';
-export const SidebarThemeToggle = () => {
+type ThemeIconProps = {
+ id: string;
+ activeId: string | undefined;
+ icon: JSX.Element | undefined;
+};
+
+const ThemeIcon = ({ id, activeId, icon }: ThemeIconProps) =>
+ icon ? (
+ cloneElement(icon, {
+ color: activeId === id ? 'primary' : undefined,
+ })
+ ) : (
+
+ );
+
+type TooltipToggleButtonProps = {
+ children: JSX.Element;
+ title: string;
+ value: string;
+};
+
+// ToggleButtonGroup uses React.children.map instead of context
+// so wrapping with Tooltip breaks ToggleButton functionality.
+const TooltipToggleButton = ({
+ children,
+ title,
+ value,
+ ...props
+}: TooltipToggleButtonProps) => (
+
+
+ {children}
+
+
+);
+
+export const ThemeToggle = () => {
const appThemeApi = useApi(appThemeApiRef);
const themeId = useObservable(
appThemeApi.activeThemeId$(),
@@ -37,11 +71,6 @@ export const SidebarThemeToggle = () => {
);
const themeIds = appThemeApi.getInstalledThemes();
- // TODO(marcuseide): can these be put on the theme itself?
- const themeIcons = {
- dark: ,
- light: ,
- };
const handleSetTheme = (
_event: React.MouseEvent,
@@ -64,22 +93,24 @@ export const SidebarThemeToggle = () => {
value={themeId ?? 'auto'}
onChange={handleSetTheme}
>
- {themeIds.map(theme => (
-
- {
+ const themeIcon = themeIds.find(t => t.id === theme.id)?.icon;
+
+ return (
+
- {themeIcons[theme.variant]}
-
+
+
+ );
+ })}
+
+
+
- ))}
-
-
-
-
-
+
diff --git a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx
new file mode 100644
index 0000000000..c86d702e30
--- /dev/null
+++ b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
+import { fireEvent } from '@testing-library/react';
+import React from 'react';
+import { UserSettingsMenu } from './UserSettingsMenu';
+
+describe(' ', () => {
+ it('displays a menu button with a sign-out option', async () => {
+ const rendered = await renderWithEffects(
+ wrapInTestApp( ),
+ );
+
+ const menuButton = rendered.getByLabelText('more');
+ fireEvent.click(menuButton);
+
+ expect(rendered.getByText('Sign Out')).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.tsx
similarity index 93%
rename from packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx
rename to plugins/user-settings/src/components/General/UserSettingsMenu.tsx
index 151ddb6e75..19c3ee4e2d 100644
--- a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx
+++ b/plugins/user-settings/src/components/General/UserSettingsMenu.tsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
-import { identityApiRef, useApi } from '@backstage/core-api';
+import { identityApiRef, useApi } from '@backstage/core';
import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
import MoreVertIcon from '@material-ui/icons/MoreVert';
@@ -39,7 +39,7 @@ export const UserSettingsMenu = () => {
return (
<>
-
+
diff --git a/plugins/user-settings/src/components/General/index.ts b/plugins/user-settings/src/components/General/index.ts
new file mode 100644
index 0000000000..2015d345fe
--- /dev/null
+++ b/plugins/user-settings/src/components/General/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { General } from './General';
+export { SignInAvatar } from './SignInAvatar';
diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/plugins/user-settings/src/components/Settings.tsx
similarity index 57%
rename from packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx
rename to plugins/user-settings/src/components/Settings.tsx
index 5687446511..f754f1e7c3 100644
--- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx
+++ b/plugins/user-settings/src/components/Settings.tsx
@@ -15,18 +15,20 @@
*/
import React from 'react';
-import List from '@material-ui/core/List';
-import ListSubheader from '@material-ui/core/ListSubheader';
-import { FlagItem, Item } from './FeatureFlagsItem';
+import { SidebarItem } from '@backstage/core';
+import { SignInAvatar } from './General';
+import { useUserProfile } from './useUserProfileInfo';
+import { settingsRouteRef } from '../plugin';
-type Props = {
- featureFlags: Item[];
+export const Settings = () => {
+ const { displayName } = useUserProfile();
+ const SidebarAvatar = () => ;
+
+ return (
+
+ );
};
-
-export const FeatureFlagsList = ({ featureFlags }: Props) => (
- Feature Flags}>
- {featureFlags.map(featureFlag => (
-
- ))}
-
-);
diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx
new file mode 100644
index 0000000000..86096e9a1f
--- /dev/null
+++ b/plugins/user-settings/src/components/SettingsPage.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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, { useState } from 'react';
+import { Content, Header, HeaderTabs, Page, pageTheme } from '@backstage/core';
+import { General } from './General';
+import { AuthProviders } from './AuthProviders';
+import { FeatureFlags } from './FeatureFlags';
+
+type Props = {
+ providerSettings?: JSX.Element;
+};
+
+export const SettingsPage = ({ providerSettings }: Props) => {
+ const [activeTab, setActiveTab] = useState(0);
+ const onTabChange = (index: number) => {
+ setActiveTab(index);
+ };
+
+ const tabs = [
+ { id: 'general', label: 'General' },
+ { id: 'auth-providers', label: 'Authentication Providers' },
+ { id: 'feature-flags', label: 'Feature Flags' },
+ ];
+
+ const content = [
+ ,
+ ,
+ ,
+ ];
+
+ return (
+
+
+
+ {content[activeTab]}
+
+ );
+};
diff --git a/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx b/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx
new file mode 100644
index 0000000000..fd3d6ce0f0
--- /dev/null
+++ b/plugins/user-settings/src/components/SidebarWithSettings.stories.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 {
+ ApiProvider,
+ ApiRegistry,
+ appThemeApiRef,
+ AppThemeSelector,
+ configApiRef,
+ ConfigReader,
+ FeatureFlags,
+ featureFlagsApiRef,
+ Sidebar,
+ SidebarDivider,
+ SidebarSpace,
+} from '@backstage/core';
+import { MemoryRouter } from 'react-router';
+import { Settings } from './Settings';
+import { SettingsPage } from './SettingsPage';
+
+export default {
+ title: 'Plugins/user-settings/Settings',
+ component: Settings,
+ decorators: [
+ (storyFn: () => JSX.Element) => (
+ {storyFn()}
+ ),
+ ],
+};
+
+export const SidebarItem = () => (
+
+
+
+
+
+);
+
+const createConfig = () =>
+ ConfigReader.fromConfigs([
+ {
+ context: '',
+ data: {
+ auth: {
+ providers: {},
+ },
+ },
+ },
+ ]);
+
+const config = createConfig();
+
+const apis = ApiRegistry.from([
+ [configApiRef, config],
+ [featureFlagsApiRef, new FeatureFlags()],
+ [appThemeApiRef, AppThemeSelector.createWithStorage([])],
+]);
+
+export const TheSettingsPage = () => (
+
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts b/plugins/user-settings/src/components/useUserProfileInfo.ts
similarity index 93%
rename from packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts
rename to plugins/user-settings/src/components/useUserProfileInfo.ts
index 60dae294a5..428719160d 100644
--- a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts
+++ b/plugins/user-settings/src/components/useUserProfileInfo.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { useApi, identityApiRef } from '@backstage/core-api';
+import { useApi, identityApiRef } from '@backstage/core';
export const useUserProfile = () => {
const identityApi = useApi(identityApiRef);
diff --git a/plugins/user-settings/src/index.ts b/plugins/user-settings/src/index.ts
new file mode 100644
index 0000000000..113930902a
--- /dev/null
+++ b/plugins/user-settings/src/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { plugin } from './plugin';
+export { Settings } from './components/Settings';
+export { SettingsPage as Router } from './components/SettingsPage';
diff --git a/plugins/user-settings/src/plugin.test.ts b/plugins/user-settings/src/plugin.test.ts
new file mode 100644
index 0000000000..810a2cda12
--- /dev/null
+++ b/plugins/user-settings/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { plugin } from './plugin';
+
+describe('user-settings', () => {
+ it('should export plugin', () => {
+ expect(plugin).toBeDefined();
+ });
+});
diff --git a/plugins/user-settings/src/plugin.ts b/plugins/user-settings/src/plugin.ts
new file mode 100644
index 0000000000..2f896c8197
--- /dev/null
+++ b/plugins/user-settings/src/plugin.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { createPlugin, createRouteRef } from '@backstage/core';
+
+export const settingsRouteRef = createRouteRef({
+ path: '/settings',
+ title: 'Settings',
+});
+
+export const plugin = createPlugin({
+ id: 'user-settings',
+});
diff --git a/plugins/user-settings/src/setupTests.ts b/plugins/user-settings/src/setupTests.ts
new file mode 100644
index 0000000000..0bfa67b49a
--- /dev/null
+++ b/plugins/user-settings/src/setupTests.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 '@testing-library/jest-dom';
diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json
index d6c3a907b7..80f930bbc2 100644
--- a/plugins/welcome/package.json
+++ b/plugins/welcome/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-welcome",
- "version": "0.1.1-alpha.23",
+ "version": "0.1.1-alpha.24",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -21,8 +21,8 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.23",
- "@backstage/theme": "^0.1.1-alpha.23",
+ "@backstage/core": "^0.1.1-alpha.24",
+ "@backstage/theme": "^0.1.1-alpha.24",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -32,8 +32,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.23",
- "@backstage/dev-utils": "^0.1.1-alpha.23",
+ "@backstage/cli": "^0.1.1-alpha.24",
+ "@backstage/dev-utils": "^0.1.1-alpha.24",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
index 3ceb0af58d..1afdb53ad4 100644
--- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
+++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
@@ -69,7 +69,10 @@ const WelcomePage = () => {
the project and we have already begun work on various aspects
of these phases. The best way to keep track of the progress is
through the
-
+
Milestones
.
@@ -113,7 +116,10 @@ const WelcomePage = () => {
We suggest you either check out the documentation for{' '}
-
+
creating a plugin
{' '}
or have a look in the code for the{' '}
@@ -121,7 +127,10 @@ const WelcomePage = () => {
existing plugins
{' '}
in the directory{' '}
-
+
plugins/
.
@@ -135,7 +144,10 @@ const WelcomePage = () => {
backstage.io
-
+
Create a plugin
diff --git a/yarn.lock b/yarn.lock
index ddc17865cc..007b5a199e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,6 +49,13 @@
resolved "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.1.tgz#1403ac5de10d8ca689fc1f65844c27179ae1d44f"
integrity sha512-UQ9BequOTIavs0pTHLMwQwKQF8tTV1oezY/H2O9chA+JNPFZSua55xpU5dPSjAU9/jLJ1VwU+HJuTVN8u7S6Fg==
+"@ardatan/aggregate-error@0.0.6":
+ version "0.0.6"
+ resolved "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz#fe6924771ea40fc98dc7a7045c2e872dc8527609"
+ integrity sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==
+ dependencies:
+ tslib "~2.0.1"
+
"@asyncapi/avro-schema-parser@^0.1.2":
version "0.1.2"
resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.1.2.tgz#f6c340ccaa24bc36399d3a0f1a6e02790a948453"
@@ -180,7 +187,7 @@
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/generator@^7.9.6":
+"@babel/generator@^7.11.5", "@babel/generator@^7.9.6":
version "7.11.6"
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
@@ -415,7 +422,12 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@7.11.3", "@babel/parser@^7.0.0", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1":
+"@babel/parser@7.11.5", "@babel/parser@^7.11.5":
+ version "7.11.5"
+ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
+ integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+
+"@babel/parser@^7.0.0", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1":
version "7.11.3"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9"
integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==
@@ -1182,7 +1194,7 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.11.2"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
@@ -1207,7 +1219,22 @@
"@babel/parser" "^7.8.6"
"@babel/types" "^7.8.6"
-"@babel/traverse@7.11.0", "@babel/traverse@^7.0.0", "@babel/traverse@^7.11.0":
+"@babel/traverse@7.11.5":
+ version "7.11.5"
+ resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
+ integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/generator" "^7.11.5"
+ "@babel/helper-function-name" "^7.10.4"
+ "@babel/helper-split-export-declaration" "^7.11.0"
+ "@babel/parser" "^7.11.5"
+ "@babel/types" "^7.11.5"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.19"
+
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.11.0":
version "7.11.0"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24"
integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==
@@ -1252,19 +1279,19 @@
globals "^11.1.0"
lodash "^4.17.19"
-"@babel/types@7.11.0", "@babel/types@^7.11.0":
- version "7.11.0"
- resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d"
- integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==
+"@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.5", "@babel/types@^7.9.6":
+ version "7.11.5"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
+ integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
-"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.5", "@babel/types@^7.9.6":
- version "7.11.5"
- resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
- integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
+"@babel/types@^7.11.0":
+ version "7.11.0"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d"
+ integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
@@ -1738,22 +1765,22 @@
xcase "^2.0.1"
"@graphql-codegen/cli@^1.17.7":
- version "1.17.8"
- resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.17.8.tgz#06a68894f01796046bb7fa092814948e592e8a75"
- integrity sha512-OIOqzdL9kcgO67fBazwpr3Pb+T2l2erYGsO37qJmCrvyqeNO9we+oYrGgVqswkRnfqaqvVWRJ2OmwzvFrXNy/A==
+ version "1.17.10"
+ resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.17.10.tgz#efebf9b887fdb94dd26dbf3eb1950e832efcda0e"
+ integrity sha512-nQYbabB3aS3XArETJc/NrpUtlj/Dzh2KeDgvVRsc3zxopcudZTKj1dcTCOA/QZPSpbzALbZZ/1loBVVrMIe+Iw==
dependencies:
"@graphql-codegen/core" "1.17.8"
- "@graphql-codegen/plugin-helpers" "^1.17.8"
- "@graphql-tools/apollo-engine-loader" "^6.0.18"
- "@graphql-tools/code-file-loader" "^6.0.18"
- "@graphql-tools/git-loader" "^6.0.18"
- "@graphql-tools/github-loader" "^6.0.18"
- "@graphql-tools/graphql-file-loader" "^6.0.18"
- "@graphql-tools/json-file-loader" "^6.0.18"
- "@graphql-tools/load" "^6.0.18"
- "@graphql-tools/prisma-loader" "^6.0.18"
- "@graphql-tools/url-loader" "^6.0.18"
- "@graphql-tools/utils" "^6.0.18"
+ "@graphql-codegen/plugin-helpers" "^1.17.9"
+ "@graphql-tools/apollo-engine-loader" "^6"
+ "@graphql-tools/code-file-loader" "^6"
+ "@graphql-tools/git-loader" "^6"
+ "@graphql-tools/github-loader" "^6"
+ "@graphql-tools/graphql-file-loader" "^6"
+ "@graphql-tools/json-file-loader" "^6"
+ "@graphql-tools/load" "^6"
+ "@graphql-tools/prisma-loader" "^6"
+ "@graphql-tools/url-loader" "^6"
+ "@graphql-tools/utils" "^6"
ansi-escapes "^4.3.1"
camel-case "^4.1.1"
chalk "^4.1.0"
@@ -1770,6 +1797,7 @@
inquirer "^7.3.3"
is-glob "^4.0.1"
json-to-pretty-yaml "^1.2.2"
+ latest-version "5.1.0"
listr "^0.14.3"
listr-update-renderer "^0.5.0"
log-symbols "^4.0.0"
@@ -1779,12 +1807,12 @@
pascal-case "^3.1.1"
request "^2.88.2"
string-env-interpolation "^1.0.1"
- ts-log "^2.1.4"
- tslib "~2.0.0"
+ ts-log "^2.2.3"
+ tslib "~2.0.1"
upper-case "^2.0.1"
valid-url "^1.0.9"
wrap-ansi "^7.0.0"
- yargs "^15.4.1"
+ yargs "^16.0.3"
"@graphql-codegen/core@1.17.8":
version "1.17.8"
@@ -1796,21 +1824,21 @@
"@graphql-tools/utils" "^6.0.18"
tslib "~2.0.0"
-"@graphql-codegen/plugin-helpers@^1.17.8":
- version "1.17.8"
- resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.17.8.tgz#d47e8bd6b425b38cfc4e2d670b31183b8e9a91e8"
- integrity sha512-Ck1E4QC4yrhX5zUtMlc/uloly6TWSpJDA7+aJrdMOY+MEUgrrM7wJsLV9azNae2OmrhZAQopSuGezjHNbsqSdA==
+"@graphql-codegen/plugin-helpers@^1.17.8", "@graphql-codegen/plugin-helpers@^1.17.9":
+ version "1.17.9"
+ resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.17.9.tgz#429f7f75b9f09f5229e42546027613f62ef0d4b1"
+ integrity sha512-kyj+qsnLGd1JLqXuLpvI6Q/VW7frhoHHNxYJWerpwsSV9m4cttmFj8d9JTfYPZbg6cLIRR7+10lVugzMKozjzA==
dependencies:
- "@graphql-tools/utils" "^6.0.18"
+ "@graphql-tools/utils" "^6"
camel-case "4.1.1"
common-tags "1.8.0"
constant-case "3.0.3"
import-from "3.0.0"
- lodash "~4.17.15"
+ lodash "~4.17.20"
lower-case "2.0.1"
param-case "3.0.3"
pascal-case "3.1.1"
- tslib "~2.0.0"
+ tslib "~2.0.1"
upper-case "2.0.1"
"@graphql-codegen/typescript-resolvers@^1.17.7":
@@ -1891,160 +1919,113 @@
graphql-tools "5.0.0"
tslib "1.11.1"
-"@graphql-tools/apollo-engine-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-6.0.18.tgz#7af9457dc7aab8068870445723a319454eb4f34e"
- integrity sha512-zP1tbf6FiIFE87oMPZ0WED8Is2YZ0zKLCDUxK5yTZ5I17mlk/8q6Q3gJbgDWi+QdA8eVcgArJBMWsUuclNNX0g==
+"@graphql-tools/apollo-engine-loader@^6":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-6.2.4.tgz#bed59ccac654e36a62f736e035697e2e5de152ba"
+ integrity sha512-aYDyEs7Q0J0og7E/B7zj2+62Jf8QerkwV+hQ5wwGLSQlYnLDTB+hMNBG/3ga9qMQ5UVQ+d45ckXKN9nOl6LX7g==
dependencies:
- "@graphql-tools/utils" "6.0.18"
- cross-fetch "3.0.5"
- tslib "~2.0.0"
+ "@graphql-tools/utils" "^6.2.4"
+ cross-fetch "3.0.6"
+ tslib "~2.0.1"
-"@graphql-tools/code-file-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.0.18.tgz#71873bcd3d5d9d7dd4279afd45de8c8c28fdd4a2"
- integrity sha512-V13hBezvcNZ+I1hoR+Q7bLH1pCxhZ9Nsn91bX+owhmOvn2W56dAn3RpGdnhoM7v6D6Uo2Zkdtqkaq1Pin4rexA==
+"@graphql-tools/code-file-loader@^6":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.2.4.tgz#ce194c19b2fcd714bffa4c0c529a4c65a6b0db4b"
+ integrity sha512-aDVI/JVUXIdqSJJKLjpBaqOAOCa5yUvsgQZu2Q9nVwV9faGlQi5MUuYAh1xp0LW80/5/unbiZ5/taRUyUY/6Eg==
dependencies:
- "@graphql-tools/graphql-tag-pluck" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
+ "@graphql-tools/graphql-tag-pluck" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
fs-extra "9.0.1"
- tslib "~2.0.0"
+ tslib "~2.0.1"
-"@graphql-tools/delegate@6.0.15":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.15.tgz#9e060bfc31fe7735bd5b2b401e98dea3fa5d3b25"
- integrity sha512-GG/zp29PMfG6eXpfe1M5C3U1EI1f3tJu2glFN8t0RIfp4FEgZs/PRvZuuep5orFge8dvX/LQpJY8Vl2JmU4WMg==
+"@graphql-tools/delegate@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.2.4.tgz#db553b63eb9512d5eb5bbfdfcd8cb1e2b534699c"
+ integrity sha512-mXe6DfoWmq49kPcDrpKHgC2DSWcD5q0YCaHHoXYPAOlnLH8VMTY8BxcE8y/Do2eyg+GLcwAcrpffVszWMwqw0w==
dependencies:
- "@ardatan/aggregate-error" "0.0.1"
- "@graphql-tools/schema" "6.0.15"
- "@graphql-tools/utils" "6.0.15"
- tslib "~2.0.0"
-
-"@graphql-tools/delegate@6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.18.tgz#093e335e346cd26791222e066a3e5929bc38b79c"
- integrity sha512-CmNTD60qcTEZM3bvOV2t3Zdj7veY0zgXXVXNgMC9Fx+D2dNdJFCwXdcPAF0SKqlJoj/alBDSl1U6nqYKT9fQOA==
- dependencies:
- "@ardatan/aggregate-error" "0.0.1"
- "@graphql-tools/schema" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
+ "@ardatan/aggregate-error" "0.0.6"
+ "@graphql-tools/schema" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
+ dataloader "2.0.0"
is-promise "4.0.0"
- tslib "~2.0.0"
+ tslib "~2.0.1"
-"@graphql-tools/git-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-6.0.18.tgz#ac0cdc07fbc8549ec2ca08456b2f70d214c3f4ba"
- integrity sha512-WR4K10+UtguMaN4/AqcJMRsTRy6Er9xKhehmEDMQTbu4pAICkLXyiSsA3f0jfhGBu6JudrzKD0HiaW6eWrMvVg==
+"@graphql-tools/git-loader@^6":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-6.2.4.tgz#2502d48cb1253bde7df3f3e1dfd2bdcf7ff72b82"
+ integrity sha512-urMwWhhsZUKnX9MDHXbMUfZd568pWwj1Bx1O2M7N8I25GqZDW54Fzj9DudlVKE5M9twMtoEyBTH7sH4tscliqg==
dependencies:
- "@graphql-tools/graphql-tag-pluck" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
+ "@graphql-tools/graphql-tag-pluck" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
+ tslib "~2.0.1"
-"@graphql-tools/github-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-6.0.18.tgz#070655daf02f4c8cd316f6cf96ee0c916cece835"
- integrity sha512-DqIuTiZvubN+uOtuuUyAJXHlgpsutyIGyNi+v/v9s4WO7oq+cE/Gc4dpmG6G8KHVXP7ngvMIGDDEXj4MGx1vpw==
+"@graphql-tools/github-loader@^6":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-6.2.4.tgz#38520b5964594a578dbb4a7693a76938a79877a1"
+ integrity sha512-p4peplm/Ot989bCD4XATK5NEXX7l39BXNw+YKaqgoEoHopyQ142I2Zb0GJiMRjW9yXGqIlDjG4reZazleiprgQ==
dependencies:
- "@graphql-tools/graphql-tag-pluck" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
- cross-fetch "3.0.5"
+ "@graphql-tools/graphql-tag-pluck" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
+ cross-fetch "3.0.6"
+ tslib "~2.0.1"
-"@graphql-tools/graphql-file-loader@^6.0.0":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.0.15.tgz#e186b8147397bd7510e1b3318f5d3f7d365fc4e1"
- integrity sha512-QbCf731A2A2hrHP+cMSAKvY3D7IauFNqp5bAGdbLwSHRqaxUIfKi7Q76/9pZ3rN/e6yu/zVz+t1rkf7lT2/8OA==
+"@graphql-tools/graphql-file-loader@^6", "@graphql-tools/graphql-file-loader@^6.0.0":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.2.4.tgz#1765b644cd621040f232f5c32321b45c187399a7"
+ integrity sha512-IcdUZoOlkCGr0KO8QCO8G031CDDv5dzHBZeN5H1gzE2AVFFwn2AexysrUXBxftm2DQIOuV+Knap7dC4Ol54kNA==
dependencies:
- "@graphql-tools/import" "6.0.15"
- "@graphql-tools/utils" "6.0.15"
+ "@graphql-tools/import" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
fs-extra "9.0.1"
- tslib "~2.0.0"
+ tslib "~2.0.1"
-"@graphql-tools/graphql-file-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.0.18.tgz#f2541c87dd8d41d8e621dd8ae60d2e21733a99b8"
- integrity sha512-9EduQkDbLf8aDEalIZFCD3W+WKnkEihBPMq28J0rrnkpxAqSke631AA8wNsXurVlld4KMuPCSTDhttZMP27frA==
+"@graphql-tools/graphql-tag-pluck@^6.2.4":
+ version "6.2.5"
+ resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.2.5.tgz#5c0c47362406a55aaf661c4af0209b542b8483dc"
+ integrity sha512-qvdIOTanBuKYLIMSYl9Tk+ej9dq00B4BqUnHqoCvYtSjD1n1UINGrqXgwMT+JXp66gUZWw8BU9Ke92mQ4UwTpg==
dependencies:
- "@graphql-tools/import" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
- fs-extra "9.0.1"
- tslib "~2.0.0"
-
-"@graphql-tools/graphql-tag-pluck@6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.0.18.tgz#54eee3bae27a1ac81cf24d92c3b1b8a8dff4d801"
- integrity sha512-8qutnHQuiPiXSjMHwQGBHQuUA9El5Ful9ib0IdU5028sM239bJHqoYVM37Gf3gQ0QxVlrV9G1dxvuNv2KNmP+A==
- dependencies:
- "@babel/parser" "7.11.3"
- "@babel/traverse" "7.11.0"
- "@babel/types" "7.11.0"
- "@graphql-tools/utils" "6.0.18"
+ "@babel/parser" "7.11.5"
+ "@babel/traverse" "7.11.5"
+ "@babel/types" "7.11.5"
+ "@graphql-tools/utils" "^6.2.4"
+ tslib "~2.0.1"
optionalDependencies:
- vue-template-compiler "^2.6.11"
+ vue-template-compiler "^2.6.12"
-"@graphql-tools/import@6.0.15":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.0.15.tgz#d6a9d3b6199a1e07d80d8021635d317b2c190c6f"
- integrity sha512-YaQizD031nlrObiAJj+DO+0Wf2ompR2G5OFNQZIOgUlm1+kfH3GPIFoE5Ww74YH6vy9s4UyYYeZJz6APxPdMzg==
+"@graphql-tools/import@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.2.4.tgz#0547f6d4754a924e80439d6af013577cdb617194"
+ integrity sha512-Q6fk6hbtDevoEVcgwb3WRn7XOqGY4MnX3Mvc+x8/b8k4RZ4wT+0WSLRDXGAKiVKRxGhgouU2lZVnGE/LDrGSCg==
dependencies:
fs-extra "9.0.1"
resolve-from "5.0.0"
+ tslib "~2.0.1"
-"@graphql-tools/import@6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.0.18.tgz#37d434ca2130ee3360446ce3cb0e35d9dc62f68c"
- integrity sha512-qGbVJOlC6dy+eu0A+lg5dFAsqYOF4uLxIWbvdGHj+4bkevkfVDuXvd5dvV3PY1f9GRS6c5pCGqOY9MsZmBTGog==
+"@graphql-tools/json-file-loader@^6", "@graphql-tools/json-file-loader@^6.0.0":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.4.tgz#0707fedfced73dd91b1dd81dfa02e83413e5aeaa"
+ integrity sha512-1iL6wwZrUt888hExlNEloSpNXuuUFYD2KV2FZ82t6yiq6bO9Iyg12SUuGd5xVXx9jUkdaHRZc0plMyuIA6gTGA==
dependencies:
+ "@graphql-tools/utils" "^6.2.4"
fs-extra "9.0.1"
- resolve-from "5.0.0"
+ tslib "~2.0.1"
-"@graphql-tools/json-file-loader@^6.0.0":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.0.15.tgz#e341a5fed1ef3af8f8bfde17ed9a14e1a6df36c0"
- integrity sha512-SQO7w+KPxW6Q3snE3G4eNOA8CcBBDYHpk8JILj93oe4BassuPY5NCUOeZ+2PYczwZQbTNDQXeW1oQou44U1aBg==
+"@graphql-tools/load@^6", "@graphql-tools/load@^6.0.0":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.4.tgz#a1a860bdc9d9e0bd93e1dffdbd2cf8839a521c41"
+ integrity sha512-FlQC50VELwRxoWUbJMMMs5gG0Dl8BaQYMrXUHTsxwqR7UmksUYnysC21rdousvs6jVZ7pf4unZfZFtBjz+8Edg==
dependencies:
- "@graphql-tools/utils" "6.0.15"
- fs-extra "9.0.1"
- tslib "~2.0.0"
-
-"@graphql-tools/json-file-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.0.18.tgz#e7fd75c9f16d34ba0e8d7b64b69ccf48f32d9f9b"
- integrity sha512-a7/jkvZXgJlm2EOExCVT4zaZYXyp+pL1pjA8NWitmLIQwP0lIPzY64st/GvCrkDsO43RTltGCMFb66qPreIWWA==
- dependencies:
- "@graphql-tools/utils" "6.0.18"
- fs-extra "9.0.1"
- tslib "~2.0.0"
-
-"@graphql-tools/load@^6.0.0":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.0.15.tgz#f2012b8938b3a535dbafefbb719997684028f30f"
- integrity sha512-STH3ZjbViRqDyCw+f7PZrnDs6yhP7m2l4x5lJBMyMeLaLwuO1z+WhgtqYZNpCYlQY2jNSLXWCa0nWmpYvdLnlA==
- dependencies:
- "@graphql-tools/merge" "6.0.15"
- "@graphql-tools/utils" "6.0.15"
+ "@graphql-tools/merge" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
globby "11.0.1"
import-from "3.0.0"
is-glob "4.0.1"
p-limit "3.0.2"
- tslib "~2.0.0"
+ tslib "~2.0.1"
unixify "1.0.0"
valid-url "1.0.9"
-"@graphql-tools/load@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.0.18.tgz#41a71707ff94d20740324fb659aa3fe021772bbe"
- integrity sha512-DjaxkruBBRoR4twHOv8kzUp2ZP30s/Gvh92sm/EhxG89BgAXRZq5Ua5OqW/ASFvTa7x3V9PyfKsxTK5N3bOgtA==
- dependencies:
- "@graphql-tools/merge" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
- globby "11.0.1"
- import-from "3.0.0"
- is-glob "4.0.1"
- p-limit "3.0.2"
- tslib "~2.0.0"
- unixify "1.0.0"
- valid-url "1.0.9"
-
-"@graphql-tools/merge@6.0.15", "@graphql-tools/merge@^6.0.0":
+"@graphql-tools/merge@^6.0.0":
version "6.0.15"
resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.15.tgz#09c84bd08971edfb3e1035d71432b47687820bdc"
integrity sha512-qusTLzkf6GtxS6LRQnEAWIwA1BeJj5SkZ2pnE4/wVe9gs0grpEsOKYxvGpBi8IZR7r8UeNpkdgk2HP0jlq/WWA==
@@ -2053,7 +2034,7 @@
"@graphql-tools/utils" "6.0.15"
tslib "~2.0.0"
-"@graphql-tools/merge@6.0.18", "@graphql-tools/merge@^6.0.18":
+"@graphql-tools/merge@^6.0.18":
version "6.0.18"
resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.18.tgz#d105e16c6f5f874ddfdba3f3f4a2f676a6d2d04a"
integrity sha512-XAFbqMyXsExnuzgr5+JQC8mxsYp0aGIR0m+GbleQDZSlqDOL2maF5xM3dGOOkguz0Paa7ei/5UfnMXyRU3cW/w==
@@ -2062,34 +2043,43 @@
"@graphql-tools/utils" "6.0.18"
tslib "~2.0.0"
-"@graphql-tools/prisma-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-6.0.18.tgz#a48216c155f3f90809781ff6acfa8c963e379252"
- integrity sha512-n27qZL18aRuPx7ODbDJbFz6ykB6TjbVs4G7Pv8hBmpmm7isy6FTpeqKV1BZtGcoio0zhBUuaKpYEs7pFzST3Iw==
+"@graphql-tools/merge@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.4.tgz#5b3b68083d55a38a7f3caac6e0adc46f428c2a3b"
+ integrity sha512-hQbiSzCJgzUYG1Aspj5EAUY9DsbTI2OK30GLBOjUI16DWkoLVXLXy4ljQYJxq6wDc4fqixMOmvxwf8FoJ9okmw==
dependencies:
- "@graphql-tools/url-loader" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
+ "@graphql-tools/schema" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
+ tslib "~2.0.1"
+
+"@graphql-tools/prisma-loader@^6":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-6.2.4.tgz#3f902b9f1d36ae0c4731e1fe963178bea300af49"
+ integrity sha512-4S6j+7kNHKLDnK6mgVj+daW/7SkbdaZ7S8kkyKQzsY8hCh0B7RUkUBqkPCZ5+rbTyKCtFOyKyMYw+ebfLQ5QXg==
+ dependencies:
+ "@graphql-tools/url-loader" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
"@types/http-proxy-agent" "^2.0.2"
"@types/js-yaml" "^3.12.5"
"@types/json-stable-stringify" "^1.0.32"
"@types/jsonwebtoken" "^8.5.0"
- ajv "^6.12.3"
+ ajv "^6.12.5"
bluebird "^3.7.2"
chalk "^4.1.0"
- debug "^4.1.1"
+ debug "^4.2.0"
dotenv "^8.2.0"
fs-extra "9.0.1"
- graphql-request "^3.0.0"
+ graphql-request "^3.1.0"
http-proxy-agent "^4.0.1"
https-proxy-agent "^5.0.0"
- isomorphic-fetch "^2.2.1"
+ isomorphic-fetch "^3.0.0"
js-yaml "^3.14.0"
json-stable-stringify "^1.0.1"
jsonwebtoken "^8.5.1"
- lodash "^4.17.19"
+ lodash "^4.17.20"
replaceall "^0.1.6"
scuid "^1.1.0"
- tslib "~2.0.0"
+ tslib "~2.0.1"
yaml-ast-parser "^0.0.43"
"@graphql-tools/relay-operation-optimizer@^6.0.18":
@@ -2116,37 +2106,30 @@
"@graphql-tools/utils" "6.0.18"
tslib "~2.0.0"
-"@graphql-tools/url-loader@6.0.18", "@graphql-tools/url-loader@^6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.0.18.tgz#0eeb4e195bf9d8edda78ad16bd2f05b9869d581a"
- integrity sha512-rdKbLYyoiKiirFDIHN5/vpaAKB6Qkjc5j1cRbv7weQn0nIHHhj4ReMBvqSiISyierQ33ix5y4Pvo1VJPD+V4qQ==
+"@graphql-tools/schema@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-6.2.4.tgz#cc4e9f5cab0f4ec48500e666719d99fc5042481d"
+ integrity sha512-rh+14lSY1q8IPbEv2J9x8UBFJ5NrDX9W5asXEUlPp+7vraLp/Tiox4GXdgyA92JhwpYco3nTf5Bo2JDMt1KnAQ==
dependencies:
- "@graphql-tools/delegate" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
- "@graphql-tools/wrap" "6.0.18"
- "@types/websocket" "1.0.1"
- cross-fetch "3.0.5"
- subscriptions-transport-ws "0.9.17"
- tslib "~2.0.0"
- valid-url "1.0.9"
- websocket "1.0.31"
+ "@graphql-tools/utils" "^6.2.4"
+ tslib "~2.0.1"
-"@graphql-tools/url-loader@^6.0.0":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.0.15.tgz#3ad621ed75c03fbc0ea08d2560f379a053c4035f"
- integrity sha512-/iGuK7J9yCECYMYQJqKNWnz4ytPHppkxh4YS5Ud9QPDNl488e+eInyNbkdiWcFGyZ4KHqEnXSDdRFg3mFNrMnw==
+"@graphql-tools/url-loader@^6", "@graphql-tools/url-loader@^6.0.0", "@graphql-tools/url-loader@^6.2.4":
+ version "6.3.0"
+ resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.3.0.tgz#75b82bdf0983d3e389c75948a7abff20fa45a630"
+ integrity sha512-lX6A22Rhbqj8FHmkCVSDflolOGy7UtCJGtGbfRuv8/VqD94JfJLnGVFxC1jODURFdj+yrs/97Wm/ntRcpy7nDA==
dependencies:
- "@graphql-tools/delegate" "6.0.15"
- "@graphql-tools/utils" "6.0.15"
- "@graphql-tools/wrap" "6.0.15"
+ "@graphql-tools/delegate" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
+ "@graphql-tools/wrap" "^6.2.4"
"@types/websocket" "1.0.1"
- cross-fetch "3.0.5"
- subscriptions-transport-ws "0.9.17"
- tslib "~2.0.0"
+ cross-fetch "3.0.6"
+ subscriptions-transport-ws "0.9.18"
+ tslib "~2.0.1"
valid-url "1.0.9"
- websocket "1.0.31"
+ websocket "1.0.32"
-"@graphql-tools/utils@6.0.15", "@graphql-tools/utils@^6.0.0":
+"@graphql-tools/utils@6.0.15":
version "6.0.15"
resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.15.tgz#6d54d383285bea3c22797531933b62a408e78e49"
integrity sha512-VG5cMLPgh9RDLGHamGpXVnBrNw7bZGT46LrxK7IIqDZI9H0GPsRCo8+p+CfDkw0IlDiEECb624WVCpm9IYNecA==
@@ -2154,7 +2137,7 @@
"@ardatan/aggregate-error" "0.0.1"
camel-case "4.1.1"
-"@graphql-tools/utils@6.0.18", "@graphql-tools/utils@^6.0.18":
+"@graphql-tools/utils@6.0.18":
version "6.0.18"
resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.18.tgz#bba960f0ab327c8304089d41da0b7a3e00fe430f"
integrity sha512-8ntYuXJucBtjViOYljeKBzScfpVTnv7BfqIPU/WJ65h6nXD+qf8fMUR1C4MpCUeFvSjMiDSB5Z4enJmau/9D3A==
@@ -2162,28 +2145,25 @@
"@ardatan/aggregate-error" "0.0.1"
camel-case "4.1.1"
-"@graphql-tools/wrap@6.0.15":
- version "6.0.15"
- resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.15.tgz#1b4a43d406f3894e4cbc931f9d4e0d282ab0076e"
- integrity sha512-yWiDBrbzml6PRl4aeJBLNGPw385LFtszMfkfYwjLSWvNyVILDCMa/XWHThw4FMaZ1nPL0GuLggW2bVkUBi3TYA==
+"@graphql-tools/utils@^6", "@graphql-tools/utils@^6.0.0", "@graphql-tools/utils@^6.0.18", "@graphql-tools/utils@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.2.4.tgz#38a2314d2e5e229ad4f78cca44e1199e18d55856"
+ integrity sha512-ybgZ9EIJE3JMOtTrTd2VcIpTXtDrn2q6eiYkeYMKRVh3K41+LZa6YnR2zKERTXqTWqhobROwLt4BZbw2O3Aeeg==
dependencies:
- "@graphql-tools/delegate" "6.0.15"
- "@graphql-tools/schema" "6.0.15"
- "@graphql-tools/utils" "6.0.15"
- aggregate-error "3.0.1"
- tslib "~2.0.0"
+ "@ardatan/aggregate-error" "0.0.6"
+ camel-case "4.1.1"
+ tslib "~2.0.1"
-"@graphql-tools/wrap@6.0.18":
- version "6.0.18"
- resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.18.tgz#a9a9bd8e1dac469671274153f419ab35317164ac"
- integrity sha512-AHegxtawd+ivpUhI1gP4xQWWYPl5GvCmvzaas03DfrGlGcV/LyKJIzdZDEs2E4oCgwCU7F9UQMxgTsq+Dttn5Q==
+"@graphql-tools/wrap@^6.2.4":
+ version "6.2.4"
+ resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.2.4.tgz#2709817da6e469753735a9fe038c9e99736b2c57"
+ integrity sha512-cyQgpybolF9DjL2QNOvTS1WDCT/epgYoiA8/8b3nwv5xmMBQ6/6nYnZwityCZ7njb7MMyk7HBEDNNlP9qNJDcA==
dependencies:
- "@graphql-tools/delegate" "6.0.18"
- "@graphql-tools/schema" "6.0.18"
- "@graphql-tools/utils" "6.0.18"
- aggregate-error "3.0.1"
+ "@graphql-tools/delegate" "^6.2.4"
+ "@graphql-tools/schema" "^6.2.4"
+ "@graphql-tools/utils" "^6.2.4"
is-promise "4.0.0"
- tslib "~2.0.0"
+ tslib "~2.0.1"
"@hapi/address@^4.0.1":
version "4.1.0"
@@ -3245,6 +3225,17 @@
prop-types "^15.7.2"
react-is "^16.8.0"
+"@material-ui/lab@^4.0.0-alpha.56":
+ version "4.0.0-alpha.56"
+ resolved "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz#ff63080949b55b40625e056bbda05e130d216d34"
+ integrity sha512-xPlkK+z/6y/24ka4gVJgwPfoCF4RCh8dXb1BNE7MtF9bXEBLN/lBxNTK8VAa0qm3V2oinA6xtUIdcRh0aeRtVw==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/utils" "^4.10.2"
+ clsx "^1.0.4"
+ prop-types "^15.7.2"
+ react-is "^16.8.0"
+
"@material-ui/pickers@^3.2.2":
version "3.2.10"
resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae"
@@ -3630,19 +3621,18 @@
resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.3.0.tgz#a051eb4db2ad778e39933a31ba806f415dd3ba90"
integrity sha512-v/xZ4Xk18ZgBARcCe99IDqdO97GU0UC784gG8PhGGFDdy5Rbdy7ixTjHkGYttkr43PB7SX6ttohNhkKSW4nATQ==
-"@roadiehq/backstage-plugin-github-pull-requests@0.3.0":
- version "0.3.0"
- resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.3.0.tgz#465cb74b1dab6f34a5a5bf4fa57b6c1d312bc5b7"
- integrity sha512-nSyMh8p3SAYj1yj5/+Po82g7aZwF5pWb+fh7bLPdf+ywl5pa+aKQxAVNCvlvgfN2WZ9gDTMbCPyWDgCfeJ/V9g==
+"@roadiehq/backstage-plugin-github-pull-requests@^0.4.3":
+ version "0.4.3"
+ resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.4.3.tgz#720fd3c53ae8e61d8cfc727cda47e236af442b02"
+ integrity sha512-IJGMys5FJ512sitaMwWgACll3ZTuE3nZUX05MNXly/WXty6VBcfboiPbSB6IzLAEWMmvYlV1YUwwL7hAl/62GQ==
dependencies:
- "@backstage/catalog-model" "^0.1.1-alpha.16"
- "@backstage/core" "^0.1.1-alpha.16"
- "@backstage/core-api" "^0.1.1-alpha.16"
- "@backstage/plugin-catalog" "^0.1.1-alpha.16"
- "@backstage/theme" "^0.1.1-alpha.16"
- "@material-ui/core" "^4.9.1"
+ "@backstage/catalog-model" "^0.1.1-alpha.23"
+ "@backstage/core" "^0.1.1-alpha.23"
+ "@backstage/plugin-catalog" "^0.1.1-alpha.23"
+ "@backstage/theme" "^0.1.1-alpha.23"
+ "@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
- "@material-ui/lab" "4.0.0-alpha.45"
+ "@material-ui/lab" "^4.0.0-alpha.56"
"@octokit/rest" "^18.0.0"
"@octokit/types" "^5.0.1"
"@types/react-dom" "^16.9.8"
@@ -3650,7 +3640,7 @@
moment "^2.27.0"
react "^16.13.1"
react-dom "^16.13.1"
- react-router-dom "6.0.0-beta.0"
+ react-router "6.0.0-beta.0"
react-use "^15.3.3"
"@roadiehq/backstage-plugin-travis-ci@^0.2.3":
@@ -4530,6 +4520,14 @@
"@babel/runtime" "^7.5.4"
"@types/testing-library__react-hooks" "^3.3.0"
+"@testing-library/react-hooks@^3.4.2":
+ version "3.4.2"
+ resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.4.2.tgz#8deb94f7684e0d896edd84a4c90e5b79a0810bc2"
+ integrity sha512-RfPG0ckOzUIVeIqlOc1YztKgFW+ON8Y5xaSPbiBkfj9nMkkiLhLeBXT5icfPX65oJV/zCZu4z8EVnUc6GY9C5A==
+ dependencies:
+ "@babel/runtime" "^7.5.4"
+ "@types/testing-library__react-hooks" "^3.4.0"
+
"@testing-library/react@^10.4.1":
version "10.4.3"
resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.3.tgz#c6f356688cffc51f6b35385583d664bb11a161f4"
@@ -5121,6 +5119,13 @@
"@types/koa-compose" "*"
"@types/node" "*"
+"@types/ldapjs@^1.0.9":
+ version "1.0.9"
+ resolved "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.9.tgz#1224192d14cc5ab5218fcea72ebb04489c52cb95"
+ integrity sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA==
+ dependencies:
+ "@types/node" "*"
+
"@types/lodash@^4.14.151":
version "4.14.161"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18"
@@ -5331,6 +5336,11 @@
resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a"
integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==
+"@types/raf@^3.4.0":
+ version "3.4.0"
+ resolved "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
+ integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
+
"@types/range-parser@*":
version "1.2.3"
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
@@ -5585,9 +5595,9 @@
"@types/node" "*"
"@types/supertest@^2.0.8":
- version "2.0.9"
- resolved "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.9.tgz#049bddbcb0ee0d60a9b836ccc977d813a1c32325"
- integrity sha512-0BTpWWWAO1+uXaP/oA0KW1eOZv4hc0knhrWowV06Gwwz3kqQxNO98fUFM2e15T+PdPRmOouNFrYvaBgdojPJ3g==
+ version "2.0.10"
+ resolved "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7"
+ integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ==
dependencies:
"@types/superagent" "*"
@@ -5640,6 +5650,13 @@
dependencies:
"@types/react-test-renderer" "*"
+"@types/testing-library__react-hooks@^3.4.0":
+ version "3.4.1"
+ resolved "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433"
+ integrity sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q==
+ dependencies:
+ "@types/react-test-renderer" "*"
+
"@types/through@*":
version "0.0.30"
resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895"
@@ -5669,7 +5686,7 @@
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==
-"@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.10.0":
+"@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.11.0":
version "3.11.0"
resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#bcc3b85e7dc6ac2db25330610513f2228c2fcfb2"
integrity sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA==
@@ -6007,11 +6024,6 @@ JSONStream@^1.0.4, JSONStream@^1.3.4:
jsonparse "^1.2.0"
through ">=2.2.7 <3"
-abab@^1.0.0:
- version "1.0.4"
- resolved "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
- integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=
-
abab@^2.0.0, abab@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
@@ -6022,6 +6034,11 @@ abbrev@1:
resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+abstract-logging@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.0.tgz#08a85814946c98ef06f4256ad470aba1886d4490"
+ integrity sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA==
+
accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -6030,13 +6047,6 @@ accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
-acorn-globals@^1.0.4:
- version "1.0.9"
- resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf"
- integrity sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=
- dependencies:
- acorn "^2.1.0"
-
acorn-globals@^4.1.0:
version "4.3.4"
resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
@@ -6068,11 +6078,6 @@ acorn-walk@^7.1.1:
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
-acorn@^2.1.0, acorn@^2.4.0:
- version "2.7.0"
- resolved "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
- integrity sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=
-
acorn@^5.5.3:
version "5.7.4"
resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
@@ -6182,7 +6187,7 @@ ajv@^5.0.0:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
-ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.7.0:
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.7.0:
version "6.12.4"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
@@ -6192,6 +6197,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ajv@^6.12.5:
+ version "6.12.5"
+ resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
+ integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -6706,7 +6721,7 @@ asn1.js@^4.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
-asn1@~0.2.0, asn1@~0.2.3:
+asn1@^0.2.4, asn1@~0.2.0, asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
@@ -6845,7 +6860,7 @@ aws4@^1.8.0:
resolved "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
-axios@^0.19.0, axios@^0.19.2:
+axios@^0.19.2:
version "0.19.2"
resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
@@ -7259,6 +7274,13 @@ backo2@^1.0.2:
resolved "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+backoff@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f"
+ integrity sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=
+ dependencies:
+ precond "0.2"
+
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -7274,10 +7296,10 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
-base64-arraybuffer@^0.1.5:
- version "0.1.5"
- resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
- integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+base64-arraybuffer@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+ integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
base64-js@^1.0.2, base64-js@^1.2.0:
version "1.3.1"
@@ -7391,9 +7413,9 @@ bl@^1.0.0:
safe-buffer "^5.1.1"
bl@^4.0.1:
- version "4.0.2"
- resolved "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a"
- integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
+ integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
@@ -7702,6 +7724,13 @@ buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.0.2"
ieee754 "^1.1.4"
+bufferutil@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
+ integrity sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
+ dependencies:
+ node-gyp-build "~3.7.0"
+
builtin-modules@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
@@ -7959,15 +7988,17 @@ canvas@^2.6.1:
node-pre-gyp "^0.11.0"
simple-get "^3.0.3"
-canvg@1.5.3:
- version "1.5.3"
- resolved "https://registry.npmjs.org/canvg/-/canvg-1.5.3.tgz#aad17915f33368bf8eb80b25d129e3ae922ddc5f"
- integrity sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg==
+canvg@^3.0.6:
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.6.tgz#4f82a34acc433daa06c494fc255420cbbb05f903"
+ integrity sha512-eFUy8R/4DgocR93LF8lr+YUxW4PYblUe/Q1gz2osk/cI5n8AsYdassvln0D9QPhLXQ6Lx7l8hwtT8FLvOn2Ihg==
dependencies:
- jsdom "^8.1.0"
+ "@babel/runtime" "^7.6.3"
+ "@types/raf" "^3.4.0"
+ core-js "3"
+ raf "^3.4.1"
rgbcolor "^1.0.1"
- stackblur-canvas "^1.4.1"
- xmldom "^0.1.22"
+ stackblur-canvas "^2.0.0"
capture-exit@^2.0.0:
version "2.0.0"
@@ -8122,11 +8153,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
safe-buffer "^5.0.1"
circleci-api@^4.0.0:
- version "4.0.0"
- resolved "https://registry.npmjs.org/circleci-api/-/circleci-api-4.0.0.tgz#d773fe68f4a59e1968881269883a23b0805b3546"
- integrity sha512-D/THFyhOv6THSkYXJhrOLIOmV7fmyDqgs1+pBFMAqDR+ywXszxa2Dqx1Zw+YD3O2zD2y5LQOifCDT96VidRG7Q==
+ version "4.1.3"
+ resolved "https://registry.npmjs.org/circleci-api/-/circleci-api-4.1.3.tgz#cafd80bc1905c6064724636741b4b5b231ed34d0"
+ integrity sha512-Km6H8LB3nNbFUjErhI+sNQrcHcU7dCBnQEi4ckBtQMkCXh+2gDvaYchfkeuIaA1hkakQT5sLmdPbYzhKbvsOMg==
dependencies:
- axios "^0.19.0"
+ axios "^0.20.0"
class-utils@^0.3.5:
version "0.3.6"
@@ -8273,6 +8304,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
+cliui@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3"
+ integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
clone-deep@^0.2.4:
version "0.2.4"
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6"
@@ -8836,16 +8876,16 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
+core-js@3, core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.0, core-js@^3.6.5:
+ version "3.6.5"
+ resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
+ integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
+
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.11, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
-core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.5:
- version "3.6.5"
- resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
- integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
-
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -8937,12 +8977,12 @@ cross-env@^7.0.0:
dependencies:
cross-spawn "^7.0.1"
-cross-fetch@3.0.5, cross-fetch@^3.0.4, cross-fetch@^3.0.5:
- version "3.0.5"
- resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c"
- integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew==
+cross-fetch@3.0.6, cross-fetch@^3.0.4, cross-fetch@^3.0.5:
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c"
+ integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==
dependencies:
- node-fetch "2.6.0"
+ node-fetch "2.6.1"
cross-spawn@7.0.1:
version "7.0.1"
@@ -9032,12 +9072,12 @@ css-in-js-utils@^2.0.0:
hyphenate-style-name "^1.0.2"
isobject "^3.0.1"
-css-line-break@1.0.1:
- version "1.0.1"
- resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz#19f2063a33e95fb2831b86446c0b80c188af450a"
- integrity sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=
+css-line-break@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+ integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
dependencies:
- base64-arraybuffer "^0.1.5"
+ base64-arraybuffer "^0.2.0"
css-loader@^3.5.3:
version "3.6.0"
@@ -9238,7 +9278,7 @@ csso@^4.0.2:
dependencies:
css-tree "1.0.0-alpha.37"
-cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6:
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6:
version "0.3.8"
resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
@@ -9248,13 +9288,6 @@ cssom@^0.4.4:
resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
-"cssstyle@>= 0.2.34 < 0.3.0":
- version "0.2.37"
- resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
- integrity sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=
- dependencies:
- cssom "0.3.x"
-
cssstyle@^1.0.0:
version "1.4.0"
resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
@@ -9491,6 +9524,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+dataloader@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f"
+ integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==
+
date-fns@^1.27.2:
version "1.30.1"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
@@ -9544,6 +9582,13 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
dependencies:
ms "^2.1.1"
+debug@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
+ integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
+ dependencies:
+ ms "2.1.2"
+
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@@ -10050,6 +10095,11 @@ dompurify@^1.0.11:
resolved "https://registry.npmjs.org/dompurify/-/dompurify-1.0.11.tgz#fe0f4a40d147f7cebbe31a50a1357539cfc1eb4d"
integrity sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ==
+dompurify@^2.0.12:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe"
+ integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg==
+
dompurify@^2.0.7:
version "2.0.12"
resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.0.12.tgz#284a2b041e1c60b8e72d7b4d2fadad36141254ae"
@@ -10485,6 +10535,11 @@ escalade@^3.0.1:
resolved "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
+escalade@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
+ integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
+
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
@@ -10505,7 +10560,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-escodegen@^1.14.1, escodegen@^1.6.1, escodegen@^1.9.1:
+escodegen@^1.14.1, escodegen@^1.9.1:
version "1.14.3"
resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
@@ -11074,6 +11129,11 @@ extract-files@^8.0.0:
resolved "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288"
integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ==
+extract-files@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
+ integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
+
extract-zip@^1.7.0:
version "1.7.0"
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
@@ -11295,10 +11355,6 @@ file-loader@^6.0.0:
loader-utils "^2.0.0"
schema-utils "^2.7.1"
-file-saver@eligrey/FileSaver.js#1.3.8:
- version "1.3.8"
- resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e"
-
file-system-cache@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
@@ -11800,7 +11856,7 @@ get-caller-file@^1.0.1:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
-get-caller-file@^2.0.1:
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -12337,12 +12393,14 @@ graphql-language-service@^3.0.0:
graphql-language-service-interface "^2.4.0"
graphql-language-service-types "^1.6.0"
-graphql-request@^3.0.0:
- version "3.0.0"
- resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-3.0.0.tgz#942d81bbd621cc2223d35fbf2b42edf8be822b83"
- integrity sha512-zW8AuLnKMYOnpVKdANU9FzLDoj4u4AoU6KZ79e+BcJaNiuw/vgCJ0p7ppDMSDrW77a12Moa7J7Mg4w0f9Kd/Kg==
+graphql-request@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-3.1.0.tgz#c487488a1aa7b9a0f02335026b4ec897d645f9d4"
+ integrity sha512-Flg2Bd4Ek9BDJ5qacZC/iYuiS3LroHxQTmlUnfqjo/6jKwowY25FVtoLTnssMCBrYspRYEYEIfF1GN8J3/o5JQ==
dependencies:
- cross-fetch "^3.0.4"
+ cross-fetch "^3.0.5"
+ extract-files "^9.0.0"
+ form-data "^3.0.0"
graphql-subscriptions@^1.0.0:
version "1.1.0"
@@ -12741,12 +12799,12 @@ html-webpack-plugin@^4.2.1, html-webpack-plugin@^4.3.0:
tapable "^1.1.3"
util.promisify "1.0.0"
-html2canvas@1.0.0-alpha.12:
- version "1.0.0-alpha.12"
- resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz#3b1992e3c9b3f56063c35fd620494f37eba88513"
- integrity sha1-OxmS48mz9WBjw1/WIElPN+uohRM=
+html2canvas@^1.0.0-rc.5:
+ version "1.0.0-rc.7"
+ resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+ integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
dependencies:
- css-line-break "1.0.1"
+ css-line-break "1.1.1"
htmlparser2@^3.3.0:
version "3.10.1"
@@ -12955,7 +13013,7 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3:
resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
-iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -13023,6 +13081,11 @@ immer@1.10.0:
resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+immer@^7.0.9:
+ version "7.0.9"
+ resolved "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e"
+ integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==
+
immutable@>=3.8.2, immutable@^3.8.1, immutable@^3.8.2, immutable@^3.x.x:
version "3.8.2"
resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
@@ -13841,7 +13904,7 @@ isobject@^4.0.0:
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
-isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
+isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
@@ -13849,6 +13912,14 @@ isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
+isomorphic-fetch@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
+ integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
+ dependencies:
+ node-fetch "^2.6.1"
+ whatwg-fetch "^3.4.1"
+
isomorphic-form-data@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz#9f6adf1c4c61ae3aefd8f110ab60fb9b143d6cec"
@@ -14476,29 +14547,6 @@ jsdom@^16.2.2:
ws "^7.2.3"
xml-name-validator "^3.0.0"
-jsdom@^8.1.0:
- version "8.5.0"
- resolved "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz#d4d8f5dbf2768635b62a62823b947cf7071ebc98"
- integrity sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=
- dependencies:
- abab "^1.0.0"
- acorn "^2.4.0"
- acorn-globals "^1.0.4"
- array-equal "^1.0.0"
- cssom ">= 0.3.0 < 0.4.0"
- cssstyle ">= 0.2.34 < 0.3.0"
- escodegen "^1.6.1"
- iconv-lite "^0.4.13"
- nwmatcher ">= 1.3.7 < 2.0.0"
- parse5 "^1.5.1"
- request "^2.55.0"
- sax "^1.1.4"
- symbol-tree ">= 3.1.0 < 4.0.0"
- tough-cookie "^2.2.0"
- webidl-conversions "^3.0.1"
- whatwg-url "^2.0.1"
- xml-name-validator ">= 2.0.1 < 3.0.0"
-
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -14697,22 +14745,23 @@ jsonwebtoken@^8.5.1:
ms "^2.1.1"
semver "^5.6.0"
-jspdf-autotable@3.5.3:
- version "3.5.3"
- resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.3.tgz#2f73adb07f340e7dbf22950e3e6c8bf853991479"
- integrity sha512-K+cNWW3x6w0R/1B5m6PYOm6v8CTTDXy/g32lZouc7SuC6zhvzMN2dauhk6dDYxPD0pky0oyPIJFwSJ/tV8PAeg==
+jspdf-autotable@3.5.9:
+ version "3.5.9"
+ resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz#8a625ef2aead44271da95e9f649843c401536925"
+ integrity sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ==
-jspdf@1.5.3:
- version "1.5.3"
- resolved "https://registry.npmjs.org/jspdf/-/jspdf-1.5.3.tgz#5a12c011479defabef5735de55c913060ed219f2"
- integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw==
+jspdf@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz#2322f8644bc41845b3abe20db4c3ca0adeadb84c"
+ integrity sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ==
dependencies:
- canvg "1.5.3"
- file-saver eligrey/FileSaver.js#1.3.8
- html2canvas "1.0.0-alpha.12"
- omggif "1.0.7"
- promise-polyfill "8.1.0"
- stackblur-canvas "2.2.0"
+ atob "^2.1.2"
+ btoa "^1.2.1"
+ optionalDependencies:
+ canvg "^3.0.6"
+ core-js "^3.6.0"
+ dompurify "^2.0.12"
+ html2canvas "^1.0.0-rc.5"
jsprim@^1.2.2:
version "1.4.1"
@@ -14913,7 +14962,7 @@ kuler@1.0.x:
dependencies:
colornames "^1.1.1"
-latest-version@^5.0.0:
+latest-version@5.1.0, latest-version@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
@@ -14953,6 +15002,27 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
+ldap-filter@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz#2b14c68a2a9d4104dbdbc910a1ca85fd189e9797"
+ integrity sha1-KxTGiiqdQQTb28kQocqF/Riel5c=
+ dependencies:
+ assert-plus "^1.0.0"
+
+ldapjs@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/ldapjs/-/ldapjs-2.2.0.tgz#c23846d35bf215b4ba4721ff22f11d3df6391e2f"
+ integrity sha512-9+ekbj97nxRYQMRgEm/HYFhFLWSRKah2PnReUfhfM5f62XBeUSFolB+AQ2Jij5tqowpksTnrdNCZLP6C+V3uag==
+ dependencies:
+ abstract-logging "^2.0.0"
+ asn1 "^0.2.4"
+ assert-plus "^1.0.0"
+ backoff "^2.5.0"
+ ldap-filter "^0.3.3"
+ once "^1.4.0"
+ vasync "^2.2.0"
+ verror "^1.8.1"
+
left-pad@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
@@ -15374,16 +15444,11 @@ lodash@4.17.15:
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
-lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.4:
+lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.20, lodash@~4.17.4:
version "4.17.20"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-lodash@~4.17.15:
- version "4.17.19"
- resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
- integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
-
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -15684,10 +15749,10 @@ material-colors@^1.2.1:
resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
-material-table@1.68.0:
- version "1.68.0"
- resolved "https://registry.npmjs.org/material-table/-/material-table-1.68.0.tgz#275c3d9a885c40ae4bc5a7461c00e877f92397b9"
- integrity sha512-dyJJaVsS3m+i6sn71AvYcVdA1P9X1XiUOM2PekfvEeeMtkdQb66oChGkk77ndYi3Ja6j4DovGVNrgeVLwXLZiw==
+material-table@^1.69.1:
+ version "1.69.1"
+ resolved "https://registry.npmjs.org/material-table/-/material-table-1.69.1.tgz#8d1c8b23207f18bd3328cae1b5775ede284682e6"
+ integrity sha512-7MA8kMtr8ToPE6gNUbOGIb4g+RGOLWK8s9gXZYNwFtg6fGAjWEJ+iqBrMmdq7fkMmTRcyOd7/sC/5OPPY8CNGg==
dependencies:
"@date-io/date-fns" "^1.1.0"
"@material-ui/pickers" "^3.2.2"
@@ -15696,8 +15761,8 @@ material-table@1.68.0:
debounce "^1.2.0"
fast-deep-equal "2.0.1"
filefy "0.1.10"
- jspdf "1.5.3"
- jspdf-autotable "3.5.3"
+ jspdf "2.1.0"
+ jspdf-autotable "3.5.9"
prop-types "^15.6.2"
react-beautiful-dnd "^13.0.0"
react-double-scrollbar "0.0.15"
@@ -16208,7 +16273,7 @@ ms@2.1.1:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-ms@^2.0.0, ms@^2.1.1:
+ms@2.1.2, ms@^2.0.0, ms@^2.1.1:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@@ -16381,10 +16446,10 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
-node-fetch@2.6.0:
- version "2.6.0"
- resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
- integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+ integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^1.0.1:
version "1.7.3"
@@ -16394,20 +16459,15 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
-node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
- version "2.6.1"
- resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
- integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+node-forge@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
+ integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
-node-forge@0.9.0:
- version "0.9.0"
- resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
- integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
-
-node-forge@^0.7.0:
- version "0.7.6"
- resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
- integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
+node-gyp-build@~3.7.0:
+ version "3.7.0"
+ resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
+ integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
node-gyp@3.x:
version "3.8.0"
@@ -16763,11 +16823,6 @@ number-is-nan@^1.0.0:
resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
-"nwmatcher@>= 1.3.7 < 2.0.0":
- version "1.4.4"
- resolved "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
- integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==
-
nwsapi@^2.0.7, nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
@@ -16927,11 +16982,6 @@ oidc-token-hash@^5.0.0:
resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888"
integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg==
-omggif@1.0.7:
- version "1.0.7"
- resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
- integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0=
-
on-finished@^2.3.0, on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -17467,11 +17517,6 @@ parse5@5.1.1:
resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
-parse5@^1.5.1:
- version "1.5.1"
- resolved "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
- integrity sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=
-
parseurl@^1.3.2, parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -18395,6 +18440,11 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
+precond@0.2:
+ version "0.2.3"
+ resolved "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
+ integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=
+
preferred-pm@^3.0.0:
version "3.0.2"
resolved "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.2.tgz#bbdbef1014e34a7490349bf70d6d244b8d57a5e1"
@@ -18519,11 +18569,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-promise-polyfill@8.1.0:
- version "8.1.0"
- resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.0.tgz#30059da54d1358ce905ac581f287e184aedf995d"
- integrity sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA==
-
promise-polyfill@^8.1.3:
version "8.1.3"
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
@@ -20001,7 +20046,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8:
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
-request@^2.55.0, request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
+request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
version "2.88.2"
resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -20399,7 +20444,7 @@ sanitize-html@^1.27.0:
srcset "^2.0.1"
xtend "^4.0.1"
-sax@>=0.6.0, sax@^1.1.4, sax@^1.2.4, sax@~1.2.4:
+sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -20458,11 +20503,11 @@ select@^1.1.2:
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
selfsigned@^1.10.7:
- version "1.10.7"
- resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
- integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
+ version "1.10.8"
+ resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
+ integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==
dependencies:
- node-forge "0.9.0"
+ node-forge "^0.10.0"
semver-compare@^1.0.0:
version "1.0.0"
@@ -21165,15 +21210,10 @@ stack-utils@^2.0.2:
dependencies:
escape-string-regexp "^2.0.0"
-stackblur-canvas@2.2.0:
- version "2.2.0"
- resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.2.0.tgz#cacc5924a0744b3e183eb2e6c1d8559c1a17c26e"
- integrity sha512-5Gf8dtlf8k6NbLzuly2NkGrkS/Ahh+I5VUjO7TnFizdJtgpfpLLEdQlLe9umbcnZlitU84kfYjXE67xlSXfhfQ==
-
-stackblur-canvas@^1.4.1:
- version "1.4.1"
- resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz#849aa6f94b272ff26f6471fa4130ed1f7e47955b"
- integrity sha1-hJqm+UsnL/JvZHH6QTDtH35HlVs=
+stackblur-canvas@^2.0.0:
+ version "2.4.0"
+ resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz#2b2eba910cb46f6feae918e1c402f863d602c01b"
+ integrity sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==
stackframe@^1.1.1:
version "1.1.1"
@@ -21598,7 +21638,18 @@ stylis@3.5.0:
resolved "https://registry.npmjs.org/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
-subscriptions-transport-ws@0.9.17, subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16:
+subscriptions-transport-ws@0.9.18:
+ version "0.9.18"
+ resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz#bcf02320c911fbadb054f7f928e51c6041a37b97"
+ integrity sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA==
+ dependencies:
+ backo2 "^1.0.2"
+ eventemitter3 "^3.1.0"
+ iterall "^1.2.1"
+ symbol-observable "^1.0.4"
+ ws "^5.2.0"
+
+subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16:
version "0.9.17"
resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz#e30e40f0caae0d2781903c01a8cb51b6e2682098"
integrity sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==
@@ -21784,7 +21835,7 @@ symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
-"symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.2, symbol-tree@^3.2.4:
+symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
@@ -22264,7 +22315,7 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
-tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -22295,11 +22346,6 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
-tr46@~0.0.3:
- version "0.0.3"
- resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
- integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-
traverse@~0.6.6:
version "0.6.6"
resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
@@ -22404,10 +22450,10 @@ ts-loader@^7.0.4:
micromatch "^4.0.0"
semver "^6.0.0"
-ts-log@^2.1.4:
- version "2.1.4"
- resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.1.4.tgz#063c5ad1cbab5d49d258d18015963489fb6fb59a"
- integrity sha512-P1EJSoyV+N3bR/IWFeAqXzKPZwHpnLY6j7j58mAvewHRipo+BQM2Y1f9Y9BjEQznKwgqqZm7H8iuixmssU7tYQ==
+ts-log@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb"
+ integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==
ts-node@^8.10.2, ts-node@^8.6.2:
version "8.10.2"
@@ -22445,7 +22491,7 @@ tslib@2.0.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3"
integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==
-tslib@2.0.1, tslib@^2.0.0, tslib@~2.0.0:
+tslib@2.0.1, tslib@^2.0.0, tslib@~2.0.0, tslib@~2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
@@ -22937,6 +22983,13 @@ use@^3.1.0:
resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+utf-8-validate@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz#63cfbccd85dc1f2b66cf7a1d0eebc08ed056bfb3"
+ integrity sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
+ dependencies:
+ node-gyp-build "~3.7.0"
+
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -23087,12 +23140,19 @@ vary@^1, vary@~1.1.2:
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+vasync@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/vasync/-/vasync-2.2.0.tgz#cfde751860a15822db3b132bc59b116a4adaf01b"
+ integrity sha1-z951GGChWCLbOxMrxZsRakra8Bs=
+ dependencies:
+ verror "1.10.0"
+
vendors@^1.0.0:
version "1.0.4"
resolved "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
-verror@1.10.0:
+verror@1.10.0, verror@^1.8.1:
version "1.10.0"
resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
@@ -23133,10 +23193,10 @@ vscode-languageserver-types@^3.15.1:
resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de"
integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==
-vue-template-compiler@^2.6.11:
- version "2.6.11"
- resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
- integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
+vue-template-compiler@^2.6.12:
+ version "2.6.12"
+ resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e"
+ integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -23219,11 +23279,6 @@ webapi-parser@^0.5.0:
dependencies:
ajv "6.5.2"
-webidl-conversions@^3.0.0, webidl-conversions@^3.0.1:
- version "3.0.1"
- resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
- integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
-
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -23250,7 +23305,7 @@ webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2:
range-parser "^1.2.1"
webpack-log "^2.0.0"
-webpack-dev-server@^3.10.3:
+webpack-dev-server@^3.11.0:
version "3.11.0"
resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c"
integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==
@@ -23387,15 +23442,16 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
-websocket@1.0.31:
- version "1.0.31"
- resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz#e5d0f16c3340ed87670e489ecae6144c79358730"
- integrity sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==
+websocket@1.0.32:
+ version "1.0.32"
+ resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz#1f16ddab3a21a2d929dec1687ab21cfdc6d3dbb1"
+ integrity sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q==
dependencies:
+ bufferutil "^4.0.1"
debug "^2.2.0"
es5-ext "^0.10.50"
- nan "^2.14.0"
typedarray-to-buffer "^3.1.5"
+ utf-8-validate "^5.0.2"
yaeti "^0.0.6"
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
@@ -23415,7 +23471,7 @@ whatwg-fetch@^2.0.4:
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
-whatwg-fetch@^3.0.0, whatwg-fetch@^3.4.0:
+whatwg-fetch@^3.0.0, whatwg-fetch@^3.4.0, whatwg-fetch@^3.4.1:
version "3.4.1"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3"
integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==
@@ -23425,14 +23481,6 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
-whatwg-url@^2.0.1:
- version "2.0.1"
- resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz#5396b2043f020ee6f704d9c45ea8519e724de659"
- integrity sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=
- dependencies:
- tr46 "~0.0.3"
- webidl-conversions "^3.0.0"
-
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
@@ -23738,20 +23786,15 @@ xml-crypto@^1.4.0:
xpath "0.0.27"
xml-encryption@^1.0.0:
- version "1.2.0"
- resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364"
- integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew==
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.1.tgz#e6d18817c4309fd07ca7793cca93c3fd06745baa"
+ integrity sha512-hn5w3l5p2+nGjlmM0CAhMChDzVGhW+M37jH35Z+GJIipXbn9PUlAIRZ6I5Wm7ynlqZjFrMAr83d/CIp9VZJMTA==
dependencies:
escape-html "^1.0.3"
- node-forge "^0.7.0"
+ node-forge "^0.10.0"
xmldom "~0.1.15"
xpath "0.0.27"
-"xml-name-validator@>= 2.0.1 < 3.0.0":
- version "2.0.1"
- resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
- integrity sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=
-
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@@ -23780,7 +23823,7 @@ xmldom@0.1.27:
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
-xmldom@0.1.x, xmldom@^0.1.22, xmldom@~0.1.15:
+xmldom@0.1.x, xmldom@~0.1.15:
version "0.1.31"
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
@@ -23820,6 +23863,11 @@ y18n@^4.0.0:
resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+y18n@^5.0.1:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.2.tgz#48218df5da2731b4403115c39a1af709c873f829"
+ integrity sha512-CkwaeZw6dQgqgPGeTWKMXCRmMcBgETFlTml1+ZOO+q7kGst8NREJ+eWwFNPVUQ4QGdAaklbqCZHH6Zuep1RjiA==
+
yaeti@^0.0.6:
version "0.0.6"
resolved "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
@@ -23881,6 +23929,11 @@ yargs-parser@^15.0.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
+yargs-parser@^20.0.0:
+ version "20.2.1"
+ resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77"
+ integrity sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA==
+
yargs-parser@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f"
@@ -23939,6 +23992,19 @@ yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
+yargs@^16.0.3:
+ version "16.0.3"
+ resolved "https://registry.npmjs.org/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c"
+ integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA==
+ dependencies:
+ cliui "^7.0.0"
+ escalade "^3.0.2"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.1"
+ yargs-parser "^20.0.0"
+
yargs@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e"