diff --git a/.changeset/calm-experts-buy.md b/.changeset/calm-experts-buy.md new file mode 100644 index 0000000000..d95844e298 --- /dev/null +++ b/.changeset/calm-experts-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': minor +--- + +Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory` diff --git a/.changeset/green-actors-argue.md b/.changeset/green-actors-argue.md new file mode 100644 index 0000000000..e21fbb040c --- /dev/null +++ b/.changeset/green-actors-argue.md @@ -0,0 +1,19 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: Removed the following deprecated package commands: + +- `app:build` - Use `package build` instead +- `app:serve` - Use `package start` instead +- `backend:build` - Use `package build` instead +- `backend:bundle` - Use `package build` instead +- `backend:dev` - Use `package start` instead +- `plugin:build` - Use `package build` instead +- `plugin:serve` - Use `package start` instead +- `build` - Use `package build` instead +- `lint` - Use `package lint` instead +- `prepack` - Use `package prepack` instead +- `postpack` - Use `package postpack` instead + +In order to replace these you need to have [migrated to using package roles](https://backstage.io/docs/tutorials/package-role-migration). diff --git a/.changeset/happy-boxes-melt.md b/.changeset/happy-boxes-melt.md new file mode 100644 index 0000000000..c478e783d3 --- /dev/null +++ b/.changeset/happy-boxes-melt.md @@ -0,0 +1,6 @@ +--- +'@techdocs/cli': patch +'@backstage/plugin-techdocs-node': patch +--- + +Fix the flag parsing for `legacyCopyReadmeMdToIndexMd` in `techdocs-cli generate` command, and decouple it's logic from the `techdocs-ref` flag. diff --git a/.changeset/lemon-goats-obey.md b/.changeset/lemon-goats-obey.md new file mode 100644 index 0000000000..7bdb59bd61 --- /dev/null +++ b/.changeset/lemon-goats-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-apache-airflow': minor +--- + +Exposed DagTableComponent as standalone component + added a prop to get only select DAGs instead of the full list diff --git a/.changeset/metal-singers-matter.md b/.changeset/metal-singers-matter.md new file mode 100644 index 0000000000..2c8d370790 --- /dev/null +++ b/.changeset/metal-singers-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Upgrade `@rollup/plugin-node-resolve` to `^13.0.6` diff --git a/.changeset/pretty-masks-live.md b/.changeset/pretty-masks-live.md new file mode 100644 index 0000000000..c45ad1e77f --- /dev/null +++ b/.changeset/pretty-masks-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Added type predicates for all entity types, e.g. isUserEntity diff --git a/.changeset/quiet-pens-notice.md b/.changeset/quiet-pens-notice.md new file mode 100644 index 0000000000..aa6aeb049d --- /dev/null +++ b/.changeset/quiet-pens-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Improve plural handling in logging output for secrets diff --git a/.changeset/serious-zebras-joke.md b/.changeset/serious-zebras-joke.md new file mode 100644 index 0000000000..78f38ca967 --- /dev/null +++ b/.changeset/serious-zebras-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Support displaying subtitle text in `SidebarSubmenuItem` diff --git a/.changeset/sharp-numbers-taste.md b/.changeset/sharp-numbers-taste.md new file mode 100644 index 0000000000..4819aca18f --- /dev/null +++ b/.changeset/sharp-numbers-taste.md @@ -0,0 +1,7 @@ +--- +'@backstage/errors': minor +--- + +The `ResponseError.fromResponse` now accepts a more narrow response type, in order to avoid incompatibilities between different fetch implementations. + +The `response` property of `ResponseError` has also been narrowed to a new `ConsumedResponse` type that omits all the properties for consuming the body of the response. This is not considered a breaking change as it was always an error to try to consume the body of the response. diff --git a/.changeset/shiny-turkeys-doubt.md b/.changeset/shiny-turkeys-doubt.md new file mode 100644 index 0000000000..edac39182c --- /dev/null +++ b/.changeset/shiny-turkeys-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Render namespaces for teams with subtitles in `MyGroupsSidebarItem` diff --git a/.changeset/shy-cameras-develop.md b/.changeset/shy-cameras-develop.md new file mode 100644 index 0000000000..c62a9a56e3 --- /dev/null +++ b/.changeset/shy-cameras-develop.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-openapi': minor +--- + +Add basic OpenAPI \$ref support. + +For more information see [here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-openapi). diff --git a/.changeset/silent-coats-brake.md b/.changeset/silent-coats-brake.md new file mode 100644 index 0000000000..b1cfbacb6a --- /dev/null +++ b/.changeset/silent-coats-brake.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +--- + +Use entity type predicates from catalog-model diff --git a/.changeset/strange-tables-flash.md b/.changeset/strange-tables-flash.md new file mode 100644 index 0000000000..b42069aa64 --- /dev/null +++ b/.changeset/strange-tables-flash.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-stack-overflow': patch +'@backstage/plugin-stack-overflow-backend': patch +--- + +app-config is now picked up properly. diff --git a/.changeset/strong-lies-explain.md b/.changeset/strong-lies-explain.md new file mode 100644 index 0000000000..d9657be6b4 --- /dev/null +++ b/.changeset/strong-lies-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Adds optional `htmlFontSize` property and also sets typography design tokens for h5 and h6 in base theme. diff --git a/.changeset/techdocs-eyes-sit.md b/.changeset/techdocs-eyes-sit.md new file mode 100644 index 0000000000..a09410a4fb --- /dev/null +++ b/.changeset/techdocs-eyes-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Add sample headings on the documented component homepage. diff --git a/.changeset/techdocs-sheep-talk.md b/.changeset/techdocs-sheep-talk.md new file mode 100644 index 0000000000..b17408d7a4 --- /dev/null +++ b/.changeset/techdocs-sheep-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Remove the 60% factor from the font size calculation of headers to use the exact size defined in BackstageTheme. diff --git a/.changeset/twelve-candles-jump.md b/.changeset/twelve-candles-jump.md new file mode 100644 index 0000000000..9d0cccd0ba --- /dev/null +++ b/.changeset/twelve-candles-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Add possibility to resolve relations by RDN, in addition to UUID and DN diff --git a/.changeset/two-crews-accept.md b/.changeset/two-crews-accept.md new file mode 100644 index 0000000000..9540f97cb5 --- /dev/null +++ b/.changeset/two-crews-accept.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Add the possibility in the `GitlabDiscoveryEntityProvider` to scan the whole project instead of concrete groups. For that, use a configuration like this one, where the group parameter is omitted (not mandatory anymore): + +```yaml +catalog: + providers: + gitlab: + yourProviderId: + host: gitlab-host # Identifies one of the hosts set up in the integrations + branch: main # Optional. Uses `master` as default + entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` +``` diff --git a/.changeset/weak-llamas-repeat.md b/.changeset/weak-llamas-repeat.md new file mode 100644 index 0000000000..a86ce3a964 --- /dev/null +++ b/.changeset/weak-llamas-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': minor +--- + +Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory` diff --git a/ADOPTERS.md b/ADOPTERS.md index 44e9dc9b44..e8d2aeed8b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -3,7 +3,7 @@ _If you're using Backstage in your organization, please try to add your company name to this list. This really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ | 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. | @@ -34,9 +34,9 @@ _If you're using Backstage in your organization, please try to add your company | [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 🌕🚀🧑‍🚀 | +| [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), [Kamil Wolny](https://github.com/mrwolny) | 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). | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | 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. | @@ -161,7 +161,7 @@ _If you're using Backstage in your organization, please try to add your company | [OVHcloud](https://www.ovhcloud.com/fr/) | [Jean-Philippe Blary](https://github.com/blaryjp), [Arnaud Bauer](mailto:arnaud.bauer@ovhcloud.com), [Flavien Chantelot](https://github.com/Dorn-) | We're providing Backstage to our collaborators to ease their daily jobs, and let them extends it using plugins. | | [Procter & Gamble](https://us.pg.com/) | [Binita Nayak](https://github.com/binitan), [Josh Rose](https://github.com/joshuarose), [RJ Winkler](https://github.com/rjwink) | P&G leverages Backstage to build internal developer portal to ensure developers' happiness. This developer portal shall act as single source of information needed by development teams to seamlessly create, find and maintain their software components/resources/documentation. | | [SANS Institute](https://www.sans.org) | [Christopher Klewin](mailto:cklewin@sans.org) | Developer portal for centralized visibility, reporting, and tooling across multiple organizations. | -| [Okay](https://www.okayhq.com/) | [Tomas Barreto](mailto:tomas@okayhq.com) | Service catalog, developer portal, and technical documentation | +| [Okay](https://www.okayhq.com/) | [Tomas Barreto](mailto:tomas@okayhq.com) | Service catalog, developer portal, and technical documentation | | [Kaluza](https://www.kaluza.com) | [James Condren](mailto:james.condron@kaluza.com) | To provide an automated golden path to developers, with a focus on discovery and documentation | | [LinkedIn](https://linkedin.com) | [Joshua Lawrence](mailto:jlawrence@linkedin.com) | We are building a platform for internal web tools | | [Forto](https://forto.com) | [Rodolfo Matos](mailto:rodolfo.matos@forto.com) | Still in a experimental phase/assessing the organisational fit. We will be using it mostly a developer portal -- pretty standard use case. | @@ -171,15 +171,16 @@ _If you're using Backstage in your organization, please try to add your company | [AEB](https://www.aeb.com/) | [David Fankhänel](mailto:dfl@aeb.com) | Central developer platform for creating new apps via templates, getting an overview via software catalog, etc | | [SALTO Systems](https://saltosystems.com) | [Ian Cowley](mailto:i.cowley@saltosystems.com) | Currently using Backstage as an internal documentation portal. | | [Lummo](https://lummo.com) | [Anjul Sahu](mailto:anjul@lummo.com) | We are building the internal developer portal using Backstage and bringing up all integrations and service information at one place. | -| [Frontside](https://frontside.com) | [Taras Mankovski](mailto:taras@frontside.com) | +| [Frontside](https://frontside.com) | [Taras Mankovski](mailto:taras@frontside.com) | | | [Stepstone](https://www.stepstone.com/en/) | [Neil Kennedy](mailto:neil.kennedy@stepstone.com) | StepStone is using Backstage to solve problems around ownership and visibility of our applications. We have thousands of repos, multiple legacy systems and a growing platform that is hard to maintain. Backstage is forming the centre of our push to embrace the chaos. | | [idwall](https://idwall.co) | [Rodrigo Catão Araujo](mailto:rodrigo@idwall.co) | Developer Portal for internal engineers to access service catalog, documentation, observability, infrastructure and internal tooling. | | [Jaguar Land Rover](https://www.jaguarlandrover.com) | [Josh Walker](mailto:jwalke18@jaguarlandrover.com) | Users can request a Gitlab user, which creates a commit with the Terraform code. | -| [Glovo](http://glovoapp.com/) | [Yaser Toutah](mailto:yaser.toutah@glovoapp.com) | Developer Portal to improve our Developer Experience, identify ownership and track metadata for our services and tools. It's our Service Catalog. In addition to that, we use it for Service Creation, and much more. | +| [Glovo](http://glovoapp.com/) | [Yaser Toutah](mailto:yaser.toutah@glovoapp.com) | Developer Portal to improve our Developer Experience, identify ownership and track metadata for our services and tools. It's our Service Catalog. In addition to that, we use it for Service Creation, and much more. | | [Dixa](https://dixa.com) | [Jens Møller](mailto:jsc@dixa.com) | We are in early stages, but using it to get overview of our repositories and ownership of these. We want among many things to use it for compliance and easier access to key metrics for our repos. | | [Notino](https://notino.com) | [Jan Remunda](mailto:jan.remunda@notino.com) | Backstage is our developer portal. We use it as service catalog and for technical documentation. | | [Polarpoint](https://polarpoint.io/) | [Surj Bains](https://github.com/polarpoint-io) | We are using Backstage as our Developer portal as well as for hosting our DevOps portal for software catalog. | | [Niche](https://niche.com) | [Zach Romitz](mailto:zach.romitz@niche.com) | We are using the Software Catalog, Software Templates, API documentation, and Techdocs to try and centralize service information. | -| [Mercedes-Benz.io](https://www.mercedes-benz.io/) | [Manuel Santos](https://github.com/manusant) | At Mercedes-Benz we use it as a developer portal with software catalog, TechDocs, Scaffolding and custom plugins. It provides an overview of our tech ecosystem to our product development teams. The portal also serves as a way to foster collaboration among the numerous companies of the Mercedes-Benz Group. -| [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. -| [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices | +| [Mercedes-Benz.io](https://www.mercedes-benz.io/) | [Manuel Santos](https://github.com/manusant) | At Mercedes-Benz we use it as a developer portal with software catalog, TechDocs, Scaffolding and custom plugins. It provides an overview of our tech ecosystem to our product development teams. The portal also serves as a way to foster collaboration among the numerous companies of the Mercedes-Benz Group. | +| [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. | +| [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices | +| [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. | diff --git a/cypress/yarn.lock b/cypress/yarn.lock index 0fc9da99b6..1ccce4ea24 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -304,9 +304,9 @@ cross-spawn@^7.0.0: which "^2.0.1" cypress@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.1.0.tgz#6514a26c721822a02bc194e9a7f72c3142aea174" - integrity sha512-aQ4JVZVib4Xd9FZW8IRZfKelUvqF4y5A+oUbNvn8TlsBmEfIg3m5Xd6Mt6PVU/jHiVJ9Psl905B3ZPnrDcmyuQ== + version "10.2.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" + integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" diff --git a/docs/FAQ.md b/docs/FAQ.md index 96d49ff060..f695a59410 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -223,8 +223,7 @@ For more information, see our No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for -[deploying](https://backstage.io/docs/getting-started/deployment-k8s) and -maintaining it. +[deploying](https://backstage.io/docs/deployment) and maintaining it. ### How secure is Backstage? diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index d14850da77..9440d9a824 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -74,6 +74,12 @@ techdocs: type: 'local' + # Optional when techdocs.publisher.type is set to 'local'. + + local: + # (Optional). Set this to specify where the generated documentation is stored. + publishDirectory: '/path/to/local/directory' + # Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise. googleGcs: diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index cbe84db2e0..8dca898ae7 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -28,7 +28,7 @@ the code. sub-folder which is used for a markdown spellchecker. - [`.yarn/`](https://github.com/backstage/backstage/tree/master/.yarn) - - Backstage ships with it's own `yarn` implementation. This allows us to have + Backstage ships with its own `yarn` implementation. This allows us to have better control over our `yarn.lock` file and hopefully avoid problems due to yarn versioning differences. diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index c607a6550e..6a420462c0 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -45,7 +45,7 @@ catalog: anotherProviderId: # another identifier organization: myorg project: myproject - repository: '*' # this will match all repos starting with service-* + repository: '*' # this will match all repos path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder yetAotherProviderId: # guess, what? Another one :) host: selfhostedazure.yourcompany.com diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index ea151e9415..aad73c8209 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -22,7 +22,7 @@ catalog: yourProviderId: host: gitlab-host # Identifies one of the hosts set up in the integrations branch: main # Optional. Uses `master` as default - group: example-group # Group and subgroup (if needed) to look for repositories + group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` ``` diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 7430f3c200..282906c580 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -135,3 +135,21 @@ package export. accessed via `/beta` or `/alpha` imports. - `@alpha` - here be dragons. Not visible in the main package entry point, alpha exports must be accessed via `/alpha` imports. + +## Node.js Releases + +The Backstage project uses [Node.js](https://nodejs.org/) for both its development +tooling and backend runtime. In order for expectations to be clear we use the +following schedule for determining the [Node.js releases](https://nodejs.org/en/about/releases/) that we support: + +- At any given point in time we support exactly two adjacent even-numbered + releases of Node.js, for example v12 and v14. +- Three months before a Node.js release becomes _Active LTS_ we switch support + to that release and the previous one. This is halfway through the _Current LTS_ + cycle for that release and occurs at the end of July every year. + +When we say _Supporting_ a Node.js release, that means the following: + +- The CI pipeline in the main Backstage repo tests towards the supported releases, and we encourage any other Backstage related projects to do the same. +- New Backstage projects created with `@backstage/create-app` will have their `engines.node` version set accordingly. +- Dropping compatibility with unsupported releases is not considered a breaking change. This includes using new syntax or APIs, as well as bumping dependencies that drop support for these versions. diff --git a/microsite/data/plugins/aws-proton.yaml b/microsite/data/plugins/aws-proton.yaml new file mode 100644 index 0000000000..022b553b24 --- /dev/null +++ b/microsite/data/plugins/aws-proton.yaml @@ -0,0 +1,9 @@ +--- +title: AWS Proton +author: Amazon Web Services +authorUrl: https://aws.amazon.com/ +category: Infrastructure +description: Create and view AWS Proton services for your components in Backstage. +documentation: https://github.com/awslabs/aws-proton-plugins-for-backstage#readme +iconUrl: https://github.com/awslabs/aws-proton-plugins-for-backstage/blob/main/docs/images/proton-logo.png?raw=true +npmPackageName: '@aws/aws-proton-plugin-for-backstage' diff --git a/microsite/data/plugins/gke-usage.yaml b/microsite/data/plugins/gke-usage.yaml index e092f84973..9668b18fdf 100644 --- a/microsite/data/plugins/gke-usage.yaml +++ b/microsite/data/plugins/gke-usage.yaml @@ -5,7 +5,7 @@ authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you the cost and resource usage of your application within Google Kubernetes Engine (GKE). documentation: https://github.com/BESTSELLER/backstage-plugin-gkeusage/blob/master/README.md -iconUrl: https://bestsellerit.com/img/google-container-engine_avatar.svg +iconUrl: img/gke-logo.png npmPackageName: '@bestsellerit/backstage-plugin-gkeusage' tags: - gke diff --git a/microsite/data/plugins/harbor.yaml b/microsite/data/plugins/harbor.yaml index 9023e0c434..f959613d4c 100644 --- a/microsite/data/plugins/harbor.yaml +++ b/microsite/data/plugins/harbor.yaml @@ -5,7 +5,7 @@ authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you information about Docker images within the Harbor cloud native registry. documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master/README.md -iconUrl: https://bestsellerit.com/img/terraform-harbor/goharbor.jpeg +iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/harbor/icon/color/harbor-icon-color.svg npmPackageName: '@bestsellerit/backstage-plugin-harbor' tags: - goharbor diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml new file mode 100644 index 0000000000..bd5dec9dc6 --- /dev/null +++ b/microsite/data/plugins/humanitec.yaml @@ -0,0 +1,11 @@ +--- +title: Humanitec Platform Orchestrator +author: Frontside +authorUrl: 'https://frontside.com' +category: Deployment # A single category e.g. CI, Machine Learning, Services, Monitoring +description: | + Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. + Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. +documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec +iconUrl: img/humanitec-logo.png +npmPackageName: '@frontside/backstage-plugin-humanitec' diff --git a/microsite/static/img/gke-logo.png b/microsite/static/img/gke-logo.png new file mode 100644 index 0000000000..5af1abbf9a Binary files /dev/null and b/microsite/static/img/gke-logo.png differ diff --git a/microsite/static/img/humanitec-logo.png b/microsite/static/img/humanitec-logo.png new file mode 100644 index 0000000000..69ab02e9cd Binary files /dev/null and b/microsite/static/img/humanitec-logo.png differ diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 94b362d99d..608869ea93 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -53,7 +53,9 @@ const updateRedactionList = ( ); logger.info( - `${values.size} secrets found in the config which will be redacted`, + `${values.size} secret${ + values.size > 1 ? 's' : '' + } found in the config which will be redacted`, ); setRootLoggerRedactionList(Array.from(values)); diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 8427ef4112..3eeb054174 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -255,6 +255,36 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; +// @public (undocumented) +export function isApiEntity(entity: Entity): entity is ApiEntityV1alpha1; + +// @public (undocumented) +export function isComponentEntity( + entity: Entity, +): entity is ComponentEntityV1alpha1; + +// @public (undocumented) +export function isDomainEntity(entity: Entity): entity is DomainEntityV1alpha1; + +// @public (undocumented) +export function isGroupEntity(entity: Entity): entity is GroupEntityV1alpha1; + +// @public (undocumented) +export function isLocationEntity( + entity: Entity, +): entity is LocationEntityV1alpha1; + +// @public (undocumented) +export function isResourceEntity( + entity: Entity, +): entity is ResourceEntityV1alpha1; + +// @public (undocumented) +export function isSystemEntity(entity: Entity): entity is SystemEntityV1alpha1; + +// @public (undocumented) +export function isUserEntity(entity: Entity): entity is UserEntityV1alpha1; + // @public export type KindValidator = { check(entity: Entity): Promise; diff --git a/packages/catalog-model/src/entity/conditions.test.ts b/packages/catalog-model/src/entity/conditions.test.ts new file mode 100644 index 0000000000..046f1c60e4 --- /dev/null +++ b/packages/catalog-model/src/entity/conditions.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright 2022 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 { Entity } from './Entity'; +import { + isApiEntity, + isComponentEntity, + isDomainEntity, + isGroupEntity, + isLocationEntity, + isResourceEntity, + isSystemEntity, + isUserEntity, +} from './conditions'; + +const apiEntity: Entity = { + apiVersion: '', + kind: 'api', + metadata: { name: 'api' }, +}; +const componentEntity: Entity = { + apiVersion: '', + kind: 'component', + metadata: { name: 'Component' }, +}; +const domainEntity: Entity = { + apiVersion: '', + kind: 'domain', + metadata: { name: 'Domain' }, +}; +const groupEntity: Entity = { + apiVersion: '', + kind: 'group', + metadata: { name: 'Group' }, +}; +const locationEntity: Entity = { + apiVersion: '', + kind: 'location', + metadata: { name: 'Location' }, +}; +const resourceEntity: Entity = { + apiVersion: '', + kind: 'resource', + metadata: { name: 'Resource' }, +}; +const systemEntity: Entity = { + apiVersion: '', + kind: 'system', + metadata: { name: 'System' }, +}; +const userEntity: Entity = { + apiVersion: '', + kind: 'user', + metadata: { name: 'User' }, +}; + +describe('isApiEntity', () => { + it('should check for the intended type', () => { + expect(isApiEntity(componentEntity)).not.toBeTruthy(); + expect(isApiEntity(domainEntity)).not.toBeTruthy(); + expect(isApiEntity(groupEntity)).not.toBeTruthy(); + expect(isApiEntity(locationEntity)).not.toBeTruthy(); + expect(isApiEntity(resourceEntity)).not.toBeTruthy(); + expect(isApiEntity(systemEntity)).not.toBeTruthy(); + expect(isApiEntity(userEntity)).not.toBeTruthy(); + expect(isApiEntity(apiEntity)).toBeTruthy(); + }); +}); + +describe('isComponentEntity', () => { + it('should check for the intended type', () => { + expect(isComponentEntity(apiEntity)).not.toBeTruthy(); + expect(isComponentEntity(domainEntity)).not.toBeTruthy(); + expect(isComponentEntity(groupEntity)).not.toBeTruthy(); + expect(isComponentEntity(locationEntity)).not.toBeTruthy(); + expect(isComponentEntity(resourceEntity)).not.toBeTruthy(); + expect(isComponentEntity(systemEntity)).not.toBeTruthy(); + expect(isComponentEntity(userEntity)).not.toBeTruthy(); + expect(isComponentEntity(componentEntity)).toBeTruthy(); + }); +}); + +describe('isDomainEntity', () => { + it('should check for the intended type', () => { + expect(isDomainEntity(apiEntity)).not.toBeTruthy(); + expect(isDomainEntity(componentEntity)).not.toBeTruthy(); + expect(isDomainEntity(groupEntity)).not.toBeTruthy(); + expect(isDomainEntity(locationEntity)).not.toBeTruthy(); + expect(isDomainEntity(resourceEntity)).not.toBeTruthy(); + expect(isDomainEntity(systemEntity)).not.toBeTruthy(); + expect(isDomainEntity(userEntity)).not.toBeTruthy(); + expect(isDomainEntity(domainEntity)).toBeTruthy(); + }); +}); + +describe('isGroupEntity', () => { + it('should check for the intended type', () => { + expect(isGroupEntity(apiEntity)).not.toBeTruthy(); + expect(isGroupEntity(componentEntity)).not.toBeTruthy(); + expect(isGroupEntity(domainEntity)).not.toBeTruthy(); + expect(isGroupEntity(locationEntity)).not.toBeTruthy(); + expect(isGroupEntity(resourceEntity)).not.toBeTruthy(); + expect(isGroupEntity(systemEntity)).not.toBeTruthy(); + expect(isGroupEntity(userEntity)).not.toBeTruthy(); + expect(isGroupEntity(groupEntity)).toBeTruthy(); + }); +}); + +describe('isLocationEntity', () => { + it('should check for the intended type', () => { + expect(isLocationEntity(apiEntity)).not.toBeTruthy(); + expect(isLocationEntity(componentEntity)).not.toBeTruthy(); + expect(isLocationEntity(domainEntity)).not.toBeTruthy(); + expect(isLocationEntity(groupEntity)).not.toBeTruthy(); + expect(isLocationEntity(resourceEntity)).not.toBeTruthy(); + expect(isLocationEntity(systemEntity)).not.toBeTruthy(); + expect(isLocationEntity(userEntity)).not.toBeTruthy(); + expect(isLocationEntity(locationEntity)).toBeTruthy(); + }); +}); + +describe('isResourceEntity', () => { + it('should check for the intended type', () => { + expect(isResourceEntity(apiEntity)).not.toBeTruthy(); + expect(isResourceEntity(componentEntity)).not.toBeTruthy(); + expect(isResourceEntity(domainEntity)).not.toBeTruthy(); + expect(isResourceEntity(groupEntity)).not.toBeTruthy(); + expect(isResourceEntity(locationEntity)).not.toBeTruthy(); + expect(isResourceEntity(systemEntity)).not.toBeTruthy(); + expect(isResourceEntity(userEntity)).not.toBeTruthy(); + expect(isResourceEntity(resourceEntity)).toBeTruthy(); + }); +}); + +describe('isSystemEntity', () => { + it('should check for the intended type', () => { + expect(isSystemEntity(apiEntity)).not.toBeTruthy(); + expect(isSystemEntity(componentEntity)).not.toBeTruthy(); + expect(isSystemEntity(domainEntity)).not.toBeTruthy(); + expect(isSystemEntity(groupEntity)).not.toBeTruthy(); + expect(isSystemEntity(locationEntity)).not.toBeTruthy(); + expect(isSystemEntity(resourceEntity)).not.toBeTruthy(); + expect(isSystemEntity(userEntity)).not.toBeTruthy(); + expect(isSystemEntity(systemEntity)).toBeTruthy(); + }); +}); + +describe('isUserEntity', () => { + it('should check for the intended type', () => { + expect(isUserEntity(apiEntity)).not.toBeTruthy(); + expect(isUserEntity(componentEntity)).not.toBeTruthy(); + expect(isUserEntity(domainEntity)).not.toBeTruthy(); + expect(isUserEntity(groupEntity)).not.toBeTruthy(); + expect(isUserEntity(locationEntity)).not.toBeTruthy(); + expect(isUserEntity(resourceEntity)).not.toBeTruthy(); + expect(isUserEntity(systemEntity)).not.toBeTruthy(); + expect(isUserEntity(userEntity)).toBeTruthy(); + }); +}); diff --git a/packages/catalog-model/src/entity/conditions.ts b/packages/catalog-model/src/entity/conditions.ts new file mode 100644 index 0000000000..d761695b5e --- /dev/null +++ b/packages/catalog-model/src/entity/conditions.ts @@ -0,0 +1,76 @@ +/* + * 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 { + ApiEntity, + ComponentEntity, + DomainEntity, + GroupEntity, + LocationEntity, + ResourceEntity, + SystemEntity, + UserEntity, +} from '../kinds'; +import { Entity } from './Entity'; + +/** + * @public + */ +export function isApiEntity(entity: Entity): entity is ApiEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'API'; +} +/** + * @public + */ +export function isComponentEntity(entity: Entity): entity is ComponentEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'COMPONENT'; +} +/** + * @public + */ +export function isDomainEntity(entity: Entity): entity is DomainEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'DOMAIN'; +} +/** + * @public + */ +export function isGroupEntity(entity: Entity): entity is GroupEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; +} +/** + * @public + */ +export function isLocationEntity(entity: Entity): entity is LocationEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'LOCATION'; +} +/** + * @public + */ +export function isResourceEntity(entity: Entity): entity is ResourceEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'RESOURCE'; +} +/** + * @public + */ +export function isSystemEntity(entity: Entity): entity is SystemEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'SYSTEM'; +} +/** + * @public + */ +export function isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; +} diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 45ee673167..039529bf3e 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './conditions'; export { DEFAULT_NAMESPACE, ANNOTATION_EDIT_URL, diff --git a/packages/cli/package.json b/packages/cli/package.json index 2cf6f4fe7b..42e8953d1e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,7 +43,7 @@ "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^13.0.0", + "@rollup/plugin-node-resolve": "^13.0.6", "@rollup/plugin-yaml": "^3.1.0", "@spotify/eslint-config-base": "^13.0.0", "@spotify/eslint-config-react": "^13.0.0", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts deleted file mode 100644 index f90052bc83..0000000000 --- a/packages/cli/src/commands/app/build.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { buildBundle } from '../../lib/bundler'; -import { getEnvironmentParallelism } from '../../lib/parallel'; -import { loadCliConfig } from '../../lib/config'; -import { paths } from '../../lib/paths'; - -export default async (opts: OptionValues) => { - const { name } = await fs.readJson(paths.resolveTarget('package.json')); - await buildBundle({ - entry: 'src/index', - parallelism: getEnvironmentParallelism(), - statsJsonEnabled: opts.stats, - ...(await loadCliConfig({ - args: opts.config, - fromPackage: name, - })), - }); -}; diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts deleted file mode 100644 index e41c2367da..0000000000 --- a/packages/cli/src/commands/app/serve.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 fs from 'fs-extra'; -import chalk from 'chalk'; -import uniq from 'lodash/uniq'; -import { OptionValues } from 'commander'; -import { serveBundle } from '../../lib/bundler'; -import { loadCliConfig } from '../../lib/config'; -import { paths } from '../../lib/paths'; -import { Lockfile } from '../../lib/versioning'; -import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; - -export default async (opts: OptionValues) => { - const lockFilePath = paths.resolveTargetRoot('yarn.lock'); - if (fs.existsSync(lockFilePath)) { - try { - const lockfile = await Lockfile.load(lockFilePath); - const result = lockfile.analyze({ - filter: includedFilter, - }); - const problemPackages = [...result.newVersions, ...result.newRanges] - .map(({ name }) => name) - .filter(name => forbiddenDuplicatesFilter(name)); - - if (problemPackages.length > 0) { - console.log( - chalk.yellow( - `⚠️ Some of the following packages may be outdated or have duplicate installations: - - ${uniq(problemPackages).join(', ')} - `, - ), - ); - console.log( - chalk.yellow( - `⚠️ The following command may fix the issue, but it could also be an issue within one of your dependencies: - - yarn backstage-cli versions:check --fix - `, - ), - ); - } - } catch (error) { - console.log( - chalk.yellow( - `⚠️ Unable to parse yarn.lock file properly: - - ${error} - - skipping analyzer for outdated or duplicate installations - `, - ), - ); - } - } else { - console.log( - chalk.yellow( - `⚠️ Unable to find yarn.lock file: - - skipping analyzer for outdated or duplicate installations - `, - ), - ); - } - - const { name } = await fs.readJson(paths.resolveTarget('package.json')); - const waitForExit = await serveBundle({ - entry: 'src/index', - checksEnabled: opts.check, - ...(await loadCliConfig({ - args: opts.config, - fromPackage: name, - withFilteredKeys: true, - })), - }); - - await waitForExit(); -}; diff --git a/packages/cli/src/commands/backend/bundle.ts b/packages/cli/src/commands/backend/bundle.ts deleted file mode 100644 index 78d4d72d92..0000000000 --- a/packages/cli/src/commands/backend/bundle.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 os from 'os'; -import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; -import tar, { CreateOptions } from 'tar'; -import { OptionValues } from 'commander'; -import { createDistWorkspace } from '../../lib/packager'; -import { paths } from '../../lib/paths'; -import { getEnvironmentParallelism } from '../../lib/parallel'; -import { buildPackage, Output } from '../../lib/builder'; - -const BUNDLE_FILE = 'bundle.tar.gz'; -const SKELETON_FILE = 'skeleton.tar.gz'; - -export default async (opts: OptionValues) => { - const targetDir = paths.resolveTarget('dist'); - const pkg = await fs.readJson(paths.resolveTarget('package.json')); - - // We build the target package without generating type declarations. - await buildPackage({ outputs: new Set([Output.cjs]) }); - - const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle')); - try { - await createDistWorkspace([pkg.name], { - targetDir: tmpDir, - buildDependencies: Boolean(opts.buildDependencies), - buildExcludes: [pkg.name], - parallelism: getEnvironmentParallelism(), - skeleton: SKELETON_FILE, - }); - - // We built the target backend package using the regular build process, but the result of - // that has now been packed into the dist workspace, so clean up the dist dir. - await fs.remove(targetDir); - await fs.mkdir(targetDir); - - // Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice. - await fs.move( - resolvePath(tmpDir, SKELETON_FILE), - resolvePath(targetDir, SKELETON_FILE), - ); - - // Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache. - await tar.create( - { - file: resolvePath(targetDir, BUNDLE_FILE), - cwd: tmpDir, - portable: true, - noMtime: true, - gzip: true, - } as CreateOptions & { noMtime: boolean }, - [''], - ); - } finally { - await fs.remove(tmpDir); - } -}; diff --git a/packages/cli/src/commands/backend/dev.ts b/packages/cli/src/commands/backend/dev.ts deleted file mode 100644 index 2ee43958a2..0000000000 --- a/packages/cli/src/commands/backend/dev.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { paths } from '../../lib/paths'; -import { serveBackend } from '../../lib/bundler/backend'; - -export default async (opts: OptionValues) => { - // Cleaning dist/ before we start the dev process helps work around an issue - // where we end up with the entrypoint executing multiple times, causing - // a port bind conflict among other things. - await fs.remove(paths.resolveTarget('dist')); - - const waitForExit = await serveBackend({ - entry: 'src/index', - checksEnabled: opts.check, - inspectEnabled: opts.inspect, - inspectBrkEnabled: opts.inspectBrk, - }); - - await waitForExit(); -}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 1a7b2c4f08..aca238a32b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -183,53 +183,6 @@ export function registerMigrateCommand(program: Command) { } export function registerCommands(program: Command) { - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('app:build') - .description('Build an app for a production release [DEPRECATED]') - .option('--stats', 'Write bundle stats to output directory') - .option(...configOption) - .action(lazy(() => import('./app/build').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('app:serve') - .description('Serve an app for local development [DEPRECATED]') - .option('--check', 'Enable type checking and linting') - .option(...configOption) - .action(lazy(() => import('./app/serve').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('backend:build') - .description('Build a backend plugin [DEPRECATED]') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./backend/build').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('backend:bundle') - .description('Bundle the backend into a deployment archive [DEPRECATED]') - .option( - '--build-dependencies', - 'Build all local package dependencies before bundling the backend', - ) - .action(lazy(() => import('./backend/bundle').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('backend:dev') - .description( - 'Start local development server with HMR for the backend [DEPRECATED]', - ) - .option('--check', 'Enable type checking and linting') - .option('--inspect', 'Enable debugger') - .option('--inspect-brk', 'Enable debugger with await to attach debugger') - // We don't actually use the config in the CLI, just pass them on to the NodeJS process - .option(...configOption) - .action(lazy(() => import('./backend/dev').then(m => m.default))); - program .command('create') .storeOptionsAsProperties(false) @@ -268,22 +221,6 @@ export function registerCommands(program: Command) { lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('plugin:build') - .description('Build a plugin [DEPRECATED]') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./plugin/build').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('plugin:serve') - .description('Serves the dev/ folder of a plugin [DEPRECATED]') - .option('--check', 'Enable type checking and linting') - .option(...configOption) - .action(lazy(() => import('./plugin/serve').then(m => m.default))); - program .command('plugin:diff') .option('--check', 'Fail if changes are required') @@ -291,27 +228,6 @@ export function registerCommands(program: Command) { .description('Diff an existing plugin with the creation template') .action(lazy(() => import('./plugin/diff').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('build') - .description('Build a package for publishing [DEPRECATED]') - .option('--outputs ', 'List of formats to output [types,cjs,esm]') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./oldBuild').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('lint [directories...]') - .option( - '--format ', - 'Lint report output format', - 'eslint-formatter-friendly', - ) - .option('--fix', 'Attempt to automatically fix violations') - .description('Lint a package [DEPRECATED]') - .action(lazy(() => import('./lint').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of package variant program .command('test') @@ -400,22 +316,6 @@ export function registerCommands(program: Command) { .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('prepack') - .description( - 'Prepares a package for packaging before publishing [DEPRECATED]', - ) - .action(lazy(() => import('./pack').then(m => m.pre))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('postpack') - .description( - 'Restores the changes made by the prepack command [DEPRECATED]', - ) - .action(lazy(() => import('./pack').then(m => m.post))); - // TODO(Rugvip): Deprecate in favor of package variant program .command('clean') diff --git a/packages/cli/src/commands/oldBuild.ts b/packages/cli/src/commands/oldBuild.ts deleted file mode 100644 index c0899d4d02..0000000000 --- a/packages/cli/src/commands/oldBuild.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { buildPackage, Output } from '../lib/builder'; -import { OptionValues } from 'commander'; - -export default async (opts: OptionValues) => { - let outputs = new Set(); - - const { outputs: outputsStr } = opts as { outputs?: string }; - if (outputsStr) { - for (const output of outputsStr.split(',') as (keyof typeof Output)[]) { - if (output in Output) { - outputs.add(Output[output]); - } else { - throw new Error(`Unknown output format: ${output}`); - } - } - } else { - outputs = new Set([Output.types, Output.esm, Output.cjs]); - } - - await buildPackage({ - outputs, - minify: opts.minify, - useApiExtractor: opts.experimentalTypeBuild, - }); -}; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 911ef36867..302ef7ef7d 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1110,6 +1110,7 @@ export type SidebarSubmenuItemDropdownItem = { // @public export type SidebarSubmenuItemProps = { title: string; + subtitle?: string; to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 41f1a1e287..a1565a348f 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -59,6 +59,13 @@ const useStyles = makeStyles( whiteSpace: 'nowrap', overflow: 'hidden', 'text-overflow': 'ellipsis', + lineHeight: 1, + }, + subtitle: { + fontSize: 10, + whiteSpace: 'nowrap', + overflow: 'hidden', + 'text-overflow': 'ellipsis', }, dropdownArrow: { position: 'absolute', @@ -105,6 +112,7 @@ export type SidebarSubmenuItemDropdownItem = { * Holds submenu item content. * * title: Text content of submenu item + * subtitle: A subtitle displayed under the main title * to: Path to navigate to when item is clicked * icon: Icon displayed on the left of text content * dropdownItems: Optional array of dropdown items displayed when submenu item is clicked. @@ -113,6 +121,7 @@ export type SidebarSubmenuItemDropdownItem = { */ export type SidebarSubmenuItemProps = { title: string; + subtitle?: string; to?: string; icon?: IconComponent; dropdownItems?: SidebarSubmenuItemDropdownItem[]; @@ -124,7 +133,7 @@ export type SidebarSubmenuItemProps = { * @public */ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { - const { title, to, icon: Icon, dropdownItems } = props; + const { title, subtitle, to, icon: Icon, dropdownItems } = props; const classes = useStyles(); const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext); const closeSubmenu = () => { @@ -158,6 +167,12 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { {Icon && } {title} +
+ {subtitle && ( + + {subtitle} + + )}
{showDropDown ? ( @@ -210,6 +225,12 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { {Icon && } {title} +
+ {subtitle && ( + + {subtitle} + + )}
diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index a01581135b..aab4e9baec 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -14,6 +14,17 @@ export class AuthenticationError extends CustomErrorBase {} // @public export class ConflictError extends CustomErrorBase {} +// @public +export type ConsumedResponse = { + readonly headers: Headers; + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: ResponseType; + readonly url: string; +}; + // @public export class CustomErrorBase extends Error { constructor(message?: string, cause?: Error | unknown); @@ -67,15 +78,21 @@ export class NotModifiedError extends CustomErrorBase {} // @public export function parseErrorResponseBody( - response: Response, + response: ConsumedResponse & { + text(): Promise; + }, ): Promise; // @public export class ResponseError extends Error { readonly body: ErrorResponseBody; readonly cause: Error; - static fromResponse(response: Response): Promise; - readonly response: Response; + static fromResponse( + response: ConsumedResponse & { + text(): Promise; + }, + ): Promise; + readonly response: ConsumedResponse; } // @public diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts index 84c8137bb5..a26891ef6a 100644 --- a/packages/errors/src/errors/ResponseError.ts +++ b/packages/errors/src/errors/ResponseError.ts @@ -19,6 +19,7 @@ import { ErrorResponseBody, parseErrorResponseBody, } from '../serialization/response'; +import { ConsumedResponse } from './types'; /** * An error thrown as the result of a failed server request. @@ -34,7 +35,7 @@ export class ResponseError extends Error { * Note that the body of this response is always consumed. Its parsed form is * in the `body` field. */ - readonly response: Response; + readonly response: ConsumedResponse; /** * The parsed JSON error body, as sent by the server. @@ -59,7 +60,9 @@ export class ResponseError extends Error { * function consumes the body of the response, and assumes that it hasn't * been consumed before. */ - static async fromResponse(response: Response): Promise { + static async fromResponse( + response: ConsumedResponse & { text(): Promise }, + ): Promise { const data = await parseErrorResponseBody(response); const status = data.response.statusCode || response.status; @@ -77,7 +80,7 @@ export class ResponseError extends Error { private constructor(props: { message: string; - response: Response; + response: ConsumedResponse; data: ErrorResponseBody; cause: Error; }) { diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index bc2fb2aa4e..9e1c2408af 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -27,3 +27,4 @@ export { } from './common'; export { CustomErrorBase } from './CustomErrorBase'; export { ResponseError } from './ResponseError'; +export type { ConsumedResponse } from './types'; diff --git a/packages/errors/src/errors/types.ts b/packages/errors/src/errors/types.ts new file mode 100644 index 0000000000..a62975571f --- /dev/null +++ b/packages/errors/src/errors/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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. + */ + +/** + * ConsumedResponse represents a Response that is known to have been consumed. + * The methods and properties used to read the body contents are therefore omitted. + * + * @public + */ +export type ConsumedResponse = { + readonly headers: Headers; + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: ResponseType; + readonly url: string; +}; diff --git a/packages/errors/src/serialization/response.ts b/packages/errors/src/serialization/response.ts index 3580220525..60894b243e 100644 --- a/packages/errors/src/serialization/response.ts +++ b/packages/errors/src/serialization/response.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ConsumedResponse } from '../errors/types'; import { SerializedError } from './error'; /** @@ -53,7 +54,7 @@ export type ErrorResponseBody = { * @param response - The response of a failed request */ export async function parseErrorResponseBody( - response: Response, + response: ConsumedResponse & { text(): Promise }, ): Promise { try { const text = await response.text(); diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 309b9c7969..1e4ed220d9 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -57,8 +57,8 @@ export default async function generate(opts: OptionValues) { runIn: opts.docker ? 'docker' : 'local', dockerImage, pullImage, - legacyCopyReadmeMdToIndexMd, mkdocs: { + legacyCopyReadmeMdToIndexMd, omitTechdocsCorePlugin, }, }, diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index f650636710..772e868635 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -145,5 +145,6 @@ export type SimpleThemeOptions = { defaultPageTheme: string; pageTheme?: Record; fontFamily?: string; + htmlFontSize?: number; }; ``` diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index da663e3398..5e222c0c3e 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -24,6 +24,7 @@ import { } from './types'; import { pageTheme as defaultPageThemes } from './pageTheme'; +const DEFAULT_HTML_FONT_SIZE = 16; const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; @@ -37,6 +38,7 @@ export function createThemeOptions( ): BackstageThemeOptions { const { palette, + htmlFontSize = DEFAULT_HTML_FONT_SIZE, fontFamily = DEFAULT_FONT_FAMILY, defaultPageTheme, pageTheme = defaultPageThemes, @@ -57,9 +59,17 @@ export function createThemeOptions( }, }, typography: { + htmlFontSize, fontFamily, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, h5: { fontWeight: 700, + fontSize: 24, + marginBottom: 4, }, h4: { fontWeight: 700, diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 3d1a64c38f..b921154005 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -149,6 +149,7 @@ export type SimpleThemeOptions = { defaultPageTheme: string; pageTheme?: Record; fontFamily?: string; + htmlFontSize?: number; }; /** diff --git a/plugins/apache-airflow/README.md b/plugins/apache-airflow/README.md index bdbb1b981f..305a94f081 100644 --- a/plugins/apache-airflow/README.md +++ b/plugins/apache-airflow/README.md @@ -42,6 +42,26 @@ yarn --cwd packages/app add @backstage/plugin-apache-airflow ); ``` +If you just want to embed the DAGs into an existing page, you can use the `ApacheAirflowDagTable` + +```tsx +import { ApacheAirflowDagTable } from '@backstage/plugin-apache-airflow'; + +export function SomeEntityPage(): JSX.Element { + return ( + + + + ); +} +``` + ## Configuration For links to the Airflow instance, the `baseUrl` must be defined in diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index ca151f0734..333e4543cf 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -8,6 +8,13 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; +// @public +export const ApacheAirflowDagTable: ({ + dagIds, + }: { + dagIds?: string[] | undefined; +}) => JSX.Element; + // Warning: (ae-missing-release-tag) "ApacheAirflowPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts index dde8c57802..838292524a 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -25,6 +25,7 @@ export type ApacheAirflowApi = { discoveryApi: DiscoveryApi; baseUrl: string; listDags(options?: { objectsPerRequest: number }): Promise; + getDags(dagIds: string[]): Promise<{ dags: Dag[]; dagsNotFound: string[] }>; updateDag(dagId: string, isPaused: boolean): Promise; getInstanceStatus(): Promise; getInstanceVersion(): Promise; diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts index 704b17eb34..b9bdb02283 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -107,6 +107,15 @@ describe('ApacheAirflowClient', () => { ); }), + rest.get(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { + const { dag_id } = req.params; + const dag = dags.find(d => d.dag_id === dag_id); + if (dag) { + return res(ctx.json(dag)); + } + return res(ctx.status(404)); + }), + rest.patch(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { const { dag_id } = req.params; const body = JSON.parse(req.body as string); @@ -160,4 +169,33 @@ describe('ApacheAirflowClient', () => { expect(response.dag_id).toEqual(dagId); expect(response.is_paused).toEqual(true); }); + + it('get only some dags', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagIds = ['mock_dag_1', 'mock_dag_3']; + const response = await client.getDags(dagIds); + expect(response.dags.length).toEqual(dagIds.length); + response.dags.forEach((dag, index) => + expect(dag.dag_id).toEqual(dagIds[index]), + ); + expect(response.dagsNotFound.length).toEqual(0); + }); + + it('get dags but ignore NOT FOUND errors', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagIds = ['mock_dag_1', 'a-random-DAG-id']; + const response = await client.getDags(dagIds); + expect(response.dags.length).toEqual(1); + expect(response.dags[0].dag_id).toEqual('mock_dag_1'); + expect(response.dagsNotFound.length).toEqual(1); + expect(response.dagsNotFound[0]).toEqual('a-random-DAG-id'); + }); }); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts index 847a1b1ca6..c722f5977d 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -75,6 +75,25 @@ export class ApacheAirflowClient implements ApacheAirflowApi { return dags; } + async getDags( + dagIds: string[], + ): Promise<{ dags: Dag[]; dagsNotFound: string[] }> { + const dagsNotFound: string[] = []; + const response = await Promise.all( + dagIds.map(id => { + return this.fetch(`/dags/${id}`).catch(e => { + if (e.message.toUpperCase('en-US') === 'NOT FOUND') { + dagsNotFound.push(id); + } else { + throw e; + } + }); + }), + ); + const dags = response.filter(Boolean) as Dag[]; + return { dags, dagsNotFound }; + } + async updateDag(dagId: string, isPaused: boolean): Promise { const init = { method: 'PATCH', diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx new file mode 100644 index 0000000000..16ff3120be --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2022 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 { ApacheAirflowApi, apacheAirflowApiRef } from '../../api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { DagTableComponent } from './DagTableComponent'; + +describe('DagTableComponent', () => { + const mockApi: jest.Mocked = { + listDags: jest.fn().mockResolvedValue([ + { + dag_id: 'mock_dag_1', + }, + { + dag_id: 'mock_dag_2', + }, + { + dag_id: 'mock_dag_3', + }, + ]), + getDags: jest.fn().mockResolvedValue({ + dags: [{ dag_id: 'mock_dag_1' }], + dagsNotFound: ['a-random-id'], + }), + } as any; + + it('should render all DAGs', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + ['mock_dag_1', 'mock_dag_2', 'mock_dag_3'].forEach(dagId => { + expect(getByText(dagId)).toBeInTheDocument(); + }); + }); + + it('should render only selected DAGs', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + expect(getByText('mock_dag_1')).toBeInTheDocument(); + expect(getByText('Warning: 1 DAGs were not found')).toBeInTheDocument(); + expect(getByText('a-random-id')).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index cf343a596e..2514d70d45 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -20,6 +20,7 @@ import { StatusOK, Table, TableColumn, + WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import Box from '@material-ui/core/Box'; @@ -129,9 +130,18 @@ export const DenseTable = ({ dags }: DenseTableProps) => { ); }; -export const DagTableComponent = () => { +type DagTableComponentProps = { + dagIds?: string[]; +}; + +export const DagTableComponent = ({ dagIds }: DagTableComponentProps) => { const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = useAsync(async (): Promise => { + if (dagIds) { + const { dags } = await apiClient.getDags(dagIds); + return dags; + } return await apiClient.listDags(); }, []); @@ -146,6 +156,22 @@ export const DagTableComponent = () => { id: el.dag_id, // table records require `id` attribute dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` })); - - return ; + const dagsNotFound = + dagIds && value + ? dagIds.filter(id => !value.find(d => d.dag_id === id)) + : []; + return ( + <> + {dagsNotFound.length ? ( + + {dagsNotFound.map(dagId => ( + {dagId} + ))} + + ) : ( + '' + )} + + + ); }; diff --git a/plugins/apache-airflow/src/index.ts b/plugins/apache-airflow/src/index.ts index 7186839c0a..e8353c7760 100644 --- a/plugins/apache-airflow/src/index.ts +++ b/plugins/apache-airflow/src/index.ts @@ -20,4 +20,8 @@ * @packageDocumentation */ -export { apacheAirflowPlugin, ApacheAirflowPage } from './plugin'; +export { + apacheAirflowPlugin, + ApacheAirflowPage, + ApacheAirflowDagTable, +} from './plugin'; diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts index 04ef1974d4..664cde36cc 100644 --- a/plugins/apache-airflow/src/plugin.ts +++ b/plugins/apache-airflow/src/plugin.ts @@ -17,11 +17,12 @@ import { rootRouteRef } from './routes'; import { apacheAirflowApiRef, ApacheAirflowClient } from './api'; import { + configApiRef, createApiFactory, + createComponentExtension, createPlugin, createRoutableExtension, discoveryApiRef, - configApiRef, } from '@backstage/core-plugin-api'; export const apacheAirflowPlugin = createPlugin({ @@ -49,3 +50,20 @@ export const ApacheAirflowPage = apacheAirflowPlugin.provide( mountPoint: rootRouteRef, }), ); + +/** + * Render the DAGs in a table + * If the dagIds is specified, only those DAGs are loaded. + * Otherwise, it's going to list all the DAGs + * @public + * @param dagIds - optional string[] of the DAGs to show in the table. If undefined, it will list all DAGs + */ +export const ApacheAirflowDagTable = apacheAirflowPlugin.provide( + createComponentExtension({ + name: 'ApacheAirflowDagTable', + component: { + lazy: () => + import('./components/DagTableComponent').then(m => m.DagTableComponent), + }, + }), +); diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 5b0d42c81d..82f1a5db7c 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -32,10 +32,11 @@ export interface Config { */ host: string; /** - * (Required) Gitlab's group[/subgroup] where the discovery is done. + * (Optional) Gitlab's group[/subgroup] where the discovery is done. + * If not defined the whole project will be scanned. * @visibility backend */ - group: string; + group?: string; /** * (Optional) Default branch to read the catalog-info.yaml file. * If not set, 'master' will be used. diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index d28930b151..892ce5f25c 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -100,7 +100,26 @@ describe('config', () => { }); expect(() => readGitlabConfigs(config)).toThrow( - "Missing required config value at 'catalog.providers.gitlab.test.group'", + "Missing required config value at 'catalog.providers.gitlab.test.host'", ); }); + + it('read full gitlab project', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + host: 'host', + branch: 'main', + }, + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + expect(result[0].group).toEqual(''); + }); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index f7eb316997..d6ad28b2ca 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -25,7 +25,7 @@ import { GitlabProviderConfig } from '../lib/types'; * @param config - The config object to extract from */ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { - const group = config.getString('group'); + const group = config.getOptionalString('group') ?? ''; const host = config.getString('host'); const branch = config.getOptionalString('branch') ?? 'master'; const catalogFile = diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts index 410ac847b3..49803b9c39 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -345,206 +345,128 @@ describe('readLdapGroups', () => { describe('resolveRelations', () => { describe('lookup', () => { - it('matches by DN', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca'])], - ]); - resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); - - it('matches by UUID', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca'])], - ]); - resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'matches by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const groupMember = new Map>([ + ['pa', new Set(['ca'])], + ]); + resolveRelations( + [parent, child], + [], + new Map(), + new Map(), + groupMember, + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + }, + ); }); describe('userMemberOf', () => { - it('populates relations by dn', () => { - const host = group({ - metadata: { name: 'host', annotations: { [LDAP_DN_ANNOTATION]: 'ha' } }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const userMemberOf = new Map>([ - ['ma', new Set(['ha'])], - ]); - resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['group:default/host']); - }); - - it('populates relations by uuid', () => { - const host = group({ - metadata: { - name: 'host', - annotations: { [LDAP_UUID_ANNOTATION]: 'ha' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const userMemberOf = new Map>([ - ['ma', new Set(['ha'])], - ]); - resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['group:default/host']); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const host = group({ + metadata: { name: 'host', annotations: { [annotation]: 'ha' } }, + }); + const member = user({ + metadata: { + name: 'member', + annotations: { [annotation]: 'ma' }, + }, + }); + const userMemberOf = new Map>([ + ['ma', new Set(['ha'])], + ]); + resolveRelations([host], [member], userMemberOf, new Map(), new Map()); + expect(member.spec.memberOf).toEqual(['group:default/host']); + }, + ); }); describe('groupMemberOf', () => { - it('populates relations by dn', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const groupMemberOf = new Map>([ - ['ca', new Set(['pa'])], - ]); - resolveRelations( - [parent, child], - [], - new Map(), - groupMemberOf, - new Map(), - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - }); - }); - - it('populates relations by uuid', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const groupMemberOf = new Map>([ + ['ca', new Set(['pa'])], + ]); + resolveRelations( + [parent, child], + [], + new Map(), + groupMemberOf, + new Map(), + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const groupMemberOf = new Map>([ - ['ca', new Set(['pa'])], - ]); - resolveRelations([parent, child], [], new Map(), groupMemberOf, new Map()); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); + ); }); describe('groupMember', () => { - it('populates relations by dn', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_DN_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_DN_ANNOTATION]: 'ca' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_DN_ANNOTATION]: 'ma' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca', 'ma'])], - ]); - resolveRelations( - [parent, child], - [member], - new Map(), - new Map(), - groupMember, - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - expect(member.spec.memberOf).toEqual(['group:default/parent']); - }); - - it('populates relations by uuid', () => { - const parent = group({ - metadata: { - name: 'parent', - annotations: { [LDAP_UUID_ANNOTATION]: 'pa' }, - }, - }); - const child = group({ - metadata: { - name: 'child', - annotations: { [LDAP_UUID_ANNOTATION]: 'ca' }, - }, - }); - const member = user({ - metadata: { - name: 'member', - annotations: { [LDAP_UUID_ANNOTATION]: 'ma' }, - }, - }); - const groupMember = new Map>([ - ['pa', new Set(['ca', 'ma'])], - ]); - resolveRelations( - [parent, child], - [member], - new Map(), - new Map(), - groupMember, - ); - expect(parent.spec.children).toEqual(['group:default/child']); - expect(child.spec.parent).toEqual('group:default/parent'); - expect(member.spec.memberOf).toEqual(['group:default/parent']); - }); + it.each([LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION])( + 'populates relations by %s', + annotation => { + const parent = group({ + metadata: { + name: 'parent', + annotations: { [annotation]: 'pa' }, + }, + }); + const child = group({ + metadata: { + name: 'child', + annotations: { [annotation]: 'ca' }, + }, + }); + const member = user({ + metadata: { + name: 'member', + annotations: { [annotation]: 'ma' }, + }, + }); + const groupMember = new Map>([ + ['pa', new Set(['ca', 'ma'])], + ]); + resolveRelations( + [parent, child], + [member], + new Map(), + new Map(), + groupMember, + ); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + expect(member.spec.memberOf).toEqual(['group:default/parent']); + }, + ); }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index d0fe30e0e6..420ba141fc 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -365,11 +365,13 @@ export function resolveRelations( for (const user of users) { userMap.set(stringifyEntityRef(user), user); userMap.set(user.metadata.annotations![LDAP_DN_ANNOTATION], user); + userMap.set(user.metadata.annotations![LDAP_RDN_ANNOTATION], user); userMap.set(user.metadata.annotations![LDAP_UUID_ANNOTATION], user); } for (const group of groups) { groupMap.set(stringifyEntityRef(group), group); groupMap.set(group.metadata.annotations![LDAP_DN_ANNOTATION], group); + groupMap.set(group.metadata.annotations![LDAP_RDN_ANNOTATION], group); groupMap.set(group.metadata.annotations![LDAP_UUID_ANNOTATION], group); } diff --git a/plugins/catalog-backend-module-openapi/.eslintrc.js b/plugins/catalog-backend-module-openapi/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-openapi/README.md b/plugins/catalog-backend-module-openapi/README.md new file mode 100644 index 0000000000..1a133521c6 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/README.md @@ -0,0 +1,27 @@ +# Catalog Backend Module for OpenAPI specifications + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at OpenAPI specifications. + +With this you can split your OpenAPI definition into multiple files and reference them. They will be bundled, using an UrlReader, during processing and stored as a single specification. + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-openapi +``` + +### Adding the plugin to your `packages/backend` + +The processor can be added by importing `OpenApiRefProcessor` in `src/plugins/catalog.ts` in your `backend` package and adding the following. + +```ts +builder.addProcessor( + OpenApiRefProcessor.fromConfig(env.config, { + logger: env.logger, + reader: env.reader, + }), +); +``` diff --git a/plugins/catalog-backend-module-openapi/api-report.md b/plugins/catalog-backend-module-openapi/api-report.md new file mode 100644 index 0000000000..92ddd1e3cd --- /dev/null +++ b/plugins/catalog-backend-module-openapi/api-report.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-openapi" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { ScmIntegrations } from '@backstage/integration'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export class OpenApiRefProcessor implements CatalogProcessor { + constructor(options: { + integrations: ScmIntegrations; + logger: Logger; + reader: UrlReader; + }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger; + reader: UrlReader; + }, + ): OpenApiRefProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + preProcessEntity(entity: Entity, location: LocationSpec): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json new file mode 100644 index 0000000000..1c99618ff8 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-openapi", + "description": "A Backstage catalog backend module that helps with OpenAPI specifications", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-openapi" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@backstage/backend-common": "^0.14.1-next.0", + "@backstage/catalog-model": "^1.1.0-next.0", + "@backstage/config": "^1.0.1", + "@backstage/integration": "^1.2.2-next.0", + "@backstage/plugin-catalog-backend": "^1.2.1-next.0", + "winston": "^3.2.1", + "yaml": "^2.1.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.26-next.0", + "@backstage/cli": "^0.17.3-next.0", + "openapi-types": "^11.0.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts new file mode 100644 index 0000000000..8f2cc571c2 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { OpenApiRefProcessor } from './OpenApiRefProcessor'; +import { bundleOpenApiSpecification } from './lib'; + +jest.mock('./lib', () => ({ + bundleOpenApiSpecification: jest.fn(), +})); + +const bundledSpecification = ''; + +describe('OpenApiRefProcessor', () => { + const mockLocation = (): LocationSpec => ({ + type: 'url', + target: `https://github.com/owner/repo/blob/main/catalog-info.yaml`, + }); + + beforeEach(() => { + (bundleOpenApiSpecification as any).mockResolvedValue(bundledSpecification); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('preProcessEntity', () => { + const setupTest = ({ kind = 'API', spec = {} } = {}) => { + const entity = { + kind, + spec: { definition: '', ...spec }, + }; + const config = new ConfigReader({}); + const reader = { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + const processor = OpenApiRefProcessor.fromConfig(config, { + logger: getVoidLogger(), + reader, + }); + + return { entity, processor }; + }; + + it('should bundle OpenAPI specifications', async () => { + const { entity, processor } = setupTest({ + kind: 'API', + spec: { type: 'openapi' }, + }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result.spec?.definition).toEqual(bundledSpecification); + }); + + it('should ignore other kinds', async () => { + const { entity, processor } = setupTest({ kind: 'Group' }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result).toEqual(entity); + }); + + it('should ignore other specification types', async () => { + const { entity, processor } = setupTest({ + kind: 'Group', + spec: { type: 'asyncapi' }, + }); + + const result = await processor.preProcessEntity( + entity as any, + mockLocation(), + ); + + expect(result).toEqual(entity); + }); + }); +}); diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts new file mode 100644 index 0000000000..01875e0779 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts @@ -0,0 +1,94 @@ +/* + * 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 { UrlReader } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { + CatalogProcessor, + LocationSpec, +} from '@backstage/plugin-catalog-backend'; +import { bundleOpenApiSpecification } from './lib'; +import { Logger } from 'winston'; + +/** @public */ +export class OpenApiRefProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + private readonly reader: UrlReader; + + static fromConfig( + config: Config, + options: { logger: Logger; reader: UrlReader }, + ) { + const integrations = ScmIntegrations.fromConfig(config); + + return new OpenApiRefProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { + integrations: ScmIntegrations; + logger: Logger; + reader: UrlReader; + }) { + this.integrations = options.integrations; + this.logger = options.logger; + this.reader = options.reader; + } + + getProcessorName(): string { + return 'OpenApiRefProcessor'; + } + + async preProcessEntity( + entity: Entity, + location: LocationSpec, + ): Promise { + if ( + !entity || + entity.kind !== 'API' || + (entity.spec && entity.spec.type !== 'openapi') + ) { + return entity; + } + + const scmIntegration = this.integrations.byUrl(location.target); + if (!scmIntegration) { + return entity; + } + + this.logger.debug(`Bundling OpenAPI specification from ${location.target}`); + try { + const bundledSpec = await bundleOpenApiSpecification( + entity.spec!.definition?.toString(), + location.target, + this.reader, + scmIntegration, + ); + + return { + ...entity, + spec: { ...entity.spec, definition: bundledSpec }, + }; + } catch (error) { + this.logger.error(`Unable to bundle OpenAPI specification`, error); + return entity; + } + } +} diff --git a/packages/cli/src/commands/backend/build.ts b/plugins/catalog-backend-module-openapi/src/index.ts similarity index 66% rename from packages/cli/src/commands/backend/build.ts rename to plugins/catalog-backend-module-openapi/src/index.ts index c85eecfed3..f98bee6bac 100644 --- a/packages/cli/src/commands/backend/build.ts +++ b/plugins/catalog-backend-module-openapi/src/index.ts @@ -13,14 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { OptionValues } from 'commander'; -import { buildPackage, Output } from '../../lib/builder'; - -export default async (opts: OptionValues) => { - await buildPackage({ - outputs: new Set([Output.cjs, Output.types]), - minify: opts.minify, - useApiExtractor: opts.experimentalTypeBuild, - }); -}; +export { OpenApiRefProcessor } from './OpenApiRefProcessor'; diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts new file mode 100644 index 0000000000..4505f158b5 --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { bundleOpenApiSpecification } from './bundle'; + +const specification = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + $ref: "./paths/pets/list.yaml" +`; + +const list = ` +--- +summary: List all pets +operationId: listPets +tags: + - pets +responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + type: string +`; + +const expectedResult = ` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + responses: + "200": + description: A paged array of pets + content: + application/json: + schema: + type: string +`; + +describe('bundleOpenApiSpecification', () => { + const readUrl = jest.fn(); + const reader = { + readUrl, + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const scmIntegration = ScmIntegrations.fromConfig(new ConfigReader({})).byUrl( + 'https://github.com/owner/repo/blob/main/openapi.yaml', + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return undefined if no specification is supplied', async () => { + expect( + await bundleOpenApiSpecification( + undefined, + 'https://github.com/owner/repo/blob/main/openapi.yaml', + reader, + scmIntegration as any, + ), + ).toBeUndefined(); + }); + + it('should return the bundled specification', async () => { + readUrl.mockResolvedValue({ + buffer: jest.fn().mockResolvedValue(list), + }); + + const result = await bundleOpenApiSpecification( + specification, + 'https://github.com/owner/repo/blob/main/openapi.yaml', + reader, + scmIntegration as any, + ); + + expect(result).toEqual(expectedResult.trimStart()); + }); +}); diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts new file mode 100644 index 0000000000..3d35429d8b --- /dev/null +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -0,0 +1,69 @@ +/* + * 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 { UrlReader } from '@backstage/backend-common'; +import { ScmIntegration } from '@backstage/integration'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import { parse, stringify } from 'yaml'; +import * as path from 'path'; + +const protocolPattern = /^(\w{2,}):\/\//i; +const getProtocol = (refPath: string) => { + const match = protocolPattern.exec(refPath); + if (match) { + return match[1].toLowerCase(); + } + return undefined; +}; + +export async function bundleOpenApiSpecification( + specification: string | undefined, + targetUrl: string, + reader: UrlReader, + scmIntegration: ScmIntegration, +): Promise { + const fileUrlReaderResolver: SwaggerParser.ResolverOptions = { + canRead: file => { + const protocol = getProtocol(file.url); + return protocol === undefined || protocol === 'file'; + }, + read: async file => { + const relativePath = path.relative('.', file.url); + const url = scmIntegration.resolveUrl({ + base: targetUrl, + url: relativePath, + }); + if (reader.readUrl) { + const data = await reader.readUrl(url); + return data.buffer(); + } + throw new Error('UrlReader has no readUrl method defined'); + }, + }; + + if (!specification) { + return undefined; + } + + const options: SwaggerParser.Options = { + resolve: { + file: fileUrlReaderResolver, + http: true, + }, + }; + const specObject = parse(specification); + const bundledSpec = await SwaggerParser.bundle(specObject, options); + return stringify(bundledSpec); +} diff --git a/packages/cli/src/commands/plugin/serve.ts b/plugins/catalog-backend-module-openapi/src/lib/index.ts similarity index 50% rename from packages/cli/src/commands/plugin/serve.ts rename to plugins/catalog-backend-module-openapi/src/lib/index.ts index 891db190e0..805569181a 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/index.ts @@ -13,24 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { serveBundle } from '../../lib/bundler'; -import { loadCliConfig } from '../../lib/config'; -import { paths } from '../../lib/paths'; - -export default async (opts: OptionValues) => { - const { name } = await fs.readJson(paths.resolveTarget('package.json')); - const waitForExit = await serveBundle({ - entry: 'dev/index', - checksEnabled: opts.check, - ...(await loadCliConfig({ - args: opts.config, - fromPackage: name, - withFilteredKeys: true, - })), - }); - - await waitForExit(); -}; +export * from './bundle'; diff --git a/packages/cli/src/commands/plugin/build.ts b/plugins/catalog-backend-module-openapi/src/setupTests.ts similarity index 66% rename from packages/cli/src/commands/plugin/build.ts rename to plugins/catalog-backend-module-openapi/src/setupTests.ts index 671aece818..d3232290a7 100644 --- a/packages/cli/src/commands/plugin/build.ts +++ b/plugins/catalog-backend-module-openapi/src/setupTests.ts @@ -14,13 +14,4 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; -import { buildPackage, Output } from '../../lib/builder'; - -export default async (opts: OptionValues) => { - await buildPackage({ - outputs: new Set([Output.esm, Output.types]), - minify: opts.minify, - useApiExtractor: opts.experimentalTypeBuild, - }); -}; +export {}; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index f895ded85e..70ec0ee948 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -20,8 +20,8 @@ import { } from '@backstage/backend-common'; import { Entity, + isUserEntity, stringifyEntityRef, - UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -93,13 +93,9 @@ export class DefaultCatalogCollator { return formatted.toLowerCase(); } - private isUserEntity(entity: Entity): entity is UserEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'USER'; - } - private getDocumentText(entity: Entity): string { let documentText = entity.metadata.description || ''; - if (this.isUserEntity(entity)) { + if (isUserEntity(entity)) { if (entity.spec?.profile?.displayName && documentText) { // combine displayName and description const displayName = entity.spec?.profile?.displayName; diff --git a/plugins/catalog-backend/src/search/util.ts b/plugins/catalog-backend/src/search/util.ts index b5df2a28a7..fd54d397c9 100644 --- a/plugins/catalog-backend/src/search/util.ts +++ b/plugins/catalog-backend/src/search/util.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { Entity, UserEntity, GroupEntity } from '@backstage/catalog-model'; - -function isUserEntity(entity: Entity): entity is UserEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'USER'; -} - -function isGroupEntity(entity: Entity): entity is GroupEntity { - return entity.kind.toLocaleUpperCase('en-US') === 'GROUP'; -} +import { Entity, isUserEntity, isGroupEntity } from '@backstage/catalog-model'; export function getDocumentText(entity: Entity): string { const documentTexts: string[] = []; diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index 5f50fde5c9..86fd38a900 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, ComponentEntity } from '@backstage/catalog-model'; +import { ComponentEntity, Entity } from '@backstage/catalog-model'; function strCmp(a: string | undefined, b: string | undefined): boolean { return Boolean( diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index b27b050a50..de8a1638ad 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -5,6 +5,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "backstage": { + "role": "backend-plugin" + }, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 1e90afb13a..fce0a05714 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -11,7 +11,7 @@ "types": "dist/index.d.ts" }, "backstage": { - "role": "frontend-plugin" + "role": "common-library" }, "scripts": { "start": "backstage-cli package start", diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 4c5fb0c83e..7764d7c2cb 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -5,6 +5,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "backstage": { + "role": "frontend-plugin" + }, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx index fa8f8405ff..f3e3a6bd9a 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx @@ -15,7 +15,10 @@ */ import React from 'react'; -import { stringifyEntityRef } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { SidebarItem, SidebarSubmenu, @@ -32,7 +35,6 @@ import { catalogApiRef, CatalogApi, entityRouteRef, - humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { getCompoundEntityRef } from '@backstage/catalog-model'; @@ -94,9 +96,11 @@ export const MyGroupsSidebarItem = (props: { {groups?.map(function groupsMap(group) { return ( ({ - ...jest.requireActual('@backstage/core-plugin-api'), -})); +const createInitialState = ({ + term = '', + filters = {}, + types = ['*'], + pageCursor = '', +} = {}) => ({ + term, + filters, + types, + pageCursor, +}); describe('SearchBar', () => { - const initialState = { - term: '', - filters: {}, - types: ['*'], - pageCursor: '', - }; + const query = jest.fn().mockResolvedValue({ results: [] }); - const query = jest.fn().mockResolvedValue({}); - const analyticsApiSpy = new MockAnalyticsApi(); - let apiRegistry: TestApiRegistry; + const analyticsApiMock = new MockAnalyticsApi(); - apiRegistry = TestApiRegistry.from( - [ - configApiRef, - new ConfigReader({ - app: { title: 'Mock title' }, - }), - ], - [searchApiRef, { query }], - ); + const configApiMock = new ConfigReader({ + app: { title: 'Mock title' }, + }); - const name = 'Search'; - const term = 'term'; + const searchApiMock = { query }; - afterAll(() => { - jest.resetAllMocks(); + beforeEach(() => { + jest.clearAllMocks(); }); it('Renders without exploding', async () => { - render( - - + await renderWithEffects( + + - , + , ); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); expect( screen.getByPlaceholderText('Search in Mock title'), ).toBeInTheDocument(); @@ -77,59 +77,72 @@ describe('SearchBar', () => { }); it('Renders with custom placeholder', async () => { - render( - - - + const placeholder = 'placeholder'; + + await renderWithEffects( + + + - , - , + , ); await waitFor(() => { - expect( - screen.getByPlaceholderText('This is a custom placeholder'), - ).toBeInTheDocument(); + expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument(); }); }); it('Renders based on initial search', async () => { + const term = 'term'; + render( - - + + - , - , + , ); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); + expect(screen.getByRole('textbox', { name: 'Search' })).toHaveValue(term); }); }); it('Updates term state when text is entered', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); - const defaultDebounceTime = 200; + const user = userEvent.setup({ delay: null }); - render( - - + await renderWithEffects( + + - , - , + , ); - const textbox = screen.getByRole('textbox', { name }); + const textbox = screen.getByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(defaultDebounceTime); + jest.advanceTimersByTime(200); }); await waitFor(() => { @@ -139,26 +152,36 @@ describe('SearchBar', () => { expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); + jest.useRealTimers(); }); it('Clear button clears term state', async () => { - render( - - + const term = 'term'; + + await renderWithEffects( + + - , + , ); + const textbox = screen.getByRole('textbox', { name: 'Search' }); + await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); + expect(textbox).toHaveValue(term); }); await userEvent.click(screen.getByRole('button', { name: 'Clear' })); await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(''); + expect(textbox).toHaveValue(''); }); expect(query).toHaveBeenLastCalledWith( @@ -167,12 +190,19 @@ describe('SearchBar', () => { }); it('Should not show clear button', async () => { - render( - - + const term = 'term'; + + await renderWithEffects( + + - , + , ); await waitFor(() => { @@ -183,170 +213,168 @@ describe('SearchBar', () => { }); it('Adheres to provided debounceTime', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); - + const user = userEvent.setup({ delay: null }); const debounceTime = 600; - render( - - + await renderWithEffects( + + - , - , + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); - expect(query).not.toHaveBeenLastCalledWith( - expect.objectContaining({ term: value }), - ); + await waitFor(() => { + expect(query).not.toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + }); act(() => { jest.advanceTimersByTime(debounceTime); }); - expect(textbox).toHaveValue(value); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ term: value }), ); + jest.useRealTimers(); }); it('does not capture analytics event if not enabled in app', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); + const user = userEvent.setup({ delay: null }); - const debounceTime = 600; - - render( - - - + await renderWithEffects( + + + - , - , + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); const value = 'value'; await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); - await waitFor(() => expect(textbox).toHaveValue(value)); + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(analyticsApiMock.getEvents()).toHaveLength(0); - expect(analyticsApiSpy.getEvents()).toHaveLength(0); jest.useRealTimers(); }); it('captures analytics events if enabled in app', async () => { - const user = userEvent.setup({ delay: null }); jest.useFakeTimers(); + const user = userEvent.setup({ delay: null }); + const types = ['techdocs', 'software-catalog']; - const debounceTime = 600; - - apiRegistry = TestApiRegistry.from( - [analyticsApiRef, analyticsApiSpy], - [ - configApiRef, - new ConfigReader({ - app: { - title: 'Mock title', - analytics: { - ga: { - trackingId: 'xyz123', + await renderWithEffects( + - - + }), + ], + ]} + > + + - , + , ); - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); + const textbox = await screen.findByRole('textbox', { name: 'Search' }); - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; + let value = 'value'; await user.type(textbox, value); - expect(analyticsApiSpy.getEvents()).toHaveLength(0); + expect(analyticsApiMock.getEvents()).toHaveLength(0); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiSpy.getEvents()).toHaveLength(1); - expect(analyticsApiSpy.getEvents()[0]).toEqual({ + expect(analyticsApiMock.getEvents()).toHaveLength(1); + expect(analyticsApiMock.getEvents()[0]).toEqual({ action: 'search', context: { extension: 'SearchBar', pluginId: 'search', routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', + searchTypes: types.toString(), }, - subject: 'value', + subject: value, }); await user.clear(textbox); + value = 'new value'; + // make sure new term is captured - await user.type(textbox, 'new value'); + await user.type(textbox, value); act(() => { - jest.advanceTimersByTime(debounceTime); + jest.advanceTimersByTime(200); }); - await waitFor(() => expect(textbox).toHaveValue('new value')); + await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiSpy.getEvents()).toHaveLength(2); - expect(analyticsApiSpy.getEvents()[1]).toEqual({ + expect(analyticsApiMock.getEvents()).toHaveLength(2); + expect(analyticsApiMock.getEvents()[1]).toEqual({ action: 'search', context: { extension: 'SearchBar', pluginId: 'search', routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', + searchTypes: types.toString(), }, - subject: 'new value', + subject: value, }); + jest.useRealTimers(); }); }); diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx deleted file mode 100644 index c48c3d3ed2..0000000000 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ /dev/null @@ -1,353 +0,0 @@ -/* - * 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 { screen, render, waitFor, act } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; -import { - SearchContextProvider, - searchApiRef, -} from '@backstage/plugin-search-react'; - -import { SearchBar } from './SearchBar'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), -})); - -describe('SearchBar', () => { - const initialState = { - term: '', - filters: {}, - types: ['*'], - pageCursor: '', - }; - - const query = jest.fn().mockResolvedValue({}); - const analyticsApiSpy = new MockAnalyticsApi(); - let apiRegistry: TestApiRegistry; - - apiRegistry = TestApiRegistry.from( - [ - configApiRef, - new ConfigReader({ - app: { title: 'Mock title' }, - }), - ], - [searchApiRef, { query }], - ); - - const name = 'Search'; - const term = 'term'; - - afterAll(() => { - jest.resetAllMocks(); - }); - - it('Renders without exploding', async () => { - render( - - - - - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - expect( - screen.getByPlaceholderText('Search in Mock title'), - ).toBeInTheDocument(); - }); - }); - - it('Renders with custom placeholder', async () => { - render( - - - - - , - , - ); - - await waitFor(() => { - expect( - screen.getByPlaceholderText('This is a custom placeholder'), - ).toBeInTheDocument(); - }); - }); - - it('Renders based on initial search', async () => { - render( - - - - - , - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); - }); - }); - - it('Updates term state when text is entered', async () => { - const user = userEvent.setup({ delay: null }); - jest.useFakeTimers(); - const defaultDebounceTime = 200; - - render( - - - - - , - , - ); - - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; - - await user.type(textbox, value); - - act(() => { - jest.advanceTimersByTime(defaultDebounceTime); - }); - - await waitFor(() => { - expect(textbox).toHaveValue(value); - }); - - expect(query).toHaveBeenLastCalledWith( - expect.objectContaining({ term: value }), - ); - jest.useRealTimers(); - }); - - it('Clear button clears term state', async () => { - render( - - - - - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(term); - }); - - await userEvent.click(screen.getByRole('button', { name: 'Clear' })); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toHaveValue(''); - }); - - expect(query).toHaveBeenLastCalledWith( - expect.objectContaining({ term: '' }), - ); - }); - - it('Should not show clear button', async () => { - render( - - - - - , - ); - - await waitFor(() => { - expect( - screen.queryByRole('button', { name: 'Clear' }), - ).not.toBeInTheDocument(); - }); - }); - - it('Adheres to provided debounceTime', async () => { - const user = userEvent.setup({ delay: null }); - jest.useFakeTimers(); - - const debounceTime = 600; - - render( - - - - - , - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; - - await user.type(textbox, value); - - expect(query).not.toHaveBeenLastCalledWith( - expect.objectContaining({ term: value }), - ); - - act(() => { - jest.advanceTimersByTime(debounceTime); - }); - expect(textbox).toHaveValue(value); - - expect(query).toHaveBeenLastCalledWith( - expect.objectContaining({ term: value }), - ); - jest.useRealTimers(); - }); - - it('does not capture analytics event if not enabled in app', async () => { - const user = userEvent.setup({ delay: null }); - jest.useFakeTimers(); - - const debounceTime = 600; - - render( - - - - - , - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; - - await user.type(textbox, value); - - act(() => { - jest.advanceTimersByTime(debounceTime); - }); - - await waitFor(() => expect(textbox).toHaveValue(value)); - - expect(analyticsApiSpy.getEvents()).toHaveLength(0); - jest.useRealTimers(); - }); - - it('captures analytics events if enabled in app', async () => { - const user = userEvent.setup({ delay: null }); - jest.useFakeTimers(); - - const debounceTime = 600; - - apiRegistry = TestApiRegistry.from( - [analyticsApiRef, analyticsApiSpy], - [ - configApiRef, - new ConfigReader({ - app: { - title: 'Mock title', - analytics: { - ga: { - trackingId: 'xyz123', - }, - }, - }, - }), - ], - [searchApiRef, { query }], - ); - - render( - - - - - , - ); - - await waitFor(() => { - expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); - }); - - const textbox = screen.getByRole('textbox', { name }); - - const value = 'value'; - - await user.type(textbox, value); - - expect(analyticsApiSpy.getEvents()).toHaveLength(0); - - act(() => { - jest.advanceTimersByTime(debounceTime); - }); - - await waitFor(() => expect(textbox).toHaveValue(value)); - - expect(analyticsApiSpy.getEvents()).toHaveLength(1); - expect(analyticsApiSpy.getEvents()[0]).toEqual({ - action: 'search', - context: { - extension: 'App', - pluginId: 'root', - routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', - }, - subject: 'value', - }); - - await user.clear(textbox); - - // make sure new term is captured - await user.type(textbox, 'new value'); - - act(() => { - jest.advanceTimersByTime(debounceTime); - }); - - await waitFor(() => expect(textbox).toHaveValue('new value')); - - expect(analyticsApiSpy.getEvents()).toHaveLength(2); - expect(analyticsApiSpy.getEvents()[1]).toEqual({ - action: 'search', - context: { - extension: 'App', - pluginId: 'root', - routeRef: 'unknown', - searchTypes: 'software-catalog,techdocs', - }, - subject: 'new value', - }); - jest.useRealTimers(); - }); -}); diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts index 7e7211ca33..2c39d61bfb 100644 --- a/plugins/stack-overflow-backend/config.d.ts +++ b/plugins/stack-overflow-backend/config.d.ts @@ -18,11 +18,11 @@ export interface Config { /** * Configuration options for the stack overflow plugin */ - stackoverflow: { + stackoverflow?: { /** * The base url of the Stack Overflow API used for the plugin * @visibility backend */ - baseUrl: string; + baseUrl?: string; }; } diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index cc41da042e..083ab18b42 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -41,6 +41,7 @@ }, "files": [ "dist", - "config" - ] + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/stack-overflow/config.d.ts b/plugins/stack-overflow/config.d.ts index 811893231e..c0638e3c19 100644 --- a/plugins/stack-overflow/config.d.ts +++ b/plugins/stack-overflow/config.d.ts @@ -18,11 +18,11 @@ export interface Config { /** * Configuration options for the stack overflow plugin */ - stackoverflow: { + stackoverflow?: { /** * The base url of the Stack Overflow API used for the plugin * @visibility frontend */ - baseUrl: string; + baseUrl?: string; }; } diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 922277c74a..793e42881d 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -54,6 +54,7 @@ }, "files": [ "dist", - "config" - ] + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index c64a001f5f..fdacc9cfbc 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -66,6 +66,13 @@ export interface Config { publisher?: | { type: 'local'; + + local?: { + /** + * Directory to store generated static files. + */ + publishDirectory?: string; + }; } | { type: 'awsS3'; diff --git a/plugins/techdocs-backend/examples/documented-component/docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/index.md index 48a903445a..5a3d7508cd 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/index.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/index.md @@ -11,6 +11,20 @@ You can see also: ## Basic Markdown +Headings: + +# h1 + +## h2 + +### h3 + +#### h4 + +##### h5 + +###### h6 + Here is a bulleted list: - Item one diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index 660b8e8cb5..e3debab526 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -111,10 +111,10 @@ export class TechdocsGenerator implements GeneratorBase { parsedLocationAnnotation, this.scmIntegrations, ); + } - if (this.options.legacyCopyReadmeMdToIndexMd) { - await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); - } + if (this.options.legacyCopyReadmeMdToIndexMd) { + await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } if (!this.options.omitTechdocsCoreMkdocsPlugin) { diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index 0119def16d..ce2e0c3ccd 100644 --- a/plugins/techdocs-node/src/stages/publish/local.test.ts +++ b/plugins/techdocs-node/src/stages/publish/local.test.ts @@ -236,5 +236,32 @@ describe('local publisher', () => { ); expect(response.status).toBe(404); }); + + it('should work with a configured directory', async () => { + const customConfig = new ConfigReader({ + techdocs: { + publisher: { + local: { + publishDirectory: tmpDir, + }, + }, + }, + }); + mockFs({ + [tmpDir]: { + 'index.html': 'found it', + }, + }); + const legacyPublisher = LocalPublish.fromConfig( + customConfig, + logger, + testDiscovery, + ); + app = express().use(legacyPublisher.docsRouter()); + + const response = await request(app).get('/index.html'); + expect(response.status).toBe(200); + expect(response.text).toEqual('found it'); + }); }); }); diff --git a/plugins/techdocs-node/src/stages/publish/local.ts b/plugins/techdocs-node/src/stages/publish/local.ts index 11c6e6dd2b..1a02cb7eee 100644 --- a/plugins/techdocs-node/src/stages/publish/local.ts +++ b/plugins/techdocs-node/src/stages/publish/local.ts @@ -44,40 +44,27 @@ import { } from './helpers'; import { ForwardedError } from '@backstage/errors'; -// TODO: Use a more persistent storage than node_modules or /tmp directory. -// Make it configurable with techdocs.publisher.local.publishDirectory -let staticDocsDir = ''; -try { - staticDocsDir = resolvePackagePath( - '@backstage/plugin-techdocs-backend', - 'static/docs', - ); -} catch (err) { - // This will most probably never be used. - // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node - // on CI/CD without installing techdocs backend plugin. - staticDocsDir = os.tmpdir(); -} - /** - * Local publisher which uses the local filesystem to store the generated static files. It uses a directory - * called "static" at the root of techdocs-backend plugin. + * Local publisher which uses the local filesystem to store the generated static files. It uses by default a + * directory called "static" at the root of techdocs-backend plugin unless a directory has been configured by + * "techdocs.publisher.local.publishDirectory". */ export class LocalPublish implements PublisherBase { private readonly legacyPathCasing: boolean; private readonly logger: Logger; private readonly discovery: PluginEndpointDiscovery; + private readonly staticDocsDir: string; - // TODO: Move the logic of setting staticDocsDir based on config over to - // fromConfig, and set the value as a class parameter. constructor(options: { logger: Logger; discovery: PluginEndpointDiscovery; legacyPathCasing: boolean; + staticDocsDir: string; }) { this.logger = options.logger; this.discovery = options.discovery; this.legacyPathCasing = options.legacyPathCasing; + this.staticDocsDir = options.staticDocsDir; } static fromConfig( @@ -90,10 +77,28 @@ export class LocalPublish implements PublisherBase { 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; + let staticDocsDir = config.getOptionalString( + 'techdocs.publisher.local.publishDirectory', + ); + if (!staticDocsDir) { + try { + staticDocsDir = resolvePackagePath( + '@backstage/plugin-techdocs-backend', + 'static/docs', + ); + } catch (err) { + // This will most probably never be used. + // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node + // on CI/CD without installing techdocs backend plugin. + staticDocsDir = os.tmpdir(); + } + } + return new LocalPublish({ logger, discovery, legacyPathCasing, + staticDocsDir, }); } @@ -144,7 +149,7 @@ export class LocalPublish implements PublisherBase { const techdocsApiUrl = await this.discovery.getBaseUrl('techdocs'); const publishedFilePaths = (await getFileTreeRecursively(publishDir)).map( abs => { - return abs.split(`${staticDocsDir}/`)[1]; + return abs.split(`${this.staticDocsDir}/`)[1]; }, ); @@ -222,9 +227,8 @@ export class LocalPublish implements PublisherBase { // Otherwise, redirect to the new path. return res.redirect(301, req.baseUrl + newPath); }); - router.use( - express.static(staticDocsDir, { + express.static(this.staticDocsDir, { // Handle content-type header the same as all other publishers. setHeaders: (res, filePath) => { const fileExtension = path.extname(filePath); @@ -275,13 +279,16 @@ export class LocalPublish implements PublisherBase { concurrency = 25, }): Promise { // Iterate through every file in the root of the publisher. - const files = await getFileTreeRecursively(staticDocsDir); + const files = await getFileTreeRecursively(this.staticDocsDir); const limit = createLimiter(concurrency); await Promise.all( files.map(f => limit(async file => { - const relativeFile = file.replace(`${staticDocsDir}${path.sep}`, ''); + const relativeFile = file.replace( + `${this.staticDocsDir}${path.sep}`, + '', + ); const newFile = lowerCaseEntityTripletInStoragePath(relativeFile); // If all parts are already lowercase, ignore. @@ -311,7 +318,7 @@ export class LocalPublish implements PublisherBase { * Utility wrapper around path.join(), used to control legacy case logic. */ protected staticEntityPathJoin(...allParts: string[]): string { - let staticEntityPath = staticDocsDir; + let staticEntityPath = this.staticDocsDir; allParts .map(part => part.split(path.sep)) diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts index e24b61da83..6a3b0461ef 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts @@ -16,8 +16,14 @@ import { RuleOptions } from './types'; +type RuleTypography = RuleOptions['theme']['typography']; + +type BackstageTypography = RuleTypography & { + htmlFontSize?: number; +}; + type TypographyHeadings = Pick< - RuleOptions['theme']['typography'], + RuleTypography, 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' >; @@ -33,13 +39,15 @@ export default ({ theme }: RuleOptions) => ` } ${headings.reduce((style, heading) => { + const htmlFontSize = + (theme.typography as BackstageTypography).htmlFontSize ?? 16; const styles = theme.typography[heading]; const { lineHeight, fontFamily, fontWeight, fontSize } = styles; const calculate = (value: typeof fontSize) => { let factor: number | string = 1; if (typeof value === 'number') { - // 60% of the size defined because it is too big - factor = (value / 16) * 0.6; + // convert px to rem + factor = value / htmlFontSize; } if (typeof value === 'string') { factor = value.replace('rem', ''); diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 39d0e2725f..6bf41324eb 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2610,7 +2610,7 @@ ansi-to-html@^0.6.11: any-promise@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== anymatch@^2.0.0: version "2.0.0" @@ -7039,12 +7039,7 @@ pinkie@^2.0.0: resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -pirates@^4.0.1: - version "4.0.4" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6" - integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw== - -pirates@^4.0.5: +pirates@^4.0.1, pirates@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== @@ -8392,9 +8387,9 @@ style-to-object@0.3.0, style-to-object@^0.3.0: inline-style-parser "0.1.1" sucrase@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.21.1" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.1.tgz#7e29ddaca012764cf843280b00e74a843bdf790f" + integrity sha512-kxXnC9yZEav5USAu8gooZID9Ph3xqwdJxZoh+WbOWQZHTB7CHj3ANwENVMZ6mAZ9k7UtJtFxvQD9R03q3yU2YQ== dependencies: commander "^4.0.0" glob "7.1.6" @@ -8552,7 +8547,7 @@ test-exclude@^6.0.0: thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== dependencies: thenify ">= 3.1.0 < 4" diff --git a/yarn.lock b/yarn.lock index 6841fc4366..c1eaefe398 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,7 +16,7 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@apidevtools/json-schema-ref-parser@^9.0.6": +"@apidevtools/json-schema-ref-parser@9.0.6", "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.6" resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg== @@ -25,6 +25,29 @@ call-me-maybe "^1.0.1" js-yaml "^3.13.1" +"@apidevtools/openapi-schemas@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@^10.1.0": + version "10.1.0" + resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz#a987d71e5be61feb623203be0c96e5985b192ab6" + integrity sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.6" + "@apidevtools/openapi-schemas" "^2.1.0" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + ajv "^8.6.3" + ajv-draft-04 "^1.0.0" + call-me-maybe "^1.0.1" + "@apollo/protobufjs@1.2.2": version "1.2.2" resolved "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c" @@ -1962,9 +1985,9 @@ "@lezer/highlight" "^1.0.0" "@codemirror/view@^6.0.0": - version "6.0.1" - resolved "https://registry.npmjs.org/@codemirror/view/-/view-6.0.1.tgz#f56327a5cd241eff5393cfdfbceea897b5266967" - integrity sha512-n5olUr4Ld+XDTZN8+cnUHzkqJPeO2kr55HvBQ+4C1xWlDaRGrsedAoxCgeuGZsXRTUup/H/0e2kAN+a0R3skdA== + version "6.0.2" + resolved "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz#27f4d08edd10a3678cf15390b4fba5e2a7220873" + integrity sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q== dependencies: "@codemirror/state" "^6.0.0" style-mod "^4.0.0" @@ -2597,7 +2620,7 @@ "@graphql-tools/utils" "8.6.13" tslib "^2.4.0" -"@graphql-tools/schema@8.3.10", "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.1.1", "@graphql-tools/schema@^8.1.2", "@graphql-tools/schema@^8.3.1": +"@graphql-tools/schema@8.3.10", "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.1.2", "@graphql-tools/schema@^8.3.1": version "8.3.10" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.10.tgz#c3e373e6ad854f533fc7e55859dd8f9e81de30dd" integrity sha512-tfhjSTi3OzheDrVzG7rkPZg2BbQjmZRLM2vvQoM2b1TnUwgUIbpAgcnf+AWDLRsoCOWlezeLgij1BLeAR0Q0jg== @@ -4286,9 +4309,9 @@ react-is "^16.8.0 || ^17.0.0" "@maxim_mazurok/gapi.client.calendar@^3.0.20220408": - version "3.0.20220610" - resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220610.tgz#46e0092da54fc8eb1342a3d1af04a071a761faac" - integrity sha512-pZwIaTw+PizFSXrF5WqP4dj+b1Vlj/hNwBY4ocWpJ2uhBDoBPAVIc8lEUNwNZVDbAgtbWHD2Kl84UsP1wmBS+w== + version "3.0.20220617" + resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220617.tgz#140bbfd2caa1770fedbdaec4182eb43684f2d876" + integrity sha512-Vek5Y655GUi4RmUs3cNLfo/KQeReEuvcViJ0KYHl28KmC2Pqmxb4V2YIUhsH7BGVyq84cTIT59qHSirB919Prw== dependencies: "@types/gapi.client" "*" @@ -4333,9 +4356,9 @@ typescript "~4.6.3" "@microsoft/microsoft-graph-types@^2.6.0": - version "2.21.0" - resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.21.0.tgz#3244b41374307f590948e25fb2de8dad2a8a81c9" - integrity sha512-ZbYetXyUqHZliM8exZzF7IPEKVMvR+PaY+P9bjNHpxeiacF3U9+c40YRv0TJHpF6+50GWmJmPD6BsKHVRBZiCA== + version "2.22.0" + resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.22.0.tgz#7ceb222311770ed5b240d627b657d25b3979964e" + integrity sha512-iKc5L036hc/wu13DX5kGf//3/WqTAErAPTyYKG6zT3vG070xXaaP7PInODznfyZduhiOvavmrRlQb34ajeDTUA== "@microsoft/tsdoc-config@~0.16.1": version "0.16.1" @@ -5319,7 +5342,7 @@ dependencies: "@rollup/pluginutils" "^3.0.8" -"@rollup/plugin-node-resolve@^13.0.0": +"@rollup/plugin-node-resolve@^13.0.6": version "13.3.0" resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== @@ -7008,9 +7031,9 @@ terser-webpack-plugin "*" "@types/testing-library__jest-dom@^5.9.1": - version "5.14.4" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.4.tgz#21567ec845f4efee55923842c79728dc49c1650b" - integrity sha512-EUCs9PTBOEyfRtLKkKd31YrRCx/9Wxjy2Uqb6IH/KAOr7/vP0i8iClOyxQqjm/UxMGU5r5s2vOBM7vSPQVmabg== + version "5.14.5" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" + integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ== dependencies: "@types/jest" "*" @@ -7317,9 +7340,9 @@ eslint-visitor-keys "^3.0.0" "@uiw/react-codemirror@^4.9.3": - version "4.9.3" - resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.3.tgz#3d7abc434b140f01119332e25e9f49ace7c381bf" - integrity sha512-iT/daKF852ZsNJGvqmxRFJ4KTOJSikeoCMl7jhUUJTETdozUf0/DqUQhvazAoduMRzqqHYnN/5isdITCyy2P6g== + version "4.9.4" + resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.4.tgz#4643201f279fed103f2cf15df9431931fe4e9f15" + integrity sha512-IsC5xDevpIeLMzHQQwT2W40gFFIdKeT1T0DHjzzai+s5SIrMlGe3QSHWeC1wSO7FtfNxFpFlTYMGJm5JwUviMA== dependencies: "@babel/runtime" ">=7.11.0" "@codemirror/theme-one-dark" "^6.0.0" @@ -7613,6 +7636,11 @@ aggregate-error@^3.0.0, aggregate-error@^3.1.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-draft-04@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -7642,7 +7670,7 @@ ajv@^6.10.0, ajv@^6.10.1, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.10.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.6.3, ajv@^8.8.0: version "8.11.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -8232,9 +8260,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.1122.0, aws-sdk@^2.840.0, aws-sdk@^2.948.0: - version "2.1158.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1158.0.tgz#924fb6dda1acecb1dc1c8901095cf64d6acdade4" - integrity sha512-uHYzZMGE+b50sWXaLhga4aD1SpB3+DEZclAkg9aYz2pDZlSDTOMh3uJ/ufsMBs7VcDKGS7mQRibCmCbwRGTIlg== + version "2.1159.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1159.0.tgz#64d585ac608d58e6ff62678be71a4c4ca61ca5dc" + integrity sha512-zm3k/ufwZnkWc6M+HDz00CWuILot4L9kJ5VJsuDS9fwsT9To6k91Y1njCtIV4tcgcXvUru0Sbm4D0w5bc2847A== dependencies: buffer "4.9.2" events "1.1.1" @@ -10719,9 +10747,9 @@ cypress-plugin-snapshots@^1.4.4: unidiff "1.0.2" cypress@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.1.0.tgz#6514a26c721822a02bc194e9a7f72c3142aea174" - integrity sha512-aQ4JVZVib4Xd9FZW8IRZfKelUvqF4y5A+oUbNvn8TlsBmEfIg3m5Xd6Mt6PVU/jHiVJ9Psl905B3ZPnrDcmyuQ== + version "10.2.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" + integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -13884,9 +13912,9 @@ google-auth-library@^7.14.0: lru-cache "^6.0.0" google-auth-library@^8.0.0, google-auth-library@^8.0.1, google-auth-library@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz#5fa0f2d3795c3e4019d2bb315ade4454cc9c30b5" - integrity sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg== + version "8.0.3" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.3.tgz#1e780430632d03df36dc22a3f5c89dbc251f2105" + integrity sha512-1eC6yaCrPfkv3bwtb3e0AOct7E7xR/uikDyXNo/j8Wd6a1ldRgAey5FmaDGNJnHNDPLtDiENQLYsA69eXOF5sA== dependencies: arrify "^2.0.0" base64-js "^1.3.0" @@ -13894,7 +13922,7 @@ google-auth-library@^8.0.0, google-auth-library@^8.0.1, google-auth-library@^8.0 fast-text-encoding "^1.0.0" gaxios "^5.0.0" gcp-metadata "^5.0.0" - gtoken "^5.3.2" + gtoken "^6.0.0" jws "^4.0.0" lru-cache "^6.0.0" @@ -13943,10 +13971,10 @@ google-p12-pem@^3.0.3: dependencies: node-forge "^1.0.0" -google-p12-pem@^3.1.3: - version "3.1.4" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" - integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== +google-p12-pem@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz#f46581add1dc6ea0b96160cda6ce37ee35ab8ca3" + integrity sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w== dependencies: node-forge "^1.3.1" @@ -14070,14 +14098,14 @@ graphql-language-service@^5.0.6: vscode-languageserver-types "^3.15.1" graphql-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/graphql-modules/-/graphql-modules-2.0.0.tgz#5f710d86eb7295da83769c8cd6d61a049d999800" - integrity sha512-CM5CIJp428+ripgcLyrioBmAKB3ucvIEOgJG4WMjnOgScHY/eCZh/8I51cF8oc+pqebOgBCR3HH//IH5v7kv+w== + version "2.1.0" + resolved "https://registry.npmjs.org/graphql-modules/-/graphql-modules-2.1.0.tgz#d99692034b4b053fba3d0ebe49e4e66772ed2785" + integrity sha512-fOUc4i5xNLkRxqx234MIr+kolrxk1tZ2onTeRIcB73mCY4NeKxej7FMLb5St+UcNLzhqM4L6Mf47rN9MPVDmgA== dependencies: - "@graphql-tools/schema" "^8.1.1" + "@graphql-tools/schema" "^8.3.1" "@graphql-tools/wrap" "^8.3.1" "@graphql-typed-document-node/core" "^3.1.0" - ramda "^0.27.1" + ramda "^0.28.0" graphql-request@^4.0.0: version "4.2.0" @@ -14145,13 +14173,13 @@ gtoken@^5.0.4: jws "^4.0.0" mime "^2.2.0" -gtoken@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" - integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== +gtoken@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-6.0.1.tgz#1276371d51e93c4eface76e3f30f9e8f1cad3f1a" + integrity sha512-J0vebk6u6i4rLTM0lQq25SdusCLMvujYNZeAouyPvSbGlcjw7P8L3W9INIFnlXUx+AUD7TDoM1mgdhzH+XX7DQ== dependencies: gaxios "^4.0.0" - google-p12-pem "^3.1.3" + google-p12-pem "^4.0.0" jws "^4.0.0" gzip-size@^6.0.0: @@ -16865,9 +16893,9 @@ keyv@^3.0.0: json-buffer "3.0.0" keyv@^4.0.0, keyv@^4.0.3: - version "4.3.1" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.3.1.tgz#7970672f137d987945821b1a07b524ce5a4edd27" - integrity sha512-nwP7AQOxFzELXsNq3zCx/oh81zu4DHWwCE6W9RaeHb7OHO0JpmKS8n801ovVQC7PTsZDWtPA5j1QY+/WWtARYg== + version "4.3.2" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.3.2.tgz#e839df676a0c7ee594c8835e7c1c83742558e5c2" + integrity sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw== dependencies: compress-brotli "^1.3.8" json-buffer "3.0.1" @@ -17554,9 +17582,9 @@ log-update@^4.0.0: wrap-ansi "^6.2.0" logform@^2.3.2, logform@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" - integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + version "2.4.1" + resolved "https://registry.npmjs.org/logform/-/logform-2.4.1.tgz#512c9eaef738044d1c619790ba0f806c80d9d3a9" + integrity sha512-7XB/tqc3VRbri9pRjU6E97mQ8vC27ivJ3lct4jhyT+n0JNDd4YKldFl0D75NqDp46hk8RC7Ma1Vjv/UPf67S+A== dependencies: "@colors/colors" "1.5.0" fecha "^4.2.0" @@ -18805,9 +18833,9 @@ msw@^0.39.2: yargs "^17.3.1" msw@^0.42.0: - version "0.42.1" - resolved "https://registry.npmjs.org/msw/-/msw-0.42.1.tgz#2496d3e191754b68686e2530de459a2e102f85c4" - integrity sha512-LZZuz7VddL45gCBgfBWHyXj6a4W7OTJY0mZPoipJ3P/xwbuJwrtwB3IJrWlqBM8aink/eTKlRxwzmtIAwCj5yQ== + version "0.42.3" + resolved "https://registry.npmjs.org/msw/-/msw-0.42.3.tgz#150c475e2cb6d53c67503bd0e3f6251bfd075328" + integrity sha512-zrKBIGCDsNUCZLd3DLSeUtRruZ0riwJgORg9/bSDw3D0PTI8XUGAK3nC0LJA9g0rChGuKaWK/SwObA8wpFrz4g== dependencies: "@mswjs/cookies" "^0.2.0" "@mswjs/interceptors" "^0.16.3" @@ -19627,6 +19655,11 @@ openapi-sampler@^1.2.1: "@types/json-schema" "^7.0.7" json-pointer "0.6.2" +openapi-types@^11.0.1: + version "11.1.0" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-11.1.0.tgz#037969f3dfa5999423ee33bf889fb0d12984277e" + integrity sha512-ZW+Jf12flFF6DXSij8DGL3svDA4RtSyHXjC/xB/JAh18gg3uVfVIFLvCfScUMowrpvlkxsMMbErakbth2g3/iQ== + openid-client@^4.1.1: version "4.9.0" resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.9.0.tgz#bdfc9194435316df419f759ce177635146b43074" @@ -21412,10 +21445,10 @@ raf@^3.4.0: dependencies: performance-now "^2.1.0" -ramda@^0.27.1: - version "0.27.2" - resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" - integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== +ramda@^0.28.0: + version "0.28.0" + resolved "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz#acd785690100337e8b063cab3470019be427cc97" + integrity sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA== randexp@^0.5.3: version "0.5.3" @@ -22776,9 +22809,9 @@ run-parallel@^1.1.9: integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== run-script-webpack-plugin@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.1.0.tgz#6250a4734511836628259666d56641a27526875d" - integrity sha512-PKddVrnTu1BO90ogvN93yP/zg3X8Q4XvSKKyGVZFye9VBVbgVkFllKykNOomi9IfNtnoFFzu6TkLONIuXq5URA== + version "0.1.1" + resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.1.1.tgz#dad3114be32eb864d2160306e4d9c52a2c1cfd59" + integrity sha512-PrxBRLv1K9itDKMlootSCyGhdTU+KbKGJ2wF6/k0eyo6M0YGPC58HYbS/J/QsDiwM0t7G99WcuCqto0J7omOXA== rxjs@7.5.5, rxjs@^7.0.0, rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1, rxjs@^7.5.5: version "7.5.5" @@ -24153,9 +24186,9 @@ stylis@^4.0.6: integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== sucrase@^3.18.0, sucrase@^3.20.2: - version "3.21.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.21.1" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.21.1.tgz#7e29ddaca012764cf843280b00e74a843bdf790f" + integrity sha512-kxXnC9yZEav5USAu8gooZID9Ph3xqwdJxZoh+WbOWQZHTB7CHj3ANwENVMZ6mAZ9k7UtJtFxvQD9R03q3yU2YQ== dependencies: commander "^4.0.0" glob "7.1.6"