Merge branch 'master' into add-global-pg-plugin-schema-flag

This commit is contained in:
Greg Bomkamp
2021-11-12 09:05:15 -05:00
committed by GitHub
630 changed files with 18071 additions and 4555 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Add theme switcher to sidebar of dev app.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
---
The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-sentry': patch
---
feature: sentry-plugin allows passing search query for listing issues
-9
View File
@@ -1,9 +0,0 @@
---
'@backstage/test-utils': patch
'@backstage/test-utils-core': patch
---
Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package.
This should have no impact since this package is considered internal and have no usages outside core packages.
Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Reader will now scroll to the top of the page when navigating between pages
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Update to the right version of @backstage/errors
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/plugin-gcp-projects': patch
---
UI updates to GCP-projects plugin
Adds the following to the project list page:
- pagination
- filtering
- sorting
- rows per page
- show/hide columns
Makes breadcrumb a link back to project list for the project details and new project views.
In project list page, updates New project button to use RouterLink instead of `href` to avoid login prompt.
In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
remove double config dep
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
---
Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Migrated to using `@backstage/app-defaults`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/cli-common': patch
---
Add docs to the last export of cli-common
-10
View File
@@ -1,10 +0,0 @@
---
'@backstage/plugin-azure-devops': patch
---
Azure DevOps frontend refactoring items from issue #7641
- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions
- Improved documentation to be easier to expand with new features in the future
- Removed Router based on feedback from maintainers
- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder': patch
---
Scaffolder: Enable back Template action buttons if template fails to execute
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added the `isDatabaseConflictError` function.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/create-app': patch
---
Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future.
To migrate an existing application, add the latest version of `@backstage/app-defaults` as a dependency in `packages/app/package.json`, and make the following change to `packages/app/src/App.tsx`:
```diff
-import { createApp, FlatRoutes } from '@backstage/core-app-api';
+import { createApp } from '@backstage/app-defaults';
+import { FlatRoutes } from '@backstage/core-app-api';
```
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-azure-devops-backend': patch
---
Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': minor
---
Tweaked style insertion logic to make sure that JSS stylesheets always receive the highest priority.
-26
View File
@@ -1,26 +0,0 @@
---
'@backstage/backend-common': patch
'@backstage/catalog-model': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/core-app-api': patch
'@backstage/errors': patch
'@backstage/search-common': patch
'@backstage/test-utils': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-backend-module-ldap': patch
'@backstage/plugin-catalog-graphql': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend-module-rails': patch
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
'@backstage/plugin-scaffolder-common': patch
'@backstage/plugin-search': patch
'@backstage/plugin-shortcuts': patch
---
Switch to use the json and observable types from `@backstage/types`
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/techdocs-common': patch
---
Support optional bucketRootPath configuration parameter in S3 and GCS publishers
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/backend-common': patch
---
Make sure that server builder `start()` propagates errors (such as failing to bind to the required port) properly and doesn't resolve the promise prematurely.
After this change, the backend logger will be able to actually capture the error as it happens:
```
2021-11-11T10:54:21.334Z backstage info Initializing http server
2021-11-11T10:54:21.335Z backstage error listen EADDRINUSE: address already in use :::7000 code=EADDRINUSE errno=-48 syscall=listen address=:: port=7000
```
-8
View File
@@ -1,8 +0,0 @@
---
'@backstage/config': patch
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
'@backstage/plugin-scaffolder-backend': patch
---
Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/config': patch
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
---
Replace usage of test-utils-core with test-utils
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-search-backend-node': patch
---
Handle special case when filter array has single value optimizing Lunr search behaviour.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': minor
---
Removed the unused `UserFlags` type.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
Deprecated the `theme` property on `AppTheme`, replacing it with `Provider`. See https://backstage.io/docs/api/deprecations#app-theme for more details.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': patch
---
Migrated to using `createSpecializedApp`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-sentry': patch
---
fix: sentry-plugin correction for allowed period values
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-app-api': patch
---
Api cleanup, adding `@public` where necessary and tweaking some comments
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/core-app-api': patch
---
The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement.
The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`.
The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or MUI, as it allows you to avoid those dependencies completely.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-components': patch
---
Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey`
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/errors': patch
---
Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}`
+3
View File
@@ -52,6 +52,7 @@ configmaps
configs
const
cookiecutter
cron
css
Datadog
dataflow
@@ -162,6 +163,8 @@ Monorepo
monorepos
msgraph
msw
mutex
mutexes
mysql
namespace
namespaced
+3 -1
View File
@@ -21,7 +21,7 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
NODE_OPTIONS: --max-old-space-size=8192
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
@@ -40,6 +40,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.0.2
- name: setup chrome
uses: browser-actions/setup-chrome@latest
- name: yarn install
run: yarn install --frozen-lockfile
+2 -5
View File
@@ -53,6 +53,8 @@ jobs:
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: setup chrome
uses: browser-actions/setup-chrome@latest
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
@@ -72,8 +74,3 @@ jobs:
run: |
sudo sysctl fs.inotify.max_user_watches=524288
yarn e2e-test run
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -0,0 +1,61 @@
name: 'Snyk Github Issue Sync'
on:
schedule:
- cron: '0 */4 * * *'
jobs:
sync:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: Create Snyk report
uses: snyk/actions/node@master
continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found
with:
args: >
--yarn-workspaces
--org=backstage-dgh
--strict-out-of-sync=false
--json-file-output=snyk.json
json: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Update Github issues
run: yarn ts-node scripts/snyk-github-issue-sync.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+67 -61
View File
@@ -1,61 +1,67 @@
| Organization | Contact | Description of Use |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit |
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. |
| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. |
| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. |
| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. |
| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. |
| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 |
| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes |
| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). |
| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. |
| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. |
| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. |
| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. |
| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. |
| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. |
| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. |
| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration |
| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. |
| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar |
| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. |
| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. |
| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. |
| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. |
| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc |
| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. |
| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. |
| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. |
| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. |
| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. |
| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. |
| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. |
| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. |
| Organization | Contact | Description of Use |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling |
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit |
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. |
| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. |
| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. |
| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. |
| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. |
| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 |
| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes |
| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). |
| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. |
| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. |
| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. |
| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. |
| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. |
| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. |
| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. |
| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration |
| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. |
| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar |
| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. |
| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. |
| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. |
| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. |
| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc |
| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. |
| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. |
| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. |
| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. |
| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. |
| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. |
| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. |
| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. |
| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. |
| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. |
| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. |
| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. |
| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling |
| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem
+56
View File
@@ -0,0 +1,56 @@
---
id: deprecations
title: Deprecations
description: A list of active and past deprecations
---
## Introduction
This page contains extended documentation for some of the deprecations in
various parts of Backstage. It is not an exhaustive list as most deprecation
only come in the form of a changelog notice and a console warning. The
deprecations listed here are the ones that need a bit more guidance than what
fits in a console message.
### App Theme
`Released 2021-11-12 in @backstage/core-plugin-api v0.1.13`
In order to provide more flexibility in what types of themes can be used and how
they are applied, the `theme` property on the `AppTheme` type is being
deprecated and replaced by a `Provider` property instead. The `Provider`
property is a React component that will be mounted at the root of the app
whenever that theme is active. This also removes the tight connection to MUI and
opens up for other type of themes, and removes the hardcoded usage of
`<CssBaseline>`.
To migrate an existing theme, remove the `theme` property and move it over to a
new `Provider` component, using `ThemeProvider` from MUI to provide the new
theme, along with `<CssBaseline>`. For example a theme that currently looks like
this:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
};
```
Would be migrated to the following:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
};
```
+10 -8
View File
@@ -67,13 +67,15 @@ production build.
## Configuration Files
It is possible to have multiple configuration files, both to support different
environments, but also to define configuration that is local to specific
packages. The configuration files to load are selected using a `--config <path>`
flag, and it is possible to load any number of files. Paths are relative to the
working directory of the executed process, for example `package/backend`. This
means that to select a config file in the repo root when running the backend,
you would use `--config ../../my-config.yaml`.
It is possible to have multiple configuration files (bundled and/or remote),
both to support different environments, but also to define configuration that is
local to specific packages. The configuration files to load are selected using a
`--config <local-path|url>` flag, and it is possible to load any number of
files. Paths are relative to the working directory of the executed process, for
example `package/backend`. This means that to select a config file in the repo
root when running the backend, you would use `--config ../../my-config.yaml`,
and for config file on a config server you would use
`--config https://some.domain.io/app-config.yaml`
If no `config` flags are specified, the default behavior is to load
`app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root.
@@ -85,7 +87,7 @@ are NOT loaded. To include them you need to explicitly include them with a flag,
for example:
```shell
yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml
yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml --config https://some.domain.io/app-config.yaml
```
All loaded configuration files are merged together using the following rules:
+19
View File
@@ -109,6 +109,25 @@ const routes = (
);
```
### Using the Search Modal
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
## Adding Search to the Backend
Add the following plugins into your backend app:
@@ -172,6 +172,7 @@ This page itself can be exported as a routable extension in the plugin:
```ts
export const CustomCatalogIndexPage = myPlugin.provide(
createRoutableExtension({
name: 'CustomCatalogIndexPage',
component: () =>
import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage),
mountPoint: catalogRouteRef,
@@ -502,6 +502,8 @@ spec:
lifecycle: production
owner: artist-relations-team
system: artist-engagement-portal
dependsOn:
- resource:default/artists-db
providesApis:
- artist-api
```
@@ -125,8 +125,8 @@ input schema.
input:
- address: '{{ json parameters.address }}'
+ address: ${{ parameters.address }}
- number: '{{ parameters.number }}'
+ number: ${{ parameters.number }} # this will now make sure that the type of number is a number 🙏
- test: '{{ parameters.test }}'
+ test: ${{ parameters.test }} # this will now make sure that the type of test is a number 🙏
```
## `parseRepoUrl` is now a `filter`
@@ -156,4 +156,16 @@ return await createRouter({
});
```
### List of custom action packages
Here is a list of Open Source custom actions that you can add to your Backstage
scaffolder backend:
| Name | Package | Owner |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
Have fun! 🚀
@@ -27,20 +27,27 @@ You can create your own Field Extension by using the
```tsx
//packages/app/scaffolder/MyCustomExtension/MyCustomExtension.tsx
import React from 'react';
import { FieldProps, FieldValidation } from '@rjsf/core';
import FormControl from '@material-ui/core/FormControl';
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
/*
This is the actual component that will get rendered in the form
*/
export const MyCustomExtension = ({ onChange, required }: FieldProps<string>) => {
export const MyCustomExtension = ({
onChange,
rawErrors,
required,
formData,
}: FieldProps<string>) => {
return (
<FormControl
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
>
)
/>
);
};
/*
@@ -63,10 +70,10 @@ export const myCustomValidation = (
```tsx
// packages/app/scaffolder/MyCustomExtension/extensions.ts
/*
/*
This is where the magic happens and creates the custom field extension.
Note that if you're writing extensions part of a separate plugin,
Note that if you're writing extensions part of a separate plugin,
then please use `plugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/
+2 -1
View File
@@ -512,7 +512,8 @@ Usage: backstage-cli config:check [options]
Options:
--package &lt;name&gt; Only load config schema that applies to the given package
--lax Do not require environment variables to be set
--lax Do not require environment variables to be set
--frontend Only validate the frontend configuration
--config &lt;path&gt; Config files to load instead of app-config.yaml (default: [])
-h, --help display help for command
```
+2
View File
@@ -136,6 +136,7 @@ a component:
```ts
export const FooPage = plugin.provide(
createRoutableExtension({
name: 'FooPage',
component: () => import('./components/FooPage').then(m => m.FooPage),
mountPoint: fooPageRouteRef,
}),
@@ -417,6 +418,7 @@ export const myPlugin = createPlugin({
export const MyPage = myPlugin.provide(
createRoutableExtension({
name: 'MyPage',
component: () => import('./components/MyPage').then(m => m.MyPage),
mountPoint: rootRouteRef,
}),
+1
View File
@@ -65,6 +65,7 @@ export const examplePlugin = createPlugin({
// Each extension should also be exported from your plugin package.
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
name: 'ExamplePage',
// The component needs to be lazy-loaded. It's what will actually be rendered in the end.
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
+1
View File
@@ -73,6 +73,7 @@ export const examplePlugin = createPlugin({
export const ExamplePage = examplePlugin.provide(
createRoutableExtension({
name: 'ExamplePage',
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
+8 -3
View File
@@ -151,7 +151,10 @@
{
"type": "subcategory",
"label": "GitLab",
"ids": ["integrations/gitlab/locations"]
"ids": [
"integrations/gitlab/locations",
"integrations/gitlab/discovery"
]
},
{
"type": "subcategory",
@@ -253,7 +256,8 @@
"type": "subcategory",
"label": "API Reference",
"ids": ["reference/index"]
}
},
"api/deprecations"
],
"Tutorials": [
"tutorials/journey",
@@ -275,7 +279,8 @@
"architecture-decisions/adrs-adr008",
"architecture-decisions/adrs-adr009",
"architecture-decisions/adrs-adr010",
"architecture-decisions/adrs-adr011"
"architecture-decisions/adrs-adr011",
"architecture-decisions/adrs-adr012"
],
"FAQ": ["FAQ"]
}
+2
View File
@@ -100,6 +100,7 @@ nav:
- Org Data: 'integrations/github/org.md'
- GitLab:
- Locations: 'integrations/gitlab/locations.md'
- Discovery: 'integrations/gitlab/discovery.md'
- Google GCS:
- Locations: 'integrations/google-cloud-storage/locations.md'
- LDAP:
@@ -163,6 +164,7 @@ nav:
- API Reference:
- Guides:
- Utility APIs: 'api/utility-apis.md'
- Deprecations: 'api/deprecations.md'
- Tutorials:
- Future developer journey: 'tutorials/journey.md'
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
+6 -6
View File
@@ -45,23 +45,21 @@
},
"resolutions": {
"**/@graphql-codegen/cli/**/ws": "^7.4.6",
"**/@roadiehq/**/@backstage/core": "*",
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*",
"graphql-language-service-interface": "2.8.2",
"graphql-language-service-parser": "1.9.0"
"**/@roadiehq/**/@backstage/catalog-model": "*"
},
"version": "1.0.0",
"dependencies": {
"@microsoft/api-documenter": "^7.13.47",
"@microsoft/api-documenter": "^7.13.68",
"@microsoft/api-extractor": "^7.18.7",
"@microsoft/api-extractor-model": "^7.13.5",
"@microsoft/tsdoc": "^0.13.2"
},
"devDependencies": {
"@types/webpack": "^5.28.0",
"@changesets/cli": "^2.14.0",
"@octokit/rest": "^18.12.0",
"@spotify/prettier-config": "^11.0.0",
"@types/webpack": "^5.28.0",
"command-exists": "^1.2.9",
"concurrently": "^6.0.0",
"eslint-plugin-notice": "^0.9.10",
@@ -69,8 +67,10 @@
"husky": "^6.0.0",
"lerna": "^4.0.0",
"lint-staged": "^11.1.2",
"minimist": "^1.2.5",
"prettier": "^2.2.1",
"shx": "^0.3.2",
"ts-node": "^10.4.0",
"yarn-lock-check": "^1.0.5"
},
"prettier": "@spotify/prettier-config",
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+17
View File
@@ -0,0 +1,17 @@
# @backstage/app-defaults
This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app.
## Installation
Install the package via Yarn:
```sh
cd packages/app
yarn add @backstage/app-defaults
```
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
+26
View File
@@ -0,0 +1,26 @@
## API Report File for "@backstage/app-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AppComponents } from '@backstage/core-app-api';
import { AppIcons } from '@backstage/core-app-api';
import { AppOptions } from '@backstage/core-app-api';
import { AppTheme } from '@backstage/core-plugin-api';
import { BackstageApp } from '@backstage/core-app-api';
import { IconComponent } from '@backstage/core-plugin-api';
// @public
export function createApp(
options?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
): BackstageApp;
// @public
export type OptionalAppOptions = {
icons?: Partial<AppIcons> & {
[key in string]: IconComponent;
};
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
components?: Partial<AppComponents>;
};
```
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
"version": "0.1.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/app-defaults"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "backstage-cli build --outputs types,esm",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.7.3",
"@backstage/core-app-api": "^0.1.20",
"@backstage/core-plugin-api": "^0.1.13",
"@backstage/theme": "^0.2.13",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"react": "^16.12.0",
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {
"@backstage/cli": "^0.8.2",
"@backstage/test-utils": "^0.1.21",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react": "*"
},
"files": [
"dist"
]
}
@@ -0,0 +1,42 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { screen } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import React, { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { createApp } from './createApp';
describe('Optional ThemeProvider', () => {
it('should render app with user-provided ThemeProvider', async () => {
const components = {
NotFoundErrorPage: () => null,
BootErrorPage: () => null,
Progress: () => null,
Router: MemoryRouter,
ErrorBoundaryFallback: () => null,
ThemeProvider: ({ children }: PropsWithChildren<{}>) => (
<main role="main">{children}</main>
),
};
const App = createApp({ components }).getProvider();
await renderWithEffects(<App />);
expect(screen.getByRole('main')).toBeInTheDocument();
});
});
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { apis, components, icons, themes } from './defaults';
import {
AppTheme,
BackstagePlugin,
IconComponent,
} from '@backstage/core-plugin-api';
import {
AppComponents,
AppOptions,
AppIcons,
createSpecializedApp,
} from '@backstage/core-app-api';
/**
* Creates a new Backstage App using a default set of components, icons and themes unless
* they are explicitly provided.
*
* @public
*/
export function createApp(
options?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
) {
return createSpecializedApp({
...options,
apis: options?.apis ?? [],
bindRoutes: options?.bindRoutes,
components: {
...components,
...options?.components,
},
configLoader: options?.configLoader,
defaultApis: apis,
icons: {
...icons,
...options?.icons,
},
plugins: (options?.plugins as BackstagePlugin<any, any>[]) ?? [],
themes: options?.themes ?? themes,
});
}
/**
* The set of app options that {@link createApp} will provide defaults for
* if they are not passed in explicitly.
*
* @public
*/
export type OptionalAppOptions = {
/**
* A set of icons to override the default icons with.
*
* The override is applied for each icon individually.
*
* @public
*/
icons?: Partial<AppIcons> & {
[key in string]: IconComponent;
};
/**
* A set of themes that override all of the default app themes.
*
* If this option is provided none of the default themes will be used.
*
* @public
*/
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[]; // TODO: simplify once AppTheme is updated
/**
* A set of components to override the default components with.
*
* The override is applied for each icon individually.
*
* @public
*/
components?: Partial<AppComponents>;
};
+274
View File
@@ -0,0 +1,274 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 {
AlertApiForwarder,
NoOpAnalyticsApi,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
OAuth2,
OktaAuth,
GitlabAuth,
Auth0Auth,
MicrosoftAuth,
BitbucketAuth,
OAuthRequestManager,
WebStorage,
UrlPatternDiscovery,
SamlAuth,
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
} from '@backstage/core-app-api';
import {
createApiFactory,
alertApiRef,
analyticsApiRef,
errorApiRef,
discoveryApiRef,
oauthRequestApiRef,
googleAuthApiRef,
githubAuthApiRef,
oauth2ApiRef,
oktaAuthApiRef,
gitlabAuthApiRef,
auth0AuthApiRef,
microsoftAuthApiRef,
storageApiRef,
configApiRef,
samlAuthApiRef,
oneloginAuthApiRef,
oidcAuthApiRef,
bitbucketAuthApiRef,
atlassianAuthApiRef,
} from '@backstage/core-plugin-api';
export const apis = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
),
}),
createApiFactory({
api: alertApiRef,
deps: {},
factory: () => new AlertApiForwarder(),
}),
createApiFactory({
api: analyticsApiRef,
deps: {},
factory: () => new NoOpAnalyticsApi(),
}),
createApiFactory({
api: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) => {
const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
UnhandledErrorForwarder.forward(errorApi, { hidden: false });
return errorApi;
},
}),
createApiFactory({
api: storageApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory({
api: oauthRequestApiRef,
deps: {},
factory: () => new OAuthRequestManager(),
}),
createApiFactory({
api: googleAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
MicrosoftAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oktaAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OktaAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oauth2ApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: samlAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, configApi }) =>
SamlAuth.create({
discoveryApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oneloginAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OneLoginAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: oidcAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: 'oidc',
title: 'Your Identity Provider',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: bitbucketAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketAuth.create({
discoveryApi,
oauthRequestApi,
defaultScopes: ['team'],
environment: configApi.getOptionalString('auth.environment'),
}),
}),
createApiFactory({
api: atlassianAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return AtlassianAuth.create({
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
});
},
}),
];
@@ -0,0 +1,38 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { OptionallyWrapInRouter } from './components';
describe('OptionallyWrapInRouter', () => {
it('should wrap with router if not yet inside a router', async () => {
render(<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should not wrap with router if already inside a router', async () => {
render(
<MemoryRouter>
<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>
</MemoryRouter>,
);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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, { ReactNode } from 'react';
import Button from '@material-ui/core/Button';
import { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components';
import {
MemoryRouter,
useInRouterContext,
BrowserRouter,
} from 'react-router-dom';
import {
AppComponents,
BootErrorPageProps,
ErrorBoundaryFallbackProps,
} from '@backstage/core-plugin-api';
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
if (useInRouterContext()) {
return <>{children}</>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
const DefaultNotFoundPage = () => (
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
);
const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => {
let message = '';
if (step === 'load-config') {
message = `The configuration failed to load, someone should have a look at this error: ${error.message}`;
} else if (step === 'load-chunk') {
message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`;
}
// TODO: figure out a nicer way to handle routing on the error page, when it can be done.
return (
<OptionallyWrapInRouter>
<ErrorPage status="501" statusMessage={message} />
</OptionallyWrapInRouter>
);
};
const DefaultErrorBoundaryFallback = ({
error,
resetError,
plugin,
}: ErrorBoundaryFallbackProps) => {
return (
<ErrorPanel
title={`Error in ${plugin?.getId()}`}
defaultExpanded
error={error}
>
<Button variant="outlined" onClick={resetError}>
Retry
</Button>
</ErrorPanel>
);
};
/**
* Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}.
*
* @public
*/
export const components: AppComponents = {
Progress,
Router: BrowserRouter,
NotFoundErrorPage: DefaultNotFoundPage,
BootErrorPage: DefaultBootErrorPage,
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
};
@@ -18,6 +18,9 @@ import { IconComponent } from '@backstage/core-plugin-api';
import MuiApartmentIcon from '@material-ui/icons/Apartment';
import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage';
import MuiCategoryIcon from '@material-ui/icons/Category';
import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder';
import MuiSubjectIcon from '@material-ui/icons/Subject';
import MuiSearchIcon from '@material-ui/icons/Search';
import MuiChatIcon from '@material-ui/icons/Chat';
import MuiDashboardIcon from '@material-ui/icons/Dashboard';
import MuiDocsIcon from '@material-ui/icons/Description';
@@ -32,46 +35,27 @@ import MuiPeopleIcon from '@material-ui/icons/People';
import MuiPersonIcon from '@material-ui/icons/Person';
import MuiWarningIcon from '@material-ui/icons/Warning';
type AppIconsKey =
| 'brokenImage'
| 'catalog'
| 'chat'
| 'dashboard'
| 'docs'
| 'email'
| 'github'
| 'group'
| 'help'
| 'kind:api'
| 'kind:component'
| 'kind:domain'
| 'kind:group'
| 'kind:location'
| 'kind:system'
| 'kind:user'
| 'user'
| 'warning';
export type AppIcons = { [key in AppIconsKey]: IconComponent };
export const defaultAppIcons: AppIcons = {
brokenImage: MuiBrokenImageIcon,
export const icons = {
brokenImage: MuiBrokenImageIcon as IconComponent,
// To be confirmed: see https://github.com/backstage/backstage/issues/4970
catalog: MuiMenuBookIcon,
chat: MuiChatIcon,
dashboard: MuiDashboardIcon,
docs: MuiDocsIcon,
email: MuiEmailIcon,
github: MuiGitHubIcon,
group: MuiPeopleIcon,
help: MuiHelpIcon,
'kind:api': MuiExtensionIcon,
'kind:component': MuiMemoryIcon,
'kind:domain': MuiApartmentIcon,
'kind:group': MuiPeopleIcon,
'kind:location': MuiLocationOnIcon,
'kind:system': MuiCategoryIcon,
'kind:user': MuiPersonIcon,
user: MuiPersonIcon,
warning: MuiWarningIcon,
catalog: MuiMenuBookIcon as IconComponent,
scaffolder: MuiCreateNewFolderIcon as IconComponent,
techdocs: MuiSubjectIcon as IconComponent,
search: MuiSearchIcon as IconComponent,
chat: MuiChatIcon as IconComponent,
dashboard: MuiDashboardIcon as IconComponent,
docs: MuiDocsIcon as IconComponent,
email: MuiEmailIcon as IconComponent,
github: MuiGitHubIcon as IconComponent,
group: MuiPeopleIcon as IconComponent,
help: MuiHelpIcon as IconComponent,
'kind:api': MuiExtensionIcon as IconComponent,
'kind:component': MuiMemoryIcon as IconComponent,
'kind:domain': MuiApartmentIcon as IconComponent,
'kind:group': MuiPeopleIcon as IconComponent,
'kind:location': MuiLocationOnIcon as IconComponent,
'kind:system': MuiCategoryIcon as IconComponent,
'kind:user': MuiPersonIcon as IconComponent,
user: MuiPersonIcon as IconComponent,
warning: MuiWarningIcon as IconComponent,
};
@@ -0,0 +1,20 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { apis } from './apis';
export { components } from './components';
export { icons } from './icons';
export { themes } from './themes';
@@ -0,0 +1,50 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { darkTheme, lightTheme } from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { AppTheme } from '@backstage/core-plugin-api';
export const themes: AppTheme[] = [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
theme: lightTheme,
Provider: ({ children }) => (
<ThemeProvider theme={lightTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
];
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
/**
* Provides the default wiring of a Backstage App
*
* @packageDocumentation
*/
export { createApp } from './createApp';
export type { OptionalAppOptions } from './createApp';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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';
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.0",
"@backstage/catalog-model": "^0.9.5",
"@backstage/cli": "^0.8.0",
"@backstage/core-app-api": "^0.1.18",
+2 -2
View File
@@ -26,7 +26,8 @@ import {
RELATION_PART_OF,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { createApp, FlatRoutes } from '@backstage/core-app-api';
import { createApp } from '@backstage/app-defaults';
import { FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -93,7 +94,6 @@ const app = createApp({
// Custom icon example
alert: AlarmIcon,
},
components: {
SignInPage: props => {
return (
+2 -2
View File
@@ -29,7 +29,7 @@ import LogoIcon from './LogoIcon';
import { NavLink } from 'react-router-dom';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Sidebar,
@@ -79,7 +79,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearch />
<SidebarSearchModal />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
+23
View File
@@ -1,5 +1,28 @@
# @backstage/backend-common
## 0.9.9
### Patch Changes
- 8c4cad0bf2: AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response.
- 0611f3b3e2: Reading app config from a remote server
- Updated dependencies
- @backstage/config-loader@0.7.2
## 0.9.8
### Patch Changes
- 96cfa561eb: Adjusted some API exports
- 10615525f3: Switch to use the json and observable types from `@backstage/types`
- 1be8d2abdb: Any set configurations which have been tagged with a visibility 'secret', are now redacted from log lines.
- Updated dependencies
- @backstage/config@0.1.11
- @backstage/cli-common@0.1.5
- @backstage/errors@0.1.4
- @backstage/integration@0.6.9
- @backstage/config-loader@0.7.1
## 0.9.7
### Patch Changes
+12 -5
View File
@@ -211,6 +211,12 @@ export type ErrorHandlerOptions = {
logClientErrors?: boolean;
};
// @public
export type FromReadableArrayOptions = Array<{
data: Readable;
path: string;
}>;
// @public (undocumented)
export function getRootLogger(): winston.Logger;
@@ -368,6 +374,9 @@ export class GitlabUrlReader implements UrlReader {
export { isChildPath };
// @public
export function isDatabaseConflictError(e: unknown): boolean;
// @public
export function loadBackendConfig(options: {
logger: Logger_2;
@@ -419,15 +428,13 @@ export type ReadTreeResponse = {
etag: string;
};
// @public (undocumented)
// @public
export type ReadTreeResponseDirOptions = {
targetDir?: string;
};
// @public (undocumented)
export interface ReadTreeResponseFactory {
// Warning: (ae-forgotten-export) The symbol "FromReadableArrayOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
fromReadableArray(
options: FromReadableArrayOptions,
@@ -442,7 +449,7 @@ export interface ReadTreeResponseFactory {
): Promise<ReadTreeResponse>;
}
// @public (undocumented)
// @public
export type ReadTreeResponseFactoryOptions = {
stream: Readable;
subpath?: string;
@@ -580,7 +587,7 @@ export type UrlReader = {
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
};
// @public (undocumented)
// @public
export type UrlReaderPredicateTuple = {
predicate: (url: URL) => boolean;
reader: UrlReader;
+10 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.9.7",
"version": "0.9.9",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,13 +29,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.4",
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.7.0",
"@backstage/errors": "^0.1.3",
"@backstage/integration": "^0.6.7",
"@backstage/cli-common": "^0.1.5",
"@backstage/config": "^0.1.11",
"@backstage/config-loader": "^0.7.2",
"@backstage/errors": "^0.1.4",
"@backstage/integration": "^0.6.9",
"@backstage/types": "^0.1.1",
"@google-cloud/storage": "^5.8.0",
"@lerna/project": "^4.0.0",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.2.1",
@@ -78,8 +79,8 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.8.0",
"@backstage/test-utils": "^0.1.18",
"@backstage/cli": "^0.8.2",
"@backstage/test-utils": "^0.1.21",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -99,7 +100,7 @@
"http-errors": "^1.7.3",
"jest": "^26.0.1",
"mock-fs": "^5.1.0",
"msw": "^0.29.0",
"msw": "^0.35.0",
"mysql2": "^2.2.5",
"recursive-readdir": "^2.2.2",
"supertest": "^6.1.3"
+54 -5
View File
@@ -18,9 +18,41 @@ import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { Logger } from 'winston';
import { findPaths } from '@backstage/cli-common';
import { Config, ConfigReader } from '@backstage/config';
import {
loadConfigSchema,
loadConfig,
ConfigSchema,
ConfigTarget,
} from '@backstage/config-loader';
import { AppConfig, Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { loadConfig } from '@backstage/config-loader';
import { isValidUrl } from './urls';
import { setRootLoggerRedactionList } from './logging/rootLogger';
// Fetch the schema and get all the secrets to pass to the rootLogger for redaction
const updateRedactionList = (
schema: ConfigSchema,
configs: AppConfig[],
logger: Logger,
) => {
const secretAppConfigs = schema.process(configs, { visibility: ['secret'] });
const secretConfig = ConfigReader.fromConfigs(secretAppConfigs);
const values = new Set<string>();
const data = secretConfig.get();
JSON.parse(
JSON.stringify(data),
(_, v) => typeof v === 'string' && values.add(v),
);
logger.info(
`${values.size} secrets found in the config which will be redacted`,
);
setRootLoggerRedactionList(Array.from(values));
};
export class ObservableConfigProxy implements Config {
private config: Config = new ConfigReader({});
@@ -149,16 +181,29 @@ export async function loadBackendConfig(options: {
argv: string[];
}): Promise<Config> {
const args = parseArgs(options.argv);
const configPaths: string[] = [args.config ?? []].flat();
const config = new ObservableConfigProxy(options.logger);
const configTargets: ConfigTarget[] = [args.config ?? []]
.flat()
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
// TODO(hhogg): This is fetching _all_ of the packages of the monorepo
// in order to find the secrets for redactions, however we only care about
// the backend ones, we need to find a way to exclude the frontend packages.
const { Project } = require('@lerna/project');
const project = new Project(paths.targetDir);
const packages = await project.getPackages();
const schema = await loadConfigSchema({
dependencies: packages.map((p: any) => p.name),
});
const config = new ObservableConfigProxy(options.logger);
const configs = await loadConfig({
configRoot: paths.targetRoot,
configPaths: configPaths.map(opt => resolvePath(opt)),
configPaths: [],
configTargets: configTargets,
watch: {
onChange(newConfigs) {
options.logger.info(
@@ -187,5 +232,9 @@ export async function loadBackendConfig(options: {
config.setConfig(ConfigReader.fromConfigs(configs));
// Subscribe to config changes and update the redaction list for logging
updateRedactionList(schema, configs, options.logger);
config.subscribe(() => updateRedactionList(schema, configs, options.logger));
return config;
}
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex } from 'knex';
import { omit } from 'lodash';
import { Config, ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import {
ensureDatabaseExists,
createNameOverride,
ensureDatabaseExists,
normalizeConnection,
createSchemaOverride,
ensureSchemaExists,
@@ -182,7 +183,7 @@ export class DatabaseManager {
);
return {
// include base connection if client type has not been overriden
// include base connection if client type has not been overridden
...(overridden ? {} : baseConnection),
...connection,
};
@@ -28,3 +28,4 @@ export {
} from './connection';
export type { PluginDatabaseManager } from './types';
export { isDatabaseConflictError } from './util';
@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
/**
* Tries to deduce whether a thrown error is a database conflict.
*
* @public
* @param e - A thrown error
* @returns True if the error looks like it was a conflict error thrown by a
* known database engine
*/
export function isDatabaseConflictError(e: unknown) {
const message = (e as any)?.message;
return (
typeof message === 'string' &&
(/SQLITE_CONSTRAINT: UNIQUE/.test(message) ||
/unique constraint/.test(message))
);
}
+1 -1
View File
@@ -15,5 +15,5 @@
*/
export * from './formats';
export * from './rootLogger';
export { createRootLogger, getRootLogger, setRootLogger } from './rootLogger';
export * from './voidLogger';
@@ -15,7 +15,12 @@
*/
import * as winston from 'winston';
import { createRootLogger, getRootLogger, setRootLogger } from './rootLogger';
import {
createRootLogger,
getRootLogger,
setRootLogger,
setRootLoggerRedactionList,
} from './rootLogger';
describe('rootLogger', () => {
it('can replace the default logger', () => {
@@ -30,7 +35,20 @@ describe('rootLogger', () => {
);
});
describe('createRootLoger', () => {
it('redacts given secrets', () => {
const logger = createRootLogger();
jest.spyOn(logger, 'write');
setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'SECRET.3']);
logger.info('Logging SECRET-1 and SECRET_2 and SECRET.3');
expect(logger.write).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Logging [REDACTED] and [REDACTED] and [REDACTED]',
}),
);
});
describe('createRootLogger', () => {
it('creates a new logger', () => {
const oldLogger = getRootLogger();
const newLogger = createRootLogger();
@@ -18,8 +18,10 @@ import { merge } from 'lodash';
import * as winston from 'winston';
import { LoggerOptions } from 'winston';
import { coloredFormat } from './formats';
import { escapeRegExp } from '../util/escapeRegExp';
let rootLogger: winston.Logger;
let redactionRegExp: RegExp;
/** @public */
export function getRootLogger(): winston.Logger {
@@ -31,6 +33,30 @@ export function setRootLogger(newLogger: winston.Logger) {
rootLogger = newLogger;
}
export function setRootLoggerRedactionList(redactionList: string[]) {
if (redactionList.length) {
redactionRegExp = new RegExp(
`(${redactionList.map(escapeRegExp).join('|')})`,
'g',
);
}
}
/**
* A winston formatting function that finds occurrences of filteredKeys
* and replaces them with the corresponding identifier.
*/
function redactLogLine(info: winston.Logform.TransformableInfo) {
// TODO(hhogg): The logger is created before the config is loaded,
// because the logger is needed in the config loader. There is a risk of
// a secret being logged out during the config loading stage.
if (redactionRegExp) {
info.message = info.message.replace(redactionRegExp, '[REDACTED]');
}
return info;
}
/** @public */
export function createRootLogger(
options: winston.LoggerOptions = {},
@@ -41,6 +67,7 @@ export function createRootLogger(
{
level: env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format(redactLogLine)(),
env.NODE_ENV === 'production' ? winston.format.json() : coloredFormat,
),
defaultMeta: {
@@ -27,6 +27,7 @@ import { UrlReaderPredicateTuple } from './types';
import AWSMock from 'aws-sdk-mock';
import aws from 'aws-sdk';
import path from 'path';
import { NotModifiedError } from '@backstage/errors';
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
@@ -131,29 +132,37 @@ describe('AwsS3UrlReader', () => {
});
describe('read', () => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
let awsS3UrlReader: AwsS3UrlReader;
beforeAll(() => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
__dirname,
'__fixtures__/awsS3/awsS3-mock-object.yaml',
),
),
),
),
);
const s3 = new aws.S3();
const awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
);
const s3 = new aws.S3();
awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
),
{ s3, treeResponseFactory },
);
{ s3, treeResponseFactory },
);
});
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.read(
@@ -176,32 +185,39 @@ describe('AwsS3UrlReader', () => {
});
describe('readUrl', () => {
AWSMock.setSDKInstance(aws);
let awsS3UrlReader: AwsS3UrlReader;
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
beforeAll(() => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
__dirname,
'__fixtures__/awsS3/awsS3-mock-object.yaml',
),
),
),
),
);
);
const s3 = new aws.S3();
const s3 = new aws.S3();
const awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
),
{ s3, treeResponseFactory },
);
{ s3, treeResponseFactory },
);
});
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.readUrl(
@@ -223,40 +239,89 @@ describe('AwsS3UrlReader', () => {
);
});
});
describe('readUrl with etag', () => {
let awsS3UrlReader: AwsS3UrlReader;
beforeAll(() => {
AWSMock.setSDKInstance(aws);
AWSMock.mock('S3', 'getObject', (_, callback) => {
callback({ statusCode: 304 }, null);
});
const s3 = new aws.S3();
awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
{ s3, treeResponseFactory },
);
});
it('returns contents of an object in a bucket', async () => {
await expect(
awsS3UrlReader.readUrl(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
{
etag: 'abc123',
},
),
).rejects.toThrow(NotModifiedError);
});
});
describe('readTree', () => {
const object: aws.S3.Types.Object = {
Key: 'awsS3-mock-object.yaml',
};
const objectList: aws.S3.ObjectList = [object];
const output: aws.S3.Types.ListObjectsV2Output = {
Contents: objectList,
};
AWSMock.setSDKInstance(aws);
AWSMock.mock('S3', 'listObjectsV2', output);
let awsS3UrlReader: AwsS3UrlReader;
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
),
),
);
beforeAll(() => {
const object: aws.S3.Types.Object = {
Key: 'awsS3-mock-object.yaml',
};
const s3 = new aws.S3();
const awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: '.amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
const objectList: aws.S3.ObjectList = [object];
const output: aws.S3.Types.ListObjectsV2Output = {
Contents: objectList,
};
AWSMock.setSDKInstance(aws);
AWSMock.mock('S3', 'listObjectsV2', output);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
__dirname,
'__fixtures__/awsS3/awsS3-mock-object.yaml',
),
),
),
),
{ s3, treeResponseFactory },
);
);
const s3 = new aws.S3();
awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: '.amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
{ s3, treeResponseFactory },
);
});
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.readTree(
'https://test.s3.us-east-2.amazonaws.com',
@@ -27,7 +27,7 @@ import {
} from './types';
import getRawBody from 'raw-body';
import { AwsS3Integration, ScmIntegrations } from '@backstage/integration';
import { ForwardedError } from '@backstage/errors';
import { ForwardedError, NotModifiedError } from '@backstage/errors';
import { ListObjectsV2Output, ObjectList } from 'aws-sdk/clients/s3';
const parseURL = (
@@ -163,6 +163,10 @@ export class AwsS3UrlReader implements UrlReader {
etag: etag,
};
} catch (e) {
if (e.statusCode === 304) {
throw new NotModifiedError();
}
throw new ForwardedError('Could not retrieve file from S3', e);
}
}
@@ -19,7 +19,7 @@ import {
AzureIntegration,
readAzureIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
@@ -51,7 +51,7 @@ describe('AzureUrlReader', () => {
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
setupRequestMockHandlers(worker);
describe('read', () => {
beforeEach(() => {
@@ -19,7 +19,7 @@ import {
BitbucketIntegration,
readBitbucketIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
@@ -72,7 +72,7 @@ describe('BitbucketUrlReader', () => {
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
setupRequestMockHandlers(worker);
describe('readUrl', () => {
worker.use(
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { msw } from '@backstage/test-utils';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { getVoidLogger } from '../logging';
@@ -28,7 +28,7 @@ const fetchUrlReader = new FetchUrlReader();
describe('FetchUrlReader', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
setupRequestMockHandlers(worker);
beforeEach(() => {
jest.clearAllMocks();
@@ -20,7 +20,7 @@ import {
GitHubIntegration,
readGitHubIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
@@ -73,7 +73,7 @@ const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('GithubUrlReader', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
setupRequestMockHandlers(worker);
beforeEach(() => {
mockFs({
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { msw } from '@backstage/test-utils';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
@@ -77,7 +77,7 @@ describe('GitlabUrlReader', () => {
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
setupRequestMockHandlers(worker);
describe('read', () => {
beforeEach(() => {
@@ -20,6 +20,7 @@ export { GithubUrlReader } from './GithubUrlReader';
export { GitlabUrlReader } from './GitlabUrlReader';
export { AwsS3UrlReader } from './AwsS3UrlReader';
export type {
FromReadableArrayOptions,
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
+106 -37
View File
@@ -24,24 +24,41 @@ import { Config } from '@backstage/config';
* @public
*/
export type UrlReader = {
/* Used to read a single file and return its content. */
/**
* Reads a single file and return its content.
*/
read(url: string): Promise<Buffer>;
/**
* A replacement for the read method that supports options and complex responses.
* Reads a single file and return its content.
*
* Use this whenever it is available, as the read method will be deprecated and
* eventually removed in the future.
* @remarks
*
* This is a replacement for the read method that supports options and
* complex responses.
*
* Use this whenever it is available, as the read method will be
* deprecated and eventually removed in a future release.
*/
readUrl?(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
/* Used to read a file tree and download as a directory. */
/**
* Reads a full or partial file tree.
*/
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
/* Used to search a file in a tree using a glob pattern. */
/**
* Searches for a file in a tree using a glob pattern.
*/
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
};
/** @public */
/**
* A predicate that decides whether a specific {@link UrlReader} can handle a
* given URL.
*
* @public
*/
export type UrlReaderPredicateTuple = {
predicate: (url: URL) => boolean;
reader: UrlReader;
@@ -49,7 +66,7 @@ export type UrlReaderPredicateTuple = {
/**
* A factory function that can read config to construct zero or more
* UrlReaders along with a predicate for when it should be used.
* {@link UrlReader}s along with a predicate for when it should be used.
*
* @public
*/
@@ -66,21 +83,28 @@ export type ReaderFactory = (options: {
*/
export type ReadUrlOptions = {
/**
* An etag can be provided to check whether readUrl's response has changed from a previous execution.
* An ETag which can be provided to check whether a
* {@link UrlReader.readUrl} response has changed from a previous execution.
*
* In the readUrl() response, an etag is returned along with the data. The etag is a unique identifer
* of the data, usually the commit SHA or etag from the target.
* @remarks
*
* When an etag is given in ReadUrlOptions, readUrl will first compare the etag against the etag
* on the target. If they match, readUrl will throw a NotModifiedError indicating that the readUrl
* response will not differ from the previous response which included this particular etag. If they
* do not match, readUrl will return the rest of ReadUrlResponse along with a new etag.
* In the {@link UrlReader.readUrl} response, an ETag is returned along with
* the data. The ETag is a unique identifier of the data, usually the commit
* SHA or ETag from the target.
*
* When an ETag is given in ReadUrlOptions, {@link UrlReader.readUrl} will
* first compare the ETag against the ETag of the target. If they match,
* {@link UrlReader.readUrl} will throw a
* {@link @backstage/errors#NotModifiedError} indicating that the response
* will not differ from the previous response which included this particular
* ETag. If they do not match, {@link UrlReader.readUrl} will return the rest
* of the response along with a new ETag.
*/
etag?: string;
};
/**
* A response object for readUrl operations.
* A response object for {@link UrlReader.readUrl} operations.
*
* @public
*/
@@ -92,13 +116,16 @@ export type ReadUrlResponse = {
/**
* Etag returned by content provider.
*
* @remarks
*
* Can be used to compare and cache responses when doing subsequent calls.
*/
etag?: string;
};
/**
* An options object for readTree operations.
* An options object for {@link UrlReader.readTree} operations.
*
* @public
*/
@@ -106,6 +133,8 @@ export type ReadTreeOptions = {
/**
* A filter that can be used to select which files should be included.
*
* @remarks
*
* The path passed to the filter function is the relative path from the URL
* that the file tree is fetched from, without any leading '/'.
*
@@ -113,56 +142,82 @@ export type ReadTreeOptions = {
* at https://github.com/my/repo/blob/master/my-dir/my-subdir/my-file.txt will
* be represented as my-subdir/my-file.txt
*
* If no filter is provided all files are extracted.
* If no filter is provided, all files are extracted.
*/
filter?(path: string, info?: { size: number }): boolean;
/**
* An etag can be provided to check whether readTree's response has changed from a previous execution.
* An ETag which can be provided to check whether a
* {@link UrlReader.readTree} response has changed from a previous execution.
*
* In the readTree() response, an etag is returned along with the tree blob. The etag is a unique identifer
* of the tree blob, usually the commit SHA or etag from the target.
* @remarks
*
* When an etag is given in ReadTreeOptions, readTree will first compare the etag against the etag
* on the target branch. If they match, readTree will throw a NotModifiedError indicating that the readTree
* response will not differ from the previous response which included this particular etag. If they
* do not match, readTree will return the rest of ReadTreeResponse along with a new etag.
* In the {@link UrlReader.readTree} response, an ETag is returned along with
* the tree blob. The ETag is a unique identifier of the tree blob, usually
* the commit SHA or ETag from the target.
*
* When an ETag is given as a request option, {@link UrlReader.readTree} will
* first compare the ETag against the ETag on the target branch. If they
* match, {@link UrlReader.readTree} will throw a
* {@link @backstage/errors#NotModifiedError} indicating that the response
* will not differ from the previous response which included this particular
* ETag. If they do not match, {@link UrlReader.readTree} will return the
* rest of the response along with a new ETag.
*/
etag?: string;
};
/** @public */
/**
* Options that control {@link ReadTreeResponse.dir} execution.
*
* @public
*/
export type ReadTreeResponseDirOptions = {
/** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */
/**
* The directory to write files to.
*
* @remarks
*
* Defaults to the OS tmpdir, or `backend.workingDirectory` if set in config.
*/
targetDir?: string;
};
/**
* A response object for readTree operations.
* A response object for {@link UrlReader.readTree} operations.
*
* @public
*/
export type ReadTreeResponse = {
/**
* files() returns an array of all the files inside the tree and corresponding functions to read their content.
* Returns an array of all the files inside the tree, and corresponding
* functions to read their content.
*/
files(): Promise<ReadTreeResponseFile[]>;
/**
* Returns the tree contents as a binary archive, using a stream.
*/
archive(): Promise<NodeJS.ReadableStream>;
/**
* dir() extracts the tree response into a directory and returns the path of the directory.
* Extracts the tree response into a directory and returns the path of the
* directory.
*/
dir(options?: ReadTreeResponseDirOptions): Promise<string>;
/**
* Etag returned by content provider.
*
* @remarks
*
* Can be used to compare and cache responses when doing subsequent calls.
*/
etag: string;
};
/**
* Represents a single file in a readTree response.
* Represents a single file in a {@link UrlReader.readTree} response.
*
* @public
*/
@@ -171,7 +226,11 @@ export type ReadTreeResponseFile = {
content(): Promise<Buffer>;
};
/** @public */
/**
* Options that control execution of {@link ReadTreeResponseFactory} methods.
*
* @public
*/
export type ReadTreeResponseFactoryOptions = {
// A binary stream of a tar archive.
stream: Readable;
@@ -183,11 +242,21 @@ export type ReadTreeResponseFactoryOptions = {
// Filter passed on from the ReadTreeOptions
filter?: (path: string, info?: { size: number }) => boolean;
};
/** @public */
/**
* Options that control {@link ReadTreeResponseFactory.fromReadableArray}
* execution.
*
* @public
*/
export type FromReadableArrayOptions = Array<{
// Data in the form of a readable
/**
* The raw data itself.
*/
data: Readable;
// A string containing the filepath of the data
/**
* The filepath of the data.
*/
path: string;
}>;
@@ -213,7 +282,7 @@ export type SearchOptions = {
/**
* An etag can be provided to check whether the search response has changed from a previous execution.
*
* In the search() response, an etag is returned along with the files. The etag is a unique identifer
* In the search() response, an etag is returned along with the files. The etag is a unique identifier
* of the current tree, usually the commit SHA or etag from the target.
*
* When an etag is given in SearchOptions, search will first compare the etag against the etag
@@ -236,7 +305,7 @@ export type SearchResponse = {
files: SearchResponseFile[];
/**
* A unique identifer of the current remote tree, usually the commit SHA or etag from the target.
* A unique identifier of the current remote tree, usually the commit SHA or etag from the target.
*/
etag: string;
};
@@ -174,27 +174,27 @@ export class ServiceBuilderImpl implements ServiceBuilder {
const server: http.Server = httpsSettings
? await createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(server, 0);
useHotCleanup(this.module, () =>
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
return new Promise((resolve, reject) => {
app.on('error', e => {
logger.error(`Failed to start up on port ${port}, ${e}`);
function handleStartupError(e: unknown) {
server.close();
reject(e);
}
server.on('error', handleStartupError);
server.listen(port, host, () => {
server.off('error', handleStartupError);
logger.info(`Listening on ${host}:${port}`);
resolve(stoppableServer);
});
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(stoppableServer);
});
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { isValidUrl } from './urls';
describe('isValidUrl', () => {
it('should return true for url', () => {
const validUrl = isValidUrl('http://some.valid.url');
expect(validUrl).toBe(true);
});
it('should return false for absolute path', () => {
const validUrl = isValidUrl('/some/absolute/path');
expect(validUrl).toBe(false);
});
it('should return false for relative path', () => {
const validUrl = isValidUrl('../some/relative/path');
expect(validUrl).toBe(false);
});
});
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 function isValidUrl(url: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { escapeRegExp } from './escapeRegExp';
describe('escapeRegExp', () => {
test('does not escape non-regex characters', () => {
expect(escapeRegExp('Backstage Backstage')).toBe('Backstage Backstage');
});
test('all the characters', () => {
expect(escapeRegExp('^$\\.*+?()[]{}|')).toBe(
'\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|',
);
});
test('character: ^', () => {
expect(escapeRegExp('^')).toBe('\\^');
});
test('character: $', () => {
expect(escapeRegExp('$')).toBe('\\$');
});
test('character: \\', () => {
expect(escapeRegExp('\\')).toBe('\\\\');
});
test('character: .', () => {
expect(escapeRegExp('.')).toBe('\\.');
});
test('character: *', () => {
expect(escapeRegExp('*')).toBe('\\*');
});
test('character: +', () => {
expect(escapeRegExp('+')).toBe('\\+');
});
test('character: ?', () => {
expect(escapeRegExp('?')).toBe('\\?');
});
test('character: (', () => {
expect(escapeRegExp('(')).toBe('\\(');
});
test('character: )', () => {
expect(escapeRegExp(')')).toBe('\\)');
});
test('character: [', () => {
expect(escapeRegExp('[')).toBe('\\[');
});
test('character: ]', () => {
expect(escapeRegExp(']')).toBe('\\]');
});
test('character: {', () => {
expect(escapeRegExp('{')).toBe('\\{');
});
test('character: }', () => {
expect(escapeRegExp('}')).toBe('\\}');
});
test('character: |', () => {
expect(escapeRegExp('|')).toBe('\\|');
});
});
@@ -14,21 +14,11 @@
* limitations under the License.
*/
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
export type RepoBuild = {
id?: number;
title: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
source: string;
};
export type RepoBuildOptions = {
top?: number;
/**
* Escapes a given string to be used inside a RegExp.
*
* Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
export const escapeRegExp = (text: string) => {
return text.replace(/[.*+?^${}(\)|[\]\\]/g, '\\$&');
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+35
View File
@@ -0,0 +1,35 @@
# @backstage/backend-tasks
Common distributed task management for Backstage backends.
## Usage
Add the library to your backend package:
```sh
# From your Backstage root directory
cd packages/backend
yarn add @backstage/backend-tasks
```
then make use of its facilities as necessary:
```typescript
import { TaskScheduler } from '@backstage/backend-tasks';
import { Duration } from 'luxon';
const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin');
await scheduler.scheduleTask({
id: 'refresh-things',
frequency: Duration.fromObject({ minutes: 10 }),
fn: async () => {
await entityProvider.run();
},
});
```
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)
+45
View File
@@ -0,0 +1,45 @@
## API Report File for "@backstage/backend-tasks"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AbortSignal as AbortSignal_2 } from 'node-abort-controller';
import { Config } from '@backstage/config';
import { DatabaseManager } from '@backstage/backend-common';
import { Duration } from 'luxon';
import { Logger as Logger_2 } from 'winston';
// @public
export interface PluginTaskScheduler {
scheduleTask(task: TaskDefinition): Promise<void>;
}
// @public
export interface TaskDefinition {
fn: TaskFunction;
frequency: Duration;
id: string;
initialDelay?: Duration;
signal?: AbortSignal_2;
timeout: Duration;
}
// @public
export type TaskFunction =
| ((abortSignal: AbortSignal_2) => void | Promise<void>)
| (() => void | Promise<void>);
// @public
export class TaskScheduler {
constructor(databaseManager: DatabaseManager, logger: Logger_2);
forPlugin(pluginId: string): PluginTaskScheduler;
// (undocumented)
static fromConfig(
config: Config,
options?: {
databaseManager?: DatabaseManager;
logger?: Logger_2;
},
): TaskScheduler;
}
```
@@ -0,0 +1,64 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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} knex
*/
exports.up = async function up(knex) {
//
// tasks
//
await knex.schema.createTable('backstage_backend_tasks__tasks', table => {
table.comment('Tasks used for scheduling work on multiple workers');
table
.text('id')
.primary()
.notNullable()
.comment('The unique ID of this particular task');
table
.text('settings_json')
.notNullable()
.comment('JSON serialized object with properties for this task');
table
.dateTime('next_run_start_at')
.notNullable()
.comment('The next time that the task should be started');
table
.text('current_run_ticket')
.nullable()
.comment('A unique ticket for the current task run');
table
.dateTime('current_run_started_at')
.nullable()
.comment('The time that the current task run started');
table
.dateTime('current_run_expires_at')
.nullable()
.comment('The time that the current task run will time out');
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
//
// tasks
//
await knex.schema.dropTable('backstage_backend_tasks__tasks');
};
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/backend-tasks"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli build --outputs cjs,types",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.9.8",
"@backstage/config": "^0.1.11",
"@backstage/errors": "^0.1.4",
"@backstage/types": "^0.1.1",
"@types/luxon": "^2.0.4",
"knex": "^0.95.1",
"lodash": "^4.17.21",
"luxon": "^2.0.2",
"node-abort-controller": "^3.0.1",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"zod": "^3.9.5"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.8",
"@backstage/cli": "^0.8.1",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
"files": [
"dist",
"migrations/**/*.{js,d.ts}"
]
}
@@ -0,0 +1,31 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { resolvePackagePath } from '@backstage/backend-common';
import { Knex } from 'knex';
import { DB_MIGRATIONS_TABLE } from './tables';
const migrationsDir = resolvePackagePath(
'@backstage/backend-tasks',
'migrations',
);
export async function migrateBackendTasks(knex: Knex): Promise<void> {
await knex.migrate.latest({
directory: migrationsDir,
tableName: DB_MIGRATIONS_TABLE,
});
}
@@ -0,0 +1,27 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';
export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';
export type DbTasksRow = {
id: string;
settings_json: string;
next_run_start_at: Date;
current_run_ticket?: string;
current_run_started_at?: Date | string;
current_run_expires_at?: Date | string;
};
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
/**
* Common distributed task management library for Backstage backends
*
* @packageDocumentation
*/
export * from './tasks';

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