diff --git a/.changeset/chilly-queens-bow.md b/.changeset/chilly-queens-bow.md deleted file mode 100644 index 00e8628776..0000000000 --- a/.changeset/chilly-queens-bow.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-azure-devops-common': patch ---- - -feat: Created pull request card component and initial pull request dashboard page. diff --git a/.changeset/clever-singers-search.md b/.changeset/clever-singers-search.md deleted file mode 100644 index 0750cea8a8..0000000000 --- a/.changeset/clever-singers-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Narrow the types returned by the request option functions, to only the specifics that they actually do return. The reason for this change is that a full `RequestInit` is unfortunate to return because it's different between `cross-fetch` and `node-fetch`. diff --git a/.changeset/cool-rules-smash.md b/.changeset/cool-rules-smash.md deleted file mode 100644 index d879dc5f5b..0000000000 --- a/.changeset/cool-rules-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Removed unused scaffolder visibility configuration; this has been moved to publish actions. Deprecated scaffolder provider configuration keys; these should use the integrations configuration instead. diff --git a/.changeset/dry-pianos-brush.md b/.changeset/dry-pianos-brush.md deleted file mode 100644 index c749962a83..0000000000 --- a/.changeset/dry-pianos-brush.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Incorporate usage of the tokenManager into the backend created using `create-app`. - -In existing backends, update the `PluginEnvironment` to include a `tokenManager`: - -```diff -// packages/backend/src/types.ts - -... -import { - ... -+ TokenManager, -} from '@backstage/backend-common'; - -export type PluginEnvironment = { - ... -+ tokenManager: TokenManager; -}; -``` - -Then, create a `ServerTokenManager`. This can either be a `noop` that requires no secret and validates all requests by default, or one that uses a secret from your `app-config.yaml` to generate and validate tokens. - -```diff -// packages/backend/src/index.ts - -... -import { - ... -+ ServerTokenManager, -} from '@backstage/backend-common'; -... - -function makeCreateEnv(config: Config) { - ... - // CHOOSE ONE - // TokenManager not requiring a secret -+ const tokenManager = ServerTokenManager.noop(); - // OR TokenManager requiring a secret -+ const tokenManager = ServerTokenManager.fromConfig(config); - - ... - return (plugin: string): PluginEnvironment => { - ... -- return { logger, cache, database, config, reader, discovery }; -+ return { logger, cache, database, config, reader, discovery, tokenManager }; - }; -} -``` diff --git a/.changeset/early-bees-think.md b/.changeset/early-bees-think.md deleted file mode 100644 index ea77fa04c7..0000000000 --- a/.changeset/early-bees-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Added apiVersionOverrides config to allow for specifying api versions to use for kubernetes objects diff --git a/.changeset/fresh-pumas-collect.md b/.changeset/fresh-pumas-collect.md deleted file mode 100644 index 15d36472ed..0000000000 --- a/.changeset/fresh-pumas-collect.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@backstage/config-loader': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-badges-backend': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-tech-insights-backend': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-todo-backend': patch ---- - -Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them diff --git a/.changeset/great-eagles-mate.md b/.changeset/great-eagles-mate.md deleted file mode 100644 index c2491be2b1..0000000000 --- a/.changeset/great-eagles-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -The theme switcher now renders the title of themes instead of their variant diff --git a/.changeset/green-toes-search.md b/.changeset/green-toes-search.md deleted file mode 100644 index 2bdba7e31a..0000000000 --- a/.changeset/green-toes-search.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': minor ---- - -**BREAKING** `DefaultTechDocsCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: - -```diff -// packages/backend/src/plugins/search.ts - -... -export default async function createPlugin({ - ... -+ tokenManager, -}: PluginEnvironment) { - ... - - indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { - discovery, - logger, -+ tokenManager, - }), - }); - - ... -} -``` diff --git a/.changeset/hungry-impalas-wave.md b/.changeset/hungry-impalas-wave.md deleted file mode 100644 index 376b9fc341..0000000000 --- a/.changeset/hungry-impalas-wave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-permission-node': minor -'@backstage/plugin-permission-backend': patch ---- - -Rename and adjust permission policy return type to reduce nesting diff --git a/.changeset/lemon-moons-stare.md b/.changeset/lemon-moons-stare.md deleted file mode 100644 index 4b7818d403..0000000000 --- a/.changeset/lemon-moons-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Create a `TokenManager` interface and `ServerTokenManager` implementation to generate and validate server tokens for authenticated backend-to-backend API requests. diff --git a/.changeset/light-cooks-train.md b/.changeset/light-cooks-train.md deleted file mode 100644 index 435305998c..0000000000 --- a/.changeset/light-cooks-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Query CronJobs from Kubernetes with apiGroup BatchV1beta1 diff --git a/.changeset/long-spiders-bow.md b/.changeset/long-spiders-bow.md deleted file mode 100644 index c54641efe3..0000000000 --- a/.changeset/long-spiders-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': minor ---- - -Remove the `backend:build-image` command from the CLI and added more deprecation warnings to other deprecated fields like `--lax` and `remove-plugin` diff --git a/.changeset/moody-pears-suffer.md b/.changeset/moody-pears-suffer.md deleted file mode 100644 index 920c0ff952..0000000000 --- a/.changeset/moody-pears-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Fixed a bug in the `CircleCI` plugin where restarting builds was hard-coded to GitHub rather than introspecting the entity source location. diff --git a/.changeset/olive-rats-destroy.md b/.changeset/olive-rats-destroy.md deleted file mode 100644 index 5908fc0f10..0000000000 --- a/.changeset/olive-rats-destroy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/cli': patch -'@techdocs/cli': patch ---- - -Bump react-dev-utils to v12 diff --git a/.changeset/rich-teachers-hide.md b/.changeset/rich-teachers-hide.md deleted file mode 100644 index 27f437cbed..0000000000 --- a/.changeset/rich-teachers-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(dependencies): bump `style-loader` from 1.2.1 to 3.3.1 diff --git a/.changeset/rude-brooms-raise.md b/.changeset/rude-brooms-raise.md deleted file mode 100644 index 6b5bd3ca3a..0000000000 --- a/.changeset/rude-brooms-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': patch ---- - -Tweaked the logged deprecation warning for `createRouteRef` to hopefully make it more clear. diff --git a/.changeset/search-birds-leave.md b/.changeset/search-birds-leave.md deleted file mode 100644 index cd5c046934..0000000000 --- a/.changeset/search-birds-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Add optional icon and secondaryAction properties for DefaultResultListItem component diff --git a/.changeset/search-zebras-matter.md b/.changeset/search-zebras-matter.md deleted file mode 100644 index 277e36dd39..0000000000 --- a/.changeset/search-zebras-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Add a new optional clearButton property to the SearchBar component. The default value for this new property is true. diff --git a/.changeset/sharp-flies-cheat.md b/.changeset/sharp-flies-cheat.md deleted file mode 100644 index 6c66232ff0..0000000000 --- a/.changeset/sharp-flies-cheat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/plugin-catalog-graphql': patch ---- - -Move to using node-fetch internally instead of cross-fetch diff --git a/.changeset/tasty-deers-play.md b/.changeset/tasty-deers-play.md deleted file mode 100644 index cf5322b0bd..0000000000 --- a/.changeset/tasty-deers-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Expose catalog lib in plugin-auth-backend, i.e `CatalogIdentityClient` class is exposed now. diff --git a/.changeset/thick-poems-camp.md b/.changeset/thick-poems-camp.md deleted file mode 100644 index 72056dfb5a..0000000000 --- a/.changeset/thick-poems-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page. diff --git a/.changeset/twenty-worms-provide.md b/.changeset/twenty-worms-provide.md deleted file mode 100644 index ae231a1fa0..0000000000 --- a/.changeset/twenty-worms-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Add options to spawn in runCommand helper diff --git a/.changeset/two-lions-cross.md b/.changeset/two-lions-cross.md deleted file mode 100644 index fffa4c28ee..0000000000 --- a/.changeset/two-lions-cross.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-azure-devops-backend': patch ---- - -**Backend** - -- Created new `/dashboard-pull-requests/:projectName` endpoint -- Created new `/all-teams` endpoint -- Implemented pull request policy evaluation conversion - -**Frontend** - -- Refactored `PullRequestsPage` and added new properties for `projectName` and `pollingInterval` -- Fixed spacing issue between repo link and creation date in `PullRequestCard` -- Added missing condition to `PullRequestCardPolicy` for `RequiredReviewers` -- Updated `useDashboardPullRequests` hook to implement long polling for pull requests diff --git a/.changeset/weak-berries-heal.md b/.changeset/weak-berries-heal.md new file mode 100644 index 0000000000..4160d078fc --- /dev/null +++ b/.changeset/weak-berries-heal.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-apache-airflow': minor +--- + +Introduces a new plugin for the Apache Airflow workflow management platform. +This implementation has been tested with the Apache Airflow v2 API, +authenticating with basic authentication through the Backstage proxy plugin. + +Supported functionality includes: + +- Information card of version information of the Airflow instance +- Information card of instance health for the meta-database and scheduler +- Table of DAGs with meta information and status, along with a link to view + details in the Airflow UI diff --git a/.changeset/weak-rivers-perform.md b/.changeset/weak-rivers-perform.md deleted file mode 100644 index 92f96ab6e0..0000000000 --- a/.changeset/weak-rivers-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Added accordions to display information on Jobs and CronJobs in the kubernetes plugin. Updated the PodsTable with fewer default columns and the ability to pass in additional ones depending on the use case. diff --git a/.changeset/witty-cats-tell.md b/.changeset/witty-cats-tell.md deleted file mode 100644 index 82e33bc0a6..0000000000 --- a/.changeset/witty-cats-tell.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Add group filtering to the scaffolder page so that individuals can surface specific templates to end users ahead of others, or group templates together. This can be accomplished by passing in a `groups` prop to the `ScaffolderPage` - -``` - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - ]} -/> -``` diff --git a/.changeset/young-bikes-argue.md b/.changeset/young-bikes-argue.md deleted file mode 100644 index a9f09cd12b..0000000000 --- a/.changeset/young-bikes-argue.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**BREAKING** `DefaultCatalogCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: - -```diff -// packages/backend/src/plugins/search.ts - -... -export default async function createPlugin({ - ... -+ tokenManager, -}: PluginEnvironment) { - ... - - indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { - discovery, -+ tokenManager, - }), - }); - - ... -} -``` diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 580109fb97..ed4b61099f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,10 +23,13 @@ /plugins/azure-devops @backstage/reviewers @marleypowell @awanlin /plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin /plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin +/plugins/jenkins @backstage/reviewers @timja +/plugins/jenkins-backend @backstage/reviewers @timja /plugins/kafka @backstage/reviewers @nirga /plugins/kafka-backend @backstage/reviewers @nirga /plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka /plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski +/plugins/git-release-manager @backstage/reviewers @erikengervall /tech-insights-backend @backstage/reviewers @xantier @iain-b /tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b /tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b diff --git a/ADOPTERS.md b/ADOPTERS.md index 11bda10020..43e7fba68b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -5,7 +5,7 @@ | [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | | [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | | [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | | [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | | [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | | [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | @@ -72,4 +72,6 @@ | [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | | [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | | [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | -| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | +| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | +| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | +| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | diff --git a/app-config.yaml b/app-config.yaml index 4adaea0da4..6cd05f44e8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -97,6 +97,11 @@ proxy: headers: Authorization: ${ILERT_AUTH_HEADER} + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} + organization: name: My Company @@ -437,3 +442,6 @@ azureDevOps: host: dev.azure.com token: my-token organization: my-company + +apacheAirflow: + baseUrl: https://your.airflow.instance.com diff --git a/cypress/yarn.lock b/cypress/yarn.lock index baf0d92715..96bc765d0d 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -13,9 +13,9 @@ figures "^1.7.0" "@cypress/request@^2.88.5": - version "2.88.5" - resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" - integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + version "2.88.10" + resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" + integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -24,19 +24,17 @@ extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" + http-signature "~1.3.6" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" - oauth-sign "~0.9.0" performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" tough-cookie "~2.5.0" tunnel-agent "^0.6.0" - uuid "^3.3.2" + uuid "^8.3.2" "@cypress/xvfb@^1.2.4": version "1.2.4" @@ -68,16 +66,6 @@ resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== -ajv@^6.12.3: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -539,16 +527,6 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -645,19 +623,6 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -675,14 +640,14 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== dependencies: assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + jsprim "^2.0.2" + sshpk "^1.14.1" human-signals@^1.1.1: version "1.1.1" @@ -796,15 +761,10 @@ jsbn@~0.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stringify-safe@~5.0.1: version "5.0.1" @@ -820,14 +780,14 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" lazy-ass@^1.6.0: @@ -985,11 +945,6 @@ number-is-nan@^1.0.0: resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -1084,7 +1039,7 @@ punycode@1.3.2: resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^2.1.0, punycode@^2.1.1: +punycode@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -1191,7 +1146,7 @@ slice-ansi@0.0.4: resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= -sshpk@^1.7.0: +sshpk@^1.14.1: version "1.16.1" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== @@ -1353,13 +1308,6 @@ untildify@^4.0.0: resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - url@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -1373,10 +1321,10 @@ util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== verror@1.10.0: version "1.10.0" diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 133653df44..14920513af 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -291,7 +291,7 @@ The figure below shows the relationship between fooApiRef.
-Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them +Figure showing the relationship between utility APIs, the apps that provide them, and the plugins that consume them
The current method for connecting Utility API providers and consumers is via the diff --git a/docs/assets/techdocs/architecture-recommended.drawio.svg b/docs/assets/techdocs/architecture-recommended.drawio.svg index e3af4b6b5f..1768b5dc86 100644 --- a/docs/assets/techdocs/architecture-recommended.drawio.svg +++ b/docs/assets/techdocs/architecture-recommended.drawio.svg @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + @@ -67,8 +67,8 @@ - - + + @@ -105,7 +105,7 @@ - + @@ -124,7 +124,7 @@ - + @@ -232,34 +232,17 @@
- TechDocs plugin + TechDocs Plugin
- TechDocs plugin + TechDocs Plugin - - - - -
-
-
- TechDocs Backend plugin -
-
-
-
- - TechDocs Backend plu... - -
-
- + @@ -268,21 +251,23 @@
- Request TechDocs site + + Request TechDocs Site +
- Request TechDocs site + Request TechDocs Site
- + -
+
Fetch files to render @@ -290,7 +275,7 @@
- + Fetch files to render @@ -302,15 +287,15 @@ - + - + -
+
Source code hosting @@ -323,28 +308,9 @@ - - - - - -
-
-
- Caching -
- (Optional) -
-
-
-
- - Caching... - -
-
- - + + + @@ -379,10 +345,132 @@ + + + + +
+
+
+ Cache Store +
+ (Optional) +
+
+
+
+ + Cache Store... + +
+
+ + + + +
+
+
+ Read/Write +
+ Objects +
+
+
+
+ + Read/Write... + +
+
+ + + + + + + +
+
+
+ + + Memcache + + +
+
+
+
+ + Memcache + +
+
+ + + + + +
+
+
+ + TechDocs Service + +
+
+
+
+ + TechDocs Service + +
+
+ + + + +
+
+
+ + Cache Middleware +
+ (Optional) +
+
+
+
+
+ + Cache Middleware... + +
+
+ + + + + + +
+
+
+ TechDocs Backend Plugin +
+
+
+
+ + TechDocs Backend Plugin + +
+
- + Viewer does not support full SVG 1.1 diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index a7f9b0cd1e..07a12f54a0 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -26,6 +26,7 @@ kubernetes: name: minikube authProvider: 'serviceAccount' skipTLSVerify: false + skipMetricsLookup: true serviceAccountToken: ${K8S_MINIKUBE_TOKEN} dashboardUrl: http://127.0.0.1:64713 # url copied from running the command: minikube service kubernetes-dashboard -n kubernetes-dashboard dashboardApp: standard @@ -37,6 +38,7 @@ kubernetes: projectId: 'gke-clusters' region: 'europe-west1' skipTLSVerify: true + skipMetricsLookup: true ``` ### `serviceLocatorMethod` @@ -86,8 +88,13 @@ cluster. Valid values are: ##### `clusters.\*.skipTLSVerify` -This determines whether or not the Kubernetes client verifies the TLS -certificate presented by the API server. Defaults to `false`. +This determines whether the Kubernetes client verifies the TLS certificate +presented by the API server. Defaults to `false`. + +##### `clusters.\*.skipMetricsLookup` + +This determines whether the Kubernetes client looks up resource metrics +CPU/Memory for pods returned by the API server. Defaults to `false`. ##### `clusters.\*.serviceAccountToken` (optional) @@ -188,8 +195,13 @@ regions. ##### `skipTLSVerify` -This determines whether or not the Kubernetes client verifies the TLS -certificate presented by the API server. Defaults to `false`. +This determines whether the Kubernetes client verifies the TLS certificate +presented by the API server. Defaults to `false`. + +##### `skipMetricsLookup` + +This determines whether the Kubernetes client looks up resource metrics +CPU/Memory for pods returned by the API server. Defaults to `false`. ### `customResources` (optional) diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 7c3717e22e..285e399cb4 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -114,13 +114,15 @@ const routes = ( In `Root.tsx`, add the `SidebarSearchModal` component: ```bash -import { SidebarSearchModal } from '@backstage/plugin-search'; +import { SidebarSearchModal, SearchContextProvider } from '@backstage/plugin-search'; export const Root = ({ children }: PropsWithChildren<{}>) => ( - + + + ... ``` diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 2cdf6f1b35..ed7a103bc4 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -11,13 +11,13 @@ would be good to also have some files in there that can be templated in. A simple `template.yaml` definition might look something like this: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -55,7 +55,7 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' + name: ${{ parameters.name }} - id: fetch-docs name: Fetch Docs @@ -69,14 +69,14 @@ spec: action: publish:github input: allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' ``` diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index b57bb96818..d22b82bc16 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -18,17 +18,31 @@ The next step is to add [add templates](http://backstage.io/docs/features/software-templates/adding-templates) to your Backstage app. -### GitHub +### Publishing defaults -For GitHub, you can configure who can see the new repositories that are created -by specifying `visibility` option. Valid options are `public`, `private` and -`internal`. The `internal` option is for GitHub Enterprise clients, which means -public within the enterprise. +Software templates can define _publish_ actions, such as `publish:github`, to +create new repositories or submit pull / merge requests to existing +repositories. You can configure the author and commit message through the +`scaffolder` configuration in `app-config.yaml`: ```yaml scaffolder: - github: - visibility: public # or 'internal' or 'private' + defaultAuthor: + name: M.C. Hammer # Defaults to `Scaffolder` + email: hammer@donthurtem.com # Defaults to `scaffolder@backstage.io` + defaultCommitMessage: "U can't touch this" # Defaults to 'Initial commit' +``` + +To configure who can see the new repositories created from software templates, +add the `repoVisibility` key within a software template: + +```yaml +- id: publish + name: Publish + action: publish:github + input: + repoUrl: '{{ parameters.repoUrl }}' + repoVisibility: public # or 'internal' or 'private' ``` ### Disabling Docker in Docker situation (Optional) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 1434d62a29..a854543acb 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -12,7 +12,7 @@ code, template in some variables, and then publish the template to some locations like GitHub or GitLab. ### Getting Started diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 81f2622312..a1dffe1580 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -6,21 +6,21 @@ description: Details around creating your own custom Software Templates Templates are stored in the **Software Catalog** under a kind `Template`. You can create your own templates with a small `yaml` definition which describes the -template and it's metadata, along with some input variables that your template +template and its metadata, along with some input variables that your template will need, and then a list of actions which are then executed by the scaffolding service. Let's take a look at a simple example: ```yaml -# Notice the v1beta2 version -apiVersion: backstage.io/v1beta2 +# Notice the v1beta3 version +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -66,8 +66,8 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} - id: fetch-docs name: Fetch Docs @@ -81,20 +81,20 @@ spec: action: publish:github input: allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' # some outputs which are saved along with the job for use in the frontend output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' + remoteUrl: ${{ steps.publish.output.remoteUrl }} + entityRef: ${{ steps.register.output.entityRef }} ``` Let's dive in and pick apart what each of these sections do and what they are. @@ -183,12 +183,12 @@ this: It would look something like the following in a template: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -315,12 +315,12 @@ template. These follow the same standard format: ```yaml - id: fetch-base # A unique id for the step name: Fetch Base # A title displayed in the frontend - if: '{{ parameters.name }}' # Optional condition, skip the step if not truthy + if: ${{ parameters.name }} # Optional condition, skip the step if not truthy action: fetch:template # An action to call input: # Input that is passed as arguments to the action handler url: ./template values: - name: '{{ parameters.name }}' + name: ${{ parameters.name }} ``` By default we ship some [built in actions](./builtin-actions.md) that you can @@ -338,22 +338,19 @@ The main two that are used are the following: ```yaml output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' # link to the remote repository - entityRef: '{{ steps.register.output.entityRef }}' # link to the entity that has been ingested to the catalog + remoteUrl: ${{ steps.publish.output.remoteUrl }} # link to the remote repository + entityRef: ${{ steps.register.output.entityRef }} # link to the entity that has been ingested to the catalog ``` ### The templating syntax -You might have noticed variables wrapped in `{{ }}` in the examples. These are -`handlebars` template strings for linking and gluing the different parts of the -template together. All the form inputs from the `parameters` section will be -available by using this template syntax (for example, -`{{ parameters.firstName }}` inserts the value of `firstName` from the -parameters). This is great for passing the values from the form into different -steps and reusing these input variables. To pass arrays or objects use the -`json` custom [helper](https://handlebarsjs.com/guide/expressions.html#helpers). -For example, `{{ json parameters.nicknames }}` will insert the result of calling -`JSON.stringify` on the value of the `nicknames` parameter. +You might have noticed variables wrapped in `${{ }}` in the examples. These are +template strings for linking and gluing the different parts of the template +together. All the form inputs from the `parameters` section will be available by +using this template syntax (for example, `${{ parameters.firstName }}` inserts +the value of `firstName` from the parameters). This is great for passing the +values from the form into different steps and reusing these input variables. +These template strings preserve the type of the parameter. As you can see above in the `Outputs` section, `actions` and `steps` can also output things. You can grab that output using `steps.$stepId.output.$property`. diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 8d25b4047f..23af83d09b 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -40,7 +40,7 @@ storage system (e.g. AWS S3, GCS or Azure Blob Storage). Read more in ## Recommended deployment -This is how we recommend deploying TechDocs in production environment. +This is how we recommend deploying TechDocs in a production environment. TechDocs Architecture diagram @@ -58,12 +58,12 @@ Similar to how it is done in the Basic setup, the TechDocs Reader requests your configured storage solution for the necessary files and returns them to TechDocs Reader. -Note about caching: We have noticed internally that some storage providers can -be quite slow, which is why we are recommending a cache that sits between the -TechDocs Reader and the Storage. - -_Feel free to suggest better ideas to us in #docs-like-code channel in Discord -or via a GitHub issue._ +Depending on your chosen cloud storage provider and its real-world proximity to +your backend server, there may be a comparably high amount of latency when +loading TechDocs sites using this deployment approach. If you encounter this, +you can optionally configure the `techdocs-backend` to cache responses in a +cache store +[supported by Backstage](../../overview/architecture-overview.md#cache). ### Security consideration diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 4148749a62..6317ae365b 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -135,6 +135,22 @@ techdocs: # the old, case-sensitive entity triplet behavior. legacyUseCaseSensitiveTripletPaths: false + # techdocs.cache is optional, and is only recommended when you've configured + # an external techdocs.publisher.type above. Also requires backend.cache to + # be configured with a valid cache store. + cache: + # Represents the number of milliseconds a statically built asset should + # stay cached. Cache invalidation is handled automatically by the frontend, + # which compares the build times in cached metadata vs. canonical storage, + # allowing long TTLs (e.g. 1 month/year) + ttl: 3600000 + + # (Optional) The time (in milliseconds) that the TechDocs backend will wait + # for a cache service to respond before continuing on as though the cached + # object was not found (e.g. when the cache sercice is unavailable). The + # default value is 1000 + readTimeout: 500 + # (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc. # You don't have to specify this anymore. diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index e5d15d5845..1d65f25d1b 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -36,7 +36,7 @@ This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted.

- create app + create app

Inside that directory, it will generate all the files and folder structure diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 02e2e4d51d..42b334c62b 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -58,7 +58,7 @@ The wizard will ask you SQLite option.

- Screenshot of the wizard asking for a name for the app, and a selection menu for the database. + Screenshot of the wizard asking for a name for the app, and a selection menu for the database.

### Run the Backstage app @@ -73,7 +73,7 @@ yarn dev ```

- Screenshot of the command output, with the message web pack compiled successfully. + Screenshot of the command output, with the message web pack compiled successfully.

It might take a little while, but as soon as the message @@ -84,7 +84,7 @@ database will be cleared when you restart the app, so you'll most likely want to carry on with the database steps.

- Screenshot of the Backstage portal. + Screenshot of the Backstage portal.

The most common next steps are to move to a persistent database, configure diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 18fdef892a..318b37f34e 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -165,6 +165,7 @@ common example being the `migrations` directory. Usage: backstage-cli backend:build [options] Options: + --minify Minify the generated code -h, --help display help for command ``` @@ -371,6 +372,7 @@ the monorepo. Usage: backstage-cli plugin:build [options] Options: + --minify Minify the generated code -h, --help display help for command ``` diff --git a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md index afda20e499..0bf8d7e581 100644 --- a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md +++ b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md @@ -7,7 +7,7 @@ authorURL: https://twitter.com/stalund **TL;DR** Today we are announcing a new Backstage feature: Software Templates. Simplify setup, standardize tooling, and deploy with the click of a button. Using automated templates, your engineers can spin up a new microservice, website, or other software component with your organization’s best practices built-in, right from the start. diff --git a/microsite/package.json b/microsite/package.json index a24de8935d..0a90cb2ec5 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -19,7 +19,7 @@ "@spotify/prettier-config": "^12.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", - "prettier": "^2.5.0", + "prettier": "^2.5.1", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config" diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 9946f4914d..b4d97d549d 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -5190,10 +5190,10 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893" - integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg== +prettier@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== prismjs@^1.22.0: version "1.25.0" diff --git a/package.json b/package.json index 9adbb1c821..2d63f92d3a 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "devDependencies": { "@changesets/cli": "^2.14.0", "@octokit/rest": "^18.12.0", - "@spotify/prettier-config": "^11.0.0", + "@spotify/prettier-config": "^12.0.0", + "@types/node": "^14.14.32", "@types/webpack": "^5.28.0", "command-exists": "^1.2.9", "cross-env": "^7.0.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index b2b50ae48b..b9a84d500c 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/app-defaults +## 0.1.2 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/core-app-api@0.2.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 7388a44ce0..fc1bca2104 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "0.1.1", + "version": "0.1.2", "private": false, "publishConfig": { "access": "public", @@ -29,23 +29,25 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*" + "@types/react": "^16.13.1 || ^17.0.0" }, "files": [ "dist" diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index f6f6049d68..3f3ac75853 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,70 @@ # example-app +## 0.2.56 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-cloudbuild@0.2.29 + - @backstage/plugin-catalog@0.7.4 + - @backstage/core-app-api@0.2.0 + - @backstage/plugin-scaffolder@0.11.14 + - @backstage/plugin-kubernetes@0.5.0 + - @backstage/app-defaults@0.1.2 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-api-docs@0.6.18 + - @backstage/plugin-azure-devops@0.1.6 + - @backstage/plugin-badges@0.2.16 + - @backstage/plugin-catalog-graph@0.2.3 + - @backstage/plugin-catalog-import@0.7.5 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/plugin-circleci@0.2.31 + - @backstage/plugin-code-coverage@0.1.19 + - @backstage/plugin-cost-insights@0.11.13 + - @backstage/plugin-explore@0.3.22 + - @backstage/plugin-gcp-projects@0.3.10 + - @backstage/plugin-github-actions@0.4.26 + - @backstage/plugin-graphiql@0.2.24 + - @backstage/plugin-home@0.4.7 + - @backstage/plugin-jenkins@0.5.14 + - @backstage/plugin-kafka@0.2.22 + - @backstage/plugin-lighthouse@0.2.31 + - @backstage/plugin-newrelic@0.3.10 + - @backstage/plugin-org@0.3.30 + - @backstage/plugin-pagerduty@0.3.19 + - @backstage/plugin-rollbar@0.3.20 + - @backstage/plugin-search@0.5.1 + - @backstage/plugin-sentry@0.3.30 + - @backstage/plugin-shortcuts@0.1.15 + - @backstage/plugin-tech-insights@0.1.1 + - @backstage/plugin-tech-radar@0.4.13 + - @backstage/plugin-techdocs@0.12.9 + - @backstage/plugin-todo@0.1.16 + - @backstage/plugin-user-settings@0.3.13 + - @backstage/cli@0.10.1 + +## 0.2.55 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-azure-devops@0.1.5 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/plugin-user-settings@0.3.12 + - @backstage/plugin-api-docs@0.6.16 + - @backstage/cli@0.10.0 + - @backstage/plugin-circleci@0.2.30 + - @backstage/core-plugin-api@0.2.2 + - @backstage/plugin-search@0.5.0 + - @backstage/plugin-tech-insights@0.1.0 + - @backstage/core-app-api@0.1.24 + - @backstage/plugin-kubernetes@0.4.22 + - @backstage/plugin-scaffolder@0.11.13 + - @backstage/plugin-techdocs@0.12.8 + ## 0.2.53 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 09141f7cf7..99b90952e3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,62 +1,63 @@ { "name": "example-app", - "version": "0.2.53", + "version": "0.2.56", "private": true, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.1", + "@backstage/app-defaults": "^0.1.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-api-docs": "^0.6.14", - "@backstage/plugin-azure-devops": "^0.1.4", - "@backstage/plugin-badges": "^0.2.14", - "@backstage/plugin-catalog": "^0.7.3", - "@backstage/plugin-catalog-graph": "^0.2.2", - "@backstage/plugin-catalog-import": "^0.7.4", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/plugin-circleci": "^0.2.29", - "@backstage/plugin-cloudbuild": "^0.2.28", - "@backstage/plugin-code-coverage": "^0.1.18", - "@backstage/plugin-cost-insights": "^0.11.11", - "@backstage/plugin-explore": "^0.3.21", - "@backstage/plugin-gcp-projects": "^0.3.9", - "@backstage/plugin-github-actions": "^0.4.24", - "@backstage/plugin-graphiql": "^0.2.21", - "@backstage/plugin-home": "^0.4.6", - "@backstage/plugin-jenkins": "^0.5.12", - "@backstage/plugin-kafka": "^0.2.21", - "@backstage/plugin-kubernetes": "^0.4.20", - "@backstage/plugin-lighthouse": "^0.2.30", - "@backstage/plugin-newrelic": "^0.3.9", - "@backstage/plugin-org": "^0.3.28", - "@backstage/plugin-pagerduty": "0.3.18", - "@backstage/plugin-rollbar": "^0.3.19", - "@backstage/plugin-scaffolder": "^0.11.11", - "@backstage/plugin-search": "^0.4.18", - "@backstage/plugin-sentry": "^0.3.29", - "@backstage/plugin-shortcuts": "^0.1.14", - "@backstage/plugin-tech-radar": "^0.4.12", - "@backstage/plugin-techdocs": "^0.12.6", - "@backstage/plugin-todo": "^0.1.15", - "@backstage/plugin-user-settings": "^0.3.11", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-api-docs": "^0.6.18", + "@backstage/plugin-azure-devops": "^0.1.6", + "@backstage/plugin-apache-airflow": "^0.0.0", + "@backstage/plugin-badges": "^0.2.16", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/plugin-catalog-graph": "^0.2.3", + "@backstage/plugin-catalog-import": "^0.7.5", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-circleci": "^0.2.31", + "@backstage/plugin-cloudbuild": "^0.2.29", + "@backstage/plugin-code-coverage": "^0.1.19", + "@backstage/plugin-cost-insights": "^0.11.13", + "@backstage/plugin-explore": "^0.3.22", + "@backstage/plugin-gcp-projects": "^0.3.10", + "@backstage/plugin-github-actions": "^0.4.26", + "@backstage/plugin-graphiql": "^0.2.24", + "@backstage/plugin-home": "^0.4.7", + "@backstage/plugin-jenkins": "^0.5.14", + "@backstage/plugin-kafka": "^0.2.22", + "@backstage/plugin-kubernetes": "^0.5.0", + "@backstage/plugin-lighthouse": "^0.2.31", + "@backstage/plugin-newrelic": "^0.3.10", + "@backstage/plugin-org": "^0.3.30", + "@backstage/plugin-pagerduty": "0.3.19", + "@backstage/plugin-rollbar": "^0.3.20", + "@backstage/plugin-scaffolder": "^0.11.14", + "@backstage/plugin-search": "^0.5.1", + "@backstage/plugin-sentry": "^0.3.30", + "@backstage/plugin-shortcuts": "^0.1.15", + "@backstage/plugin-tech-radar": "^0.4.13", + "@backstage/plugin-techdocs": "^0.12.9", + "@backstage/plugin-todo": "^0.1.16", + "@backstage/plugin-user-settings": "^0.3.13", "@backstage/search-common": "^0.2.0", - "@backstage/theme": "^0.2.11", + "@backstage/plugin-tech-insights": "^0.1.1", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@roadiehq/backstage-plugin-buildkite": "^1.0.8", "@roadiehq/backstage-plugin-github-insights": "^1.1.23", "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13", "@roadiehq/backstage-plugin-travis-ci": "^1.0.11", "history": "^5.0.0", "prop-types": "^15.7.2", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", @@ -64,7 +65,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.24", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 7e5d3db926..59f107c70d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -87,6 +87,7 @@ import { providers } from './identityProviders'; import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; +import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; const app = createApp({ apis, @@ -218,6 +219,7 @@ const routes = ( /> } /> } /> + } /> ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e1496b556c..2d9b9b97f1 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -29,7 +29,10 @@ import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { GraphiQLIcon } from '@backstage/plugin-graphiql'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearchModal } from '@backstage/plugin-search'; +import { + SidebarSearchModal, + SearchContextProvider, +} from '@backstage/plugin-search'; import { Shortcuts } from '@backstage/plugin-shortcuts'; import { Sidebar, @@ -80,7 +83,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + + + {/* Global nav, not org-specific */} diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 3b9b381380..9810f49386 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -106,10 +106,7 @@ import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; import { EntityTodoContent } from '@backstage/plugin-todo'; import { Button, Grid } from '@material-ui/core'; import BadgeIcon from '@material-ui/icons/CallToAction'; -import { - EntityBuildkiteContent, - isBuildkiteAvailable, -} from '@roadiehq/backstage-plugin-buildkite'; + import { EntityGithubInsightsContent, EntityGithubInsightsLanguagesCard, @@ -168,10 +165,6 @@ export const cicdContent = ( - - - - diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 869bf46019..ad3fb33365 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/backend-common +## 0.9.13 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- 5a008576c4: Add possibility to use custom error handler +- 98a9c35f0c: Add options argument to support additional database migrations configuration +- 6298de32dd: Add knexConfig config section + +## 0.9.12 + +### Patch Changes + +- 905dd952ac: Create a `TokenManager` interface and `ServerTokenManager` implementation to generate and validate server tokens for authenticated backend-to-backend API requests. +- 6b500622d5: Move to using node-fetch internally instead of cross-fetch +- 54989b671d: Fixed a potential crash in the log redaction code +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/config-loader@0.8.1 + ## 0.9.11 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9c66c0573e..d20f568e98 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -175,23 +175,22 @@ export function createStatusCheckRouter(options: { // @public (undocumented) export class DatabaseManager { forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): DatabaseManager; + static fromConfig( + config: Config, + options?: DatabaseManagerOptions, + ): DatabaseManager; } +// @public +export type DatabaseManagerOptions = { + migrations?: PluginDatabaseManager['migrations']; +}; + // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { - constructor({ dockerClient }: { dockerClient: Docker }); + constructor(options: { dockerClient: Docker }); // (undocumented) - runContainer({ - imageName, - command, - args, - logStream, - mountDirs, - workingDir, - envVars, - pullImage, - }: RunContainerOptions): Promise; + runContainer(options: RunContainerOptions): Promise; } // @public @@ -227,34 +226,17 @@ export function getVoidLogger(): winston.Logger; // @public (undocumented) export class Git { // (undocumented) - add({ dir, filepath }: { dir: string; filepath: string }): Promise; + add(options: { dir: string; filepath: string }): Promise; // (undocumented) - addRemote({ - dir, - url, - remote, - }: { + addRemote(options: { dir: string; remote: string; url: string; }): Promise; // (undocumented) - clone({ - url, - dir, - ref, - }: { - url: string; - dir: string; - ref?: string; - }): Promise; + clone(options: { url: string; dir: string; ref?: string }): Promise; // (undocumented) - commit({ - dir, - message, - author, - committer, - }: { + commit(options: { dir: string; message: string; author: { @@ -267,41 +249,22 @@ export class Git { }; }): Promise; // (undocumented) - currentBranch({ - dir, - fullName, - }: { + currentBranch(options: { dir: string; fullName?: boolean; }): Promise; // (undocumented) - fetch({ dir, remote }: { dir: string; remote?: string }): Promise; + fetch(options: { dir: string; remote?: string }): Promise; // (undocumented) - static fromAuth: ({ - username, - password, - logger, - }: { - username?: string | undefined; - password?: string | undefined; - logger?: Logger_2 | undefined; + static fromAuth: (options: { + username?: string; + password?: string; + logger?: Logger_2; }) => Git; // (undocumented) - init({ - dir, - defaultBranch, - }: { - dir: string; - defaultBranch?: string; - }): Promise; + init(options: { dir: string; defaultBranch?: string }): Promise; // (undocumented) - merge({ - dir, - theirs, - ours, - author, - committer, - }: { + merge(options: { dir: string; theirs: string; ours?: string; @@ -315,17 +278,11 @@ export class Git { }; }): Promise; // (undocumented) - push({ dir, remote }: { dir: string; remote: string }): Promise; + push(options: { dir: string; remote: string }): Promise; // (undocumented) - readCommit({ - dir, - sha, - }: { - dir: string; - sha: string; - }): Promise; + readCommit(options: { dir: string; sha: string }): Promise; // (undocumented) - resolveRef({ dir, ref }: { dir: string; ref: string }): Promise; + resolveRef(options: { dir: string; ref: string }): Promise; } // @public @@ -395,6 +352,9 @@ export type PluginCacheManager = { // @public export interface PluginDatabaseManager { getClient(): Promise; + migrations?: { + skip?: boolean; + }; } // @public @@ -561,6 +521,8 @@ export type ServiceBuilder = { setRequestLoggingHandler( requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + disableDefaultErrorHandler(): ServiceBuilder; start(): Promise; }; @@ -623,8 +585,8 @@ export type UrlReaderPredicateTuple = { // @public export class UrlReaders { - static create({ logger, config, factories }: UrlReadersOptions): UrlReader; - static default({ logger, config, factories }: UrlReadersOptions): UrlReader; + static create(options: UrlReadersOptions): UrlReader; + static default(options: UrlReadersOptions): UrlReader; } // @public (undocumented) @@ -642,4 +604,8 @@ export function useHotCleanup( // @public export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; + +// Warnings were encountered during analysis: +// +// src/database/types.d.ts:23:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration ``` diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index bdc7740c13..fe3c7a5bec 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -96,6 +96,12 @@ export interface Config { * @default database */ pluginDivisionMode?: 'database' | 'schema'; + /** + * Arbitrary config object to pass to knex when initializing + * (https://knexjs.org/#Installation-client). Most notable is the debug + * and asyncStackTraces booleans + */ + knexConfig?: object; /** Plugin specific database configuration and client override */ plugin?: { [pluginId: string]: { @@ -111,6 +117,14 @@ export interface Config { * Defaults to base config if unspecified. */ ensureExists?: boolean; + /** + * Arbitrary config object to pass to knex when initializing + * (https://knexjs.org/#Installation-client). Most notable is the + * debug and asyncStackTraces booleans. + * + * This is merged recursively into the base knexConfig + */ + knexConfig?: object; }; }; }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 275ffa5d93..130a3fcaa5 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.11", + "version": "0.9.13", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,9 +31,9 @@ "dependencies": { "@backstage/cli-common": "^0.1.6", "@backstage/config": "^0.1.11", - "@backstage/config-loader": "^0.8.0", + "@backstage/config-loader": "^0.8.1", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", "@lerna/project": "^4.0.0", @@ -81,8 +81,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index f2fe855234..bd2e7b9f71 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -36,25 +36,45 @@ describe('DatabaseManager', () => { afterEach(() => jest.resetAllMocks()); describe('DatabaseManager.fromConfig', () => { - it('accesses the backend.database key', () => { - const config = new ConfigReader({ - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', - }, + const backendConfig = { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', }, }, - }); + }, + }; + + it('accesses the backend.database key', () => { + const config = new ConfigReader(backendConfig); const getConfigSpy = jest.spyOn(config, 'getConfig'); DatabaseManager.fromConfig(config); expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); + + it('handles default options', () => { + const config = new ConfigReader(backendConfig); + const database = DatabaseManager.fromConfig(config); + const client = database.forPlugin('test'); + + expect(client.migrations?.skip).toBe(false); + }); + + it('handles migrations options', () => { + const config = new ConfigReader(backendConfig); + const database = DatabaseManager.fromConfig(config, { + migrations: { skip: true }, + }); + const client = database.forPlugin('test'); + + expect(client.migrations?.skip).toBe(true); + }); }); describe('DatabaseManager.forPlugin', () => { @@ -505,5 +525,42 @@ describe('DatabaseManager', () => { 'database_name_overriden', ); }); + + it('fetches and merges additional knex config', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + database: 'foodb', + }, + knexConfig: { + something: false, + }, + plugin: { + testdbname: { + knexConfig: { + debug: true, + }, + }, + }, + }, + }, + }), + ); + await testManager.forPlugin('testdbname').getClient(); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [baseConfig] = mockCalls[0]; + + expect(baseConfig.data).toEqual( + expect.objectContaining({ + debug: true, + something: false, + }), + ); + }); }); }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index cf5e801d66..8e23a219ec 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { Knex } from 'knex'; -import { omit } from 'lodash'; import { Config, ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; +import { Knex } from 'knex'; +import { merge, omit } from 'lodash'; +import { mergeDatabaseConfig } from './config'; import { - createNameOverride, - ensureDatabaseExists, - normalizeConnection, - createSchemaOverride, - ensureSchemaExists, createDatabaseClient, + createNameOverride, + createSchemaOverride, + ensureDatabaseExists, + ensureSchemaExists, + normalizeConnection, } from './connection'; import { PluginDatabaseManager } from './types'; -import { mergeDatabaseConfig } from './config'; /** * Provides a config lookup path for a plugin's config block. @@ -36,6 +36,15 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } +/** + * Configuration options object. + * + * @public + */ +export type DatabaseManagerOptions = { + migrations?: PluginDatabaseManager['migrations']; +}; + /** @public */ export class DatabaseManager { /** @@ -47,19 +56,25 @@ export class DatabaseManager { * names if config is not provided. * * @param config - The loaded application configuration. + * @param options - An optional configuration object. */ - static fromConfig(config: Config): DatabaseManager { + static fromConfig( + config: Config, + options?: DatabaseManagerOptions, + ): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); return new DatabaseManager( databaseConfig, databaseConfig.getOptionalString('prefix'), + options, ); } private constructor( private readonly config: Config, private readonly prefix: string = 'backstage_plugin_', + private readonly options?: DatabaseManagerOptions, ) {} /** @@ -76,6 +91,10 @@ export class DatabaseManager { getClient(): Promise { return _this.getDatabase(pluginId); }, + migrations: { + skip: false, + ..._this.options?.migrations, + }, }; } @@ -138,6 +157,24 @@ export class DatabaseManager { }; } + /** + * Provides the knexConfig which should be used for a given plugin. + * + * @param pluginId Plugin to get the knexConfig for + * @returns the merged kexConfig value or undefined if it isn't specified + */ + private getAdditionalKnexConfig(pluginId: string): JsonObject | undefined { + const pluginConfig = this.config + .getOptionalConfig(`${pluginPath(pluginId)}.knexConfig`) + ?.get(); + + const baseConfig = this.config + .getOptionalConfig('knexConfig') + ?.get(); + + return merge(baseConfig, pluginConfig); + } + private getEnsureExistsConfig(pluginId: string): boolean { const baseConfig = this.config.getOptionalBoolean('ensureExists') ?? true; return ( @@ -200,6 +237,7 @@ export class DatabaseManager { const { client } = this.getClientType(pluginId); return { + ...this.getAdditionalKnexConfig(pluginId), client, connection: this.getConnectionConfig(pluginId), }; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index e96f86980b..344f1088b8 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -30,6 +30,18 @@ export interface PluginDatabaseManager { * stores so that plugins are discouraged from database integration. */ getClient(): Promise; + + /** + * This property is used to control the behavior of database migrations. + */ + migrations?: { + /** + * skip database migrations. Useful if connecting to a read-only database. + * + * @default false + */ + skip?: boolean; + }; } /** diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 2a4226f88f..12db7d42a1 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -54,10 +54,13 @@ export function setRootLoggerRedactionList(redactionList: string[]) { * and replaces them with the corresponding identifier. */ function redactLogLine(info: winston.Logform.TransformableInfo) { - // TODO(hhogg): The logger is created before the config is loaded, - // because the logger is needed in the config loader. There is a risk of - // a secret being logged out during the config loading stage. - if (redactionRegExp) { + // TODO(hhogg): The logger is created before the config is loaded, because the + // logger is needed in the config loader. There is a risk of a secret being + // logged out during the config loading stage. + // TODO(freben): Added a check that info.message actually was a string, + // because it turned out that this was not necessarily guaranteed. + // https://github.com/backstage/backstage/issues/8306 + if (redactionRegExp && typeof info.message === 'string') { info.message = info.message.replace(redactionRegExp, '[REDACTED]'); } diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index 7120a9570f..a920ec080b 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -46,7 +46,8 @@ export class UrlReaders { /** * Creates a UrlReader without any known types. */ - static create({ logger, config, factories }: UrlReadersOptions): UrlReader { + static create(options: UrlReadersOptions): UrlReader { + const { logger, config, factories } = options; const mux = new UrlReaderPredicateMux(logger); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config, @@ -68,7 +69,8 @@ export class UrlReaders { * * Any additional factories passed will be loaded before the default ones. */ - static default({ logger, config, factories = [] }: UrlReadersOptions) { + static default(options: UrlReadersOptions) { + const { logger, config, factories = [] } = options; return UrlReaders.create({ logger, config, diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 00b103b240..372ecb0260 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import git, { ProgressCallback, MergeResult, @@ -42,44 +43,32 @@ export class Git { }, ) {} - async add({ - dir, - filepath, - }: { - dir: string; - filepath: string; - }): Promise { + async add(options: { dir: string; filepath: string }): Promise { + const { dir, filepath } = options; this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`); return git.add({ fs, dir, filepath }); } - async addRemote({ - dir, - url, - remote, - }: { + async addRemote(options: { dir: string; remote: string; url: string; }): Promise { + const { dir, url, remote } = options; this.config.logger?.info( `Creating new remote {dir=${dir},remote=${remote},url=${url}}`, ); return git.addRemote({ fs, dir, remote, url }); } - async commit({ - dir, - message, - author, - committer, - }: { + async commit(options: { dir: string; message: string; author: { name: string; email: string }; committer: { name: string; email: string }; }): Promise { + const { dir, message, author, committer } = options; this.config.logger?.info( `Committing file to repo {dir=${dir},message=${message}}`, ); @@ -87,15 +76,12 @@ export class Git { return git.commit({ fs, dir, message, author, committer }); } - async clone({ - url, - dir, - ref, - }: { + async clone(options: { url: string; dir: string; ref?: string; }): Promise { + const { url, dir, ref } = options; this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`); return git.clone({ fs, @@ -114,51 +100,35 @@ export class Git { } // https://isomorphic-git.org/docs/en/currentBranch - async currentBranch({ - dir, - fullName, - }: { + async currentBranch(options: { dir: string; fullName?: boolean; }): Promise { - const fullname = fullName ?? false; - return git.currentBranch({ fs, dir, fullname }) as Promise< + const { dir, fullName = false } = options; + return git.currentBranch({ fs, dir, fullname: fullName }) as Promise< string | undefined >; } // https://isomorphic-git.org/docs/en/fetch - async fetch({ - dir, - remote, - }: { - dir: string; - remote?: string; - }): Promise { - const remoteValue = remote ?? 'origin'; + async fetch(options: { dir: string; remote?: string }): Promise { + const { dir, remote = 'origin' } = options; this.config.logger?.info( - `Fetching remote=${remoteValue} for repository {dir=${dir}}`, + `Fetching remote=${remote} for repository {dir=${dir}}`, ); await git.fetch({ fs, http, dir, - remote: remoteValue, + remote, onProgress: this.onProgressHandler(), - headers: { - 'user-agent': 'git/@isomorphic-git', - }, + headers: { 'user-agent': 'git/@isomorphic-git' }, onAuth: this.onAuth, }); } - async init({ - dir, - defaultBranch = 'master', - }: { - dir: string; - defaultBranch?: string; - }): Promise { + async init(options: { dir: string; defaultBranch?: string }): Promise { + const { dir, defaultBranch = 'master' } = options; this.config.logger?.info(`Init git repository {dir=${dir}}`); return git.init({ @@ -169,19 +139,14 @@ export class Git { } // https://isomorphic-git.org/docs/en/merge - async merge({ - dir, - theirs, - ours, - author, - committer, - }: { + async merge(options: { dir: string; theirs: string; ours?: string; author: { name: string; email: string }; committer: { name: string; email: string }; }): Promise { + const { dir, theirs, ours, author, committer } = options; this.config.logger?.info( `Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`, ); @@ -197,7 +162,8 @@ export class Git { }); } - async push({ dir, remote }: { dir: string; remote: string }) { + async push(options: { dir: string; remote: string }) { + const { dir, remote } = options; this.config.logger?.info( `Pushing directory to remote {dir=${dir},remote=${remote}}`, ); @@ -215,24 +181,17 @@ export class Git { } // https://isomorphic-git.org/docs/en/readCommit - async readCommit({ - dir, - sha, - }: { + async readCommit(options: { dir: string; sha: string; }): Promise { + const { dir, sha } = options; return git.readCommit({ fs, dir, oid: sha }); } // https://isomorphic-git.org/docs/en/resolveRef - async resolveRef({ - dir, - ref, - }: { - dir: string; - ref: string; - }): Promise { + async resolveRef(options: { dir: string; ref: string }): Promise { + const { dir, ref } = options; return git.resolveRef({ fs, dir, ref }); } @@ -256,13 +215,12 @@ export class Git { }; }; - static fromAuth = ({ - username, - password, - logger, - }: { + static fromAuth = (options: { username?: string; password?: string; logger?: Logger; - }) => new Git({ username, password, logger }); + }) => { + const { username, password, logger } = options; + return new Git({ username, password, logger }); + }; } diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index bd97f444de..6229cbb683 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { applyCspDirectives } from './ServiceBuilderImpl'; +import { NextFunction, Request, Response } from 'express'; +import { applyCspDirectives, ServiceBuilderImpl } from './ServiceBuilderImpl'; describe('ServiceBuilderImpl', () => { describe('applyCspDirectives', () => { @@ -33,4 +34,27 @@ describe('ServiceBuilderImpl', () => { expect(result!['upgrade-insecure-requests']).toBeUndefined(); }); }); + + describe('setCustomErrorHandler', () => { + it('check if custom error handler is undefined', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); + expect(serviceBuilderProto.errorHandler).toBeUndefined(); + }); + + it('adds custom error handler', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); + const customErrorHandler = ( + error: Error, + _req: Request, + _res: Response, + next: NextFunction, + ) => { + next(error); + }; + serviceBuilderProto.setErrorHandler(customErrorHandler); + expect(serviceBuilderProto.errorHandler).toEqual(customErrorHandler); + }); + }); }); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index b085cccfd6..54f145539f 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; -import express, { Router } from 'express'; +import express, { Router, ErrorRequestHandler } from 'express'; import helmet from 'helmet'; import * as http from 'http'; import stoppable from 'stoppable'; @@ -25,7 +25,7 @@ import { Logger } from 'winston'; import { useHotCleanup } from '../../hot'; import { getRootLogger } from '../../logging'; import { - errorHandler, + errorHandler as defaultErrorHandler, notFoundHandler, requestLoggingHandler as defaultRequestLoggingHandler, } from '../../middleware'; @@ -66,6 +66,8 @@ export class ServiceBuilderImpl implements ServiceBuilder { private httpsSettings: HttpsSettings | undefined; private routers: [string, Router][]; private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; + private errorHandler: ErrorRequestHandler | undefined; + private useDefaultErrorHandler: boolean; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -73,6 +75,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { constructor(moduleRef: NodeModule) { this.routers = []; this.module = moduleRef; + this.useDefaultErrorHandler = true; } loadConfig(config: Config): ServiceBuilder { @@ -152,6 +155,16 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + setErrorHandler(errorHandler: ErrorRequestHandler) { + this.errorHandler = errorHandler; + return this; + } + + disableDefaultErrorHandler() { + this.useDefaultErrorHandler = false; + return this; + } + async start(): Promise { const app = express(); const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = @@ -169,7 +182,14 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(root, route); } app.use(notFoundHandler()); - app.use(errorHandler()); + + if (this.errorHandler) { + app.use(this.errorHandler); + } + + if (this.useDefaultErrorHandler) { + app.use(defaultErrorHandler()); + } const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 2ad379f31e..3e94196006 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import cors from 'cors'; -import { Router, RequestHandler } from 'express'; +import { Router, RequestHandler, ErrorRequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; @@ -98,6 +98,21 @@ export type ServiceBuilder = { requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + /** + * Sets an additional errorHandler to run before the defaultErrorHandler. + * + * For execution of only the custom error handler make sure to also invoke disableDefaultErrorHandler() + * otherwise the defaultErrorHandler is executed at the end of the error middleware chain. + * + * @param errorHandler - an error handler + */ + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + + /** + * Disables the default error handler + */ + disableDefaultErrorHandler(): ServiceBuilder; + /** * Starts the server using the given settings. */ diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index b81bd995ca..424913f316 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -28,20 +28,22 @@ export type UserOptions = { export class DockerContainerRunner implements ContainerRunner { private readonly dockerClient: Docker; - constructor({ dockerClient }: { dockerClient: Docker }) { - this.dockerClient = dockerClient; + constructor(options: { dockerClient: Docker }) { + this.dockerClient = options.dockerClient; } - async runContainer({ - imageName, - command, - args, - logStream = new PassThrough(), - mountDirs = {}, - workingDir, - envVars = {}, - pullImage = true, - }: RunContainerOptions) { + async runContainer(options: RunContainerOptions) { + const { + imageName, + command, + args, + logStream = new PassThrough(), + mountDirs = {}, + workingDir, + envVars = {}, + pullImage = true, + } = options; + // Show a better error message when Docker is unavailable. try { await this.dockerClient.ping(); diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 2bc6474615..a22895140e 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", @@ -43,8 +43,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.0", + "@backstage/backend-test-utils": "^0.1.10", + "@backstage/cli": "^0.10.0", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 9927aa3cc7..584187b9d3 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-test-utils +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.12 + - @backstage/cli@0.10.0 + ## 0.1.9 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index c3d4f730b7..2c1724e50f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/cli": "^0.9.0", + "@backstage/backend-common": "^0.9.12", + "@backstage/cli": "^0.10.0", "@backstage/config": "^0.1.9", "knex": "^0.95.1", "mysql2": "^2.2.5", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 20e0d7885b..fa8632e2ef 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,46 @@ # example-backend +## 0.2.56 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.5.0 + - @backstage/plugin-scaffolder-backend@0.15.16 + - @backstage/plugin-kubernetes-backend@0.4.0 + - @backstage/backend-common@0.9.13 + - @backstage/plugin-catalog-backend@0.19.1 + - @backstage/plugin-search-backend@0.2.8 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.6 + - @backstage/plugin-search-backend-module-pg@0.2.2 + - @backstage/plugin-techdocs-backend@0.12.0 + - @backstage/plugin-todo-backend@0.1.15 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.0 + - @backstage/plugin-azure-devops-backend@0.2.4 + - example-app@0.2.56 + +## 0.2.55 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/plugin-scaffolder-backend@0.15.15 + - @backstage/plugin-auth-backend@0.4.10 + - @backstage/plugin-kubernetes-backend@0.3.20 + - @backstage/plugin-badges-backend@0.1.13 + - @backstage/plugin-catalog-backend@0.19.0 + - @backstage/plugin-code-coverage-backend@0.1.16 + - @backstage/plugin-jenkins-backend@0.1.9 + - @backstage/plugin-tech-insights-backend@0.1.3 + - @backstage/plugin-techdocs-backend@0.11.0 + - @backstage/plugin-todo-backend@0.1.14 + - @backstage/backend-common@0.9.12 + - @backstage/plugin-azure-devops-backend@0.2.3 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.2 + - @backstage/plugin-tech-insights-node@0.1.1 + - example-app@0.2.55 + ## 0.2.54 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index bf18728b9f..ae61bc7a45 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.54", + "version": "0.2.56", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,39 +24,39 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/integration": "^0.6.8", + "@backstage/integration": "^0.6.10", "@backstage/plugin-app-backend": "^0.3.19", - "@backstage/plugin-auth-backend": "^0.4.9", - "@backstage/plugin-azure-devops-backend": "^0.2.2", - "@backstage/plugin-badges-backend": "^0.1.12", - "@backstage/plugin-catalog-backend": "^0.18.0", - "@backstage/plugin-code-coverage-backend": "^0.1.15", + "@backstage/plugin-auth-backend": "^0.5.0", + "@backstage/plugin-azure-devops-backend": "^0.2.4", + "@backstage/plugin-badges-backend": "^0.1.13", + "@backstage/plugin-catalog-backend": "^0.19.1", + "@backstage/plugin-code-coverage-backend": "^0.1.16", "@backstage/plugin-graphql-backend": "^0.1.9", - "@backstage/plugin-jenkins-backend": "^0.1.8", - "@backstage/plugin-kubernetes-backend": "^0.3.19", + "@backstage/plugin-jenkins-backend": "^0.1.9", + "@backstage/plugin-kubernetes-backend": "^0.4.0", "@backstage/plugin-kafka-backend": "^0.2.12", "@backstage/plugin-proxy-backend": "^0.2.14", "@backstage/plugin-rollbar-backend": "^0.1.16", - "@backstage/plugin-scaffolder-backend": "^0.15.14", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.7", - "@backstage/plugin-search-backend": "^0.2.7", + "@backstage/plugin-scaffolder-backend": "^0.15.16", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.0", + "@backstage/plugin-search-backend": "^0.2.8", "@backstage/plugin-search-backend-node": "^0.4.2", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.5", - "@backstage/plugin-search-backend-module-pg": "^0.2.1", - "@backstage/plugin-techdocs-backend": "^0.10.9", - "@backstage/plugin-tech-insights-backend": "^0.1.2", - "@backstage/plugin-tech-insights-node": "^0.1.0", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.1", - "@backstage/plugin-todo-backend": "^0.1.13", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.6", + "@backstage/plugin-search-backend-module-pg": "^0.2.2", + "@backstage/plugin-techdocs-backend": "^0.12.0", + "@backstage/plugin-tech-insights-backend": "^0.1.3", + "@backstage/plugin-tech-insights-node": "^0.1.1", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.2", + "@backstage/plugin-todo-backend": "^0.1.15", "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", - "example-app": "^0.2.53", + "example-app": "^0.2.56", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -68,7 +68,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index eb1e0502db..c32bbccbb0 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -29,6 +29,7 @@ export default async function createPlugin({ config, discovery, reader, + cache, }: PluginEnvironment): Promise { // Preparers are responsible for fetching source files for documentation. const preparers = await Preparers.fromConfig(config, { @@ -64,5 +65,6 @@ export default async function createPlugin({ logger, config, discovery, + cache, }); } diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 4ab727b6b3..28a3d5f969 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -35,7 +35,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 0ba07aec3c..60331d9dc8 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -42,7 +42,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ed8525a2cc..0c30e8b251 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/cli +## 0.10.1 + +### Patch Changes + +- 0ebb05eee2: Add cli option to minify the generated code of a plugin or backend package + + ``` + backstage-cli plugin:build --minify + backstage-cli backend:build --minify + ``` + +- cd450844f6: Updated the frontend plugin template to put React dependencies in `peerDependencies` by default, as well as allowing both React v16 and v17. This change can be applied to existing plugins by running `yarn backstage-cli plugin:diff` within the plugin package directory. + +## 0.10.0 + +### Minor Changes + +- ea99ef5198: Remove the `backend:build-image` command from the CLI and added more deprecation warnings to other deprecated fields like `--lax` and `remove-plugin` + +### Patch Changes + +- e7230ef814: Bump react-dev-utils to v12 +- 416b68675d: build(dependencies): bump `style-loader` from 1.2.1 to 3.3.1 +- Updated dependencies + - @backstage/config-loader@0.8.1 + ## 0.9.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d40e2788c..984c1c70d1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.9.1", + "version": "0.10.1", "private": false, "publishConfig": { "access": "public" @@ -30,7 +30,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.6", "@backstage/config": "^0.1.11", - "@backstage/config-loader": "^0.8.0", + "@backstage/config-loader": "^0.8.1", "@backstage/errors": "^0.1.5", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", @@ -38,9 +38,9 @@ "@lerna/project": "^4.0.0", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-json": "^4.0.2", + "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.0.0", - "@rollup/plugin-yaml": "^3.0.0", + "@rollup/plugin-yaml": "^3.1.0", "@spotify/eslint-config-base": "^12.0.0", "@spotify/eslint-config-react": "^12.0.0", "@spotify/eslint-config-typescript": "^12.0.0", @@ -62,7 +62,7 @@ "css-loader": "^5.2.6", "dashify": "^2.0.0", "diff": "^5.0.0", - "esbuild": "^0.8.56", + "esbuild": "^0.14.1", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", "eslint-formatter-friendly": "^7.0.0", @@ -89,14 +89,13 @@ "ora": "^5.3.0", "postcss": "^8.1.0", "process": "^0.11.10", - "react": "^16.0.0", "react-dev-utils": "^12.0.0-next.47", "react-hot-loader": "^4.12.21", "recursive-readdir": "^2.2.2", "replace-in-file": "^6.0.0", - "rollup": "2.44.x", - "rollup-plugin-dts": "^3.0.1", - "rollup-plugin-esbuild": "2.6.x", + "rollup": "^2.60.2", + "rollup-plugin-dts": "^4.0.1", + "rollup-plugin-esbuild": "^4.7.2", "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", @@ -117,14 +116,14 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", + "@backstage/theme": "^0.2.14", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/backend/build.ts b/packages/cli/src/commands/backend/build.ts index 6cb1347b1e..8c25f56746 100644 --- a/packages/cli/src/commands/backend/build.ts +++ b/packages/cli/src/commands/backend/build.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -export default async () => { +export default async (cmd: Command) => { await buildPackage({ outputs: new Set([Output.cjs, Output.types]), + minify: cmd.minify, }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4eb34823ce..94a8104ff1 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -47,6 +47,7 @@ export function registerCommands(program: CommanderStatic) { program .command('backend:build') .description('Build a backend plugin') + .option('--minify', 'Minify the generated code') .action(lazy(() => import('./backend/build').then(m => m.default))); program @@ -116,6 +117,7 @@ export function registerCommands(program: CommanderStatic) { program .command('plugin:build') .description('Build a plugin') + .option('--minify', 'Minify the generated code') .action(lazy(() => import('./plugin/build').then(m => m.default))); program diff --git a/packages/cli/src/commands/plugin/build.ts b/packages/cli/src/commands/plugin/build.ts index 8a63a017a2..9c5e173989 100644 --- a/packages/cli/src/commands/plugin/build.ts +++ b/packages/cli/src/commands/plugin/build.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -export default async () => { +export default async (cmd: Command) => { await buildPackage({ outputs: new Set([Output.esm, Output.types]), + minify: cmd.minify, }); }; diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 3c95a87bdd..ba60e8fea9 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -100,6 +100,7 @@ export const makeConfigs = async ( }), esbuild({ target: 'es2019', + minify: options.minify, }), ], }); diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index f75d1b1dda..e88c388013 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -22,4 +22,5 @@ export enum Output { export type BuildOptions = { outputs: Set; + minify?: boolean; }; diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index f8bbfa09ce..5bd90e3d22 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -75,7 +75,9 @@ class PackageJsonHandler { await this.syncScripts(); await this.syncPublishConfig(); await this.syncDependencies('dependencies'); + await this.syncDependencies('peerDependencies', true); await this.syncDependencies('devDependencies'); + await this.syncReactDeps(); } // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. @@ -207,12 +209,12 @@ class PackageJsonHandler { } } - private async syncDependencies(fieldName: string) { + private async syncDependencies(fieldName: string, required: boolean = false) { const pkgDeps = this.pkg[fieldName]; const targetDeps = (this.targetPkg[fieldName] = this.targetPkg[fieldName] || {}); - if (!pkgDeps) { + if (!pkgDeps && !required) { return; } @@ -231,10 +233,26 @@ class PackageJsonHandler { continue; } - await this.syncField(key, pkgDeps, targetDeps, fieldName, true, true); + await this.syncField( + key, + pkgDeps, + targetDeps, + fieldName, + true, + !required, + ); } } + private async syncReactDeps() { + const targetDeps = (this.targetPkg.dependencies = + this.targetPkg.dependencies || {}); + + // Remove these from from deps since they're now in peerDeps + await this.syncField('react', {}, targetDeps, 'dependencies'); + await this.syncField('react-dom', {}, targetDeps, 'dependencies'); + } + private async write() { await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)}\n`); } diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 3b512b22c3..d65e1e06d6 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -66,7 +66,7 @@ export const version = findVersion(); export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); export function createPackageVersionProvider(lockfile?: Lockfile) { - return (name: string, versionHint?: string) => { + return (name: string, versionHint?: string): string => { const packageVersion = packageVersions[name]; const targetVersion = versionHint || packageVersion; if (!targetVersion) { @@ -94,6 +94,6 @@ export function createPackageVersionProvider(lockfile?: Lockfile) { if (semver.parse(versionHint)?.prerelease.length) { return versionHint!; } - return `^${versionHint}`; + return versionHint?.match(/^[\d\.]+$/) ? `^${versionHint}` : versionHint!; }; } diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 624302da93..7461b0a57b 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -32,10 +32,11 @@ "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", "@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.57'}}", - "react": "{{versionQuery 'react' '16.13.1'}}", - "react-dom": "{{versionQuery 'react-dom' '16.13.1'}}", "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, + "peerDependencies": { + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0'}}" + }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 8c484fe2ac..d7e402270a 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/codemods +## 0.1.25 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/core-app-api@0.2.0 + +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/core-plugin-api@0.2.2 + - @backstage/core-app-api@0.1.24 + ## 0.1.23 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index e95367a5ac..32bf6be518 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.23", + "version": "0.1.25", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 00ace6e704..93fb5dd02c 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/config-loader +## 0.8.1 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- 4bea7b81d3: Uses key visibility as fallback in non-object arrays + ## 0.8.0 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index c275a1f045..78f8da3443 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.8.0", + "version": "0.8.1", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index 0395c7da6a..be07b915ed 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -20,6 +20,7 @@ import { filterByVisibility, filterErrorsByVisibility } from './filtering'; const data = { arr: ['f', 'b', 's'], + arrU: ['f', 'b', 't'], objArr: [ { f: 1, b: 2, s: 3 }, { f: 4, b: 5, s: 6 }, @@ -40,6 +41,8 @@ const data = { const visibility = new Map( Object.entries({ + '/arrU': 'frontend', + '/arrU/2': 'backend', '/arr/0': 'frontend', '/arr/1': 'backend', '/arr/2': 'secret', @@ -71,6 +74,9 @@ describe('filterByVisibility', () => { 'arr[0]', 'arr[1]', 'arr[2]', + 'arrU[0]', + 'arrU[1]', + 'arrU[2]', 'objArr[0].f', 'objArr[0].b', 'objArr[0].s', @@ -97,10 +103,12 @@ describe('filterByVisibility', () => { obj: { f: 'a' }, arrF: [], objF: {}, + arrU: ['f', 'b'], }, filteredKeys: [ 'arr[1]', 'arr[2]', + 'arrU[2]', 'objArr[0].b', 'objArr[0].s', 'objArr[1].b', @@ -120,6 +128,7 @@ describe('filterByVisibility', () => { { data: { arr: ['b'], + arrU: ['t'], objArr: [{ b: 2 }, { b: 5 }], obj: { b: {} }, arrF: [{ never: 'here' }], @@ -132,6 +141,8 @@ describe('filterByVisibility', () => { filteredKeys: [ 'arr[0]', 'arr[2]', + 'arrU[0]', + 'arrU[1]', 'objArr[0].f', 'objArr[0].s', 'objArr[1].f', @@ -154,6 +165,9 @@ describe('filterByVisibility', () => { filteredKeys: [ 'arr[0]', 'arr[1]', + 'arrU[0]', + 'arrU[1]', + 'arrU[2]', 'objArr[0].f', 'objArr[0].b', 'objArr[1].f', diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index b4226d7dad..b2f39d3cc4 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -61,11 +61,17 @@ export function filterByVisibility( const arr = new Array(); for (const [index, value] of jsonVal.entries()) { - const out = transform( - value, + let path = visibilityPath; + const hasVisibilityInIndex = visibilityByDataPath.get( `${visibilityPath}/${index}`, - `${filterPath}[${index}]`, ); + + if (hasVisibilityInIndex || typeof value === 'object') { + path = `${visibilityPath}/${index}`; + } + + const out = transform(value, path, `${filterPath}[${index}]`); + if (out !== undefined) { arr.push(out); } diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index ad71ad6fe2..ff15f45898 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/core-app-api +## 0.2.0 + +### Minor Changes + +- a036b65c2f: **BREAKING CHANGE** + + The app `SignInPage` component has been updated to switch out the `onResult` callback for a new `onSignInSuccess` callback. This is an immediate breaking change without any deprecation period, as it was deemed to be the way of making this change that had the lowest impact. + + The new `onSignInSuccess` callback directly accepts an implementation of an `IdentityApi`, rather than a `SignInResult`. The `SignInPage` from `@backstage/core-component` has been updated to fit this new API, and as long as you pass on `props` directly you should not see any breakage. + + However, if you implement your own custom `SignInPage`, then this will be a breaking change and you need to migrate over to using the new callback. While doing so you can take advantage of the `UserIdentity.fromLegacy` helper from `@backstage/core-components` to make the migration simpler by still using the `SignInResult` type. This helper is also deprecated though and is only provided for immediate migration. Long-term it will be necessary to build the `IdentityApi` using for example `UserIdentity.create` instead. + + The following is an example of how you can migrate existing usage immediately using `UserIdentity.fromLegacy`: + + ```ts + onResult(signInResult); + // becomes + onSignInSuccess(UserIdentity.fromLegacy(signInResult)); + ``` + + The following is an example of how implement the new `onSignInSuccess` callback of the `SignInPage` using `UserIdentity.create`: + + ```ts + const identityResponse = await authApi.getBackstageIdentity(); + // Profile is optional and will be removed, but allows the + // synchronous getProfile method of the IdentityApi to be used. + const profile = await authApi.getProfile(); + onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + authApi, + profile, + }), + ); + ``` + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/app-defaults@0.1.2 + - @backstage/version-bridge@0.1.1 + +## 0.1.24 + +### Patch Changes + +- 0e7f256034: Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page. +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.1.23 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index e69ee4f325..51fd6bd06f 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -39,6 +39,7 @@ import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; @@ -250,24 +251,13 @@ export class AppThemeSelector implements AppThemeApi { // @public export class AtlassianAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } // @public export class Auth0Auth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; } // @public @@ -303,13 +293,7 @@ export type BackstagePluginWithAnyOutput = Omit< // @public export class BitbucketAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } // @public @@ -402,13 +386,7 @@ export class GithubAuth implements OAuthApi, SessionApi { // @deprecated constructor(sessionManager: SessionManager); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): GithubAuth; + static create(options: OAuthApiCreateOptions): GithubAuth; // (undocumented) getAccessToken(scope?: string, options?: AuthRequestOptions): Promise; // (undocumented) @@ -441,25 +419,13 @@ export type GithubSession = { // @public export class GitlabAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; } // @public export class GoogleAuth { // (undocumented) - static create({ - discoveryApi, - oauthRequestApi, - environment, - provider, - defaultScopes, - }: OAuthApiCreateOptions): typeof googleAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } // @public @@ -477,13 +443,7 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { // @public export class MicrosoftAuth { // (undocumented) - static create({ - environment, - provider, - oauthRequestApi, - discoveryApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; } // @public @@ -507,14 +467,7 @@ export class OAuth2 scopeTransform: (scopes: string[]) => string[]; }); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - scopeTransform, - }: OAuth2CreateOptions): OAuth2; + static create(options: OAuth2CreateOptions): OAuth2; // (undocumented) getAccessToken( scope?: string | string[], @@ -570,24 +523,15 @@ export class OAuthRequestManager implements OAuthRequestApi { // @public export class OktaAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; } // @public export class OneLoginAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - }: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T; + static create( + options: OneLoginAuthCreateOptions, + ): typeof oneloginAuthApiRef.T; } // @public @@ -607,11 +551,7 @@ export class SamlAuth // @deprecated constructor(sessionManager: SessionManager); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - }: AuthApiCreateOptions): SamlAuth; + static create(options: AuthApiCreateOptions): SamlAuth; // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, @@ -635,10 +575,10 @@ export type SamlSession = { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo; diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 6777088b68..23079d7ba0 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.23", + "version": "0.2.0", "private": false, "publishConfig": { "access": "public", @@ -29,26 +29,28 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/app-defaults": "^0.1.1", - "@backstage/core-components": "^0.7.5", + "@backstage/app-defaults": "^0.1.2", + "@backstage/core-components": "^0.8.0", "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.0", + "@backstage/version-bridge": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@types/react": "*", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts new file mode 100644 index 0000000000..8a2f12594c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -0,0 +1,90 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +function mkError(thing: string) { + return new Error( + `Tried to access IdentityApi ${thing} before app was loaded`, + ); +} + +/** + * Implementation of the connection between the App-wide IdentityApi + * and sign-in page. + */ +export class AppIdentityProxy implements IdentityApi { + private target?: IdentityApi; + + // This is called by the app manager once the sign-in page provides us with an implementation + setTarget(identityApi: IdentityApi) { + this.target = identityApi; + } + + getUserId(): string { + if (!this.target) { + throw mkError('getUserId'); + } + return this.target.getUserId(); + } + + getProfile(): ProfileInfo { + if (!this.target) { + throw mkError('getProfile'); + } + return this.target.getProfile(); + } + + async getProfileInfo(): Promise { + if (!this.target) { + throw mkError('getProfileInfo'); + } + return this.target.getProfileInfo(); + } + + async getBackstageIdentity(): Promise { + if (!this.target) { + throw mkError('getBackstageIdentity'); + } + return this.target.getBackstageIdentity(); + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + if (!this.target) { + throw mkError('getCredentials'); + } + return this.target.getCredentials(); + } + + async getIdToken(): Promise { + if (!this.target) { + throw mkError('getIdToken'); + } + return this.target.getIdToken(); + } + + async signOut(): Promise { + if (!this.target) { + throw mkError('signOut'); + } + await this.target.signOut(); + location.reload(); + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts index caad40ddbf..07ebefca28 100644 --- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -30,12 +30,14 @@ const DEFAULT_PROVIDER = { * @public */ export default class AtlassianAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index 0a158ccb9e..d0a9dc5d99 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -30,13 +30,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class Auth0Auth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['openid', `email`, `profile`], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index e488580c4d..c97cccc6ed 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -45,13 +45,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class BitbucketAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['team'], - }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['team'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 3e9c899346..4da92efbdf 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -56,13 +56,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class GithubAuth implements OAuthApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read:user'], - }: OAuthApiCreateOptions) { + static create(options: OAuthApiCreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['read:user'], + } = options; + const connector = new DefaultAuthConnector({ discoveryApi, environment, diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index f46f2ef72e..00085de8ac 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -30,13 +30,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class GitlabAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read_user'], - }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['read_user'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts index a6f37b250f..8a528f4511 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -32,17 +32,19 @@ const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; * @public */ export default class GoogleAuth { - static create({ - discoveryApi, - oauthRequestApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - defaultScopes = [ - 'openid', - `${SCOPE_PREFIX}userinfo.email`, - `${SCOPE_PREFIX}userinfo.profile`, - ], - }: OAuthApiCreateOptions): typeof googleAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T { + const { + discoveryApi, + oauthRequestApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + defaultScopes = [ + 'openid', + `${SCOPE_PREFIX}userinfo.email`, + `${SCOPE_PREFIX}userinfo.profile`, + ], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 148be66873..be5776609d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -30,19 +30,21 @@ const DEFAULT_PROVIDER = { * @public */ export default class MicrosoftAuth { - static create({ - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - discoveryApi, - defaultScopes = [ - 'openid', - 'offline_access', - 'profile', - 'email', - 'User.Read', - ], - }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + const { + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + discoveryApi, + defaultScopes = [ + 'openid', + 'offline_access', + 'profile', + 'email', + 'User.Read', + ], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 403e8445d8..582526083b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -70,14 +70,16 @@ export default class OAuth2 BackstageIdentityApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = [], - scopeTransform = x => x, - }: OAuth2CreateOptions) { + static create(options: OAuth2CreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = [], + scopeTransform = x => x, + } = options; + const connector = new DefaultAuthConnector({ discoveryApi, environment, diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts index 465a124051..42c0ddd0ed 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -42,13 +42,15 @@ const OKTA_SCOPE_PREFIX: string = 'okta.'; * @public */ export default class OktaAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', 'email', 'profile', 'offline_access'], - }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof oktaAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['openid', 'email', 'profile', 'offline_access'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index 93b9f6634c..9493d0809e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -57,12 +57,16 @@ const SCOPE_PREFIX: string = 'onelogin.'; * @public */ export default class OneLoginAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - }: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T { + static create( + options: OneLoginAuthCreateOptions, + ): typeof oneloginAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index c1b70e963d..5988e81c48 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -52,11 +52,13 @@ const DEFAULT_PROVIDER = { export default class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - }: AuthApiCreateOptions) { + static create(options: AuthApiCreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + } = options; + const connector = new DirectAuthConnector({ discoveryApi, environment, diff --git a/packages/core-app-api/src/app/AppIdentity.ts b/packages/core-app-api/src/app/AppIdentity.ts deleted file mode 100644 index 64e698102f..0000000000 --- a/packages/core-app-api/src/app/AppIdentity.ts +++ /dev/null @@ -1,85 +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 { IdentityApi, ProfileInfo } from '@backstage/core-plugin-api'; -import { SignInResult } from './types'; - -/** - * Implementation of the connection between the App-wide IdentityApi - * and sign-in page. - */ -export class AppIdentity implements IdentityApi { - private hasIdentity = false; - private userId?: string; - private profile?: ProfileInfo; - private idTokenFunc?: () => Promise; - private signOutFunc?: () => Promise; - - getUserId(): string { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi userId before app was loaded', - ); - } - return this.userId!; - } - - getProfile(): ProfileInfo { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi profile before app was loaded', - ); - } - return this.profile!; - } - - async getIdToken(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi idToken before app was loaded', - ); - } - return this.idTokenFunc?.(); - } - - async signOut(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi signOutFunc before app was loaded', - ); - } - await this.signOutFunc?.(); - location.reload(); - } - - // This is indirectly called by the sign-in page to continue into the app. - setSignInResult(result: SignInResult) { - if (this.hasIdentity) { - return; - } - if (!result.userId) { - throw new Error('Invalid sign-in result, userId not set'); - } - if (!result.profile) { - throw new Error('Invalid sign-in result, profile not set'); - } - this.hasIdentity = true; - this.userId = result.userId; - this.profile = result.profile; - this.idTokenFunc = result.getIdToken; - this.signOutFunc = result.signOut; - } -} diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 5903a8c874..21940c08d5 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -43,12 +43,14 @@ import { AppThemeApi, ConfigApi, featureFlagsApiRef, + IdentityApi, identityApiRef, BackstagePlugin, RouteRef, SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; +import { UserIdentity } from '@backstage/core-components'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -66,7 +68,7 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { RouteTracker } from '../routing/RouteTracker'; import { validateRoutes } from '../routing/validation'; import { AppContextProvider } from './AppContext'; -import { AppIdentity } from './AppIdentity'; +import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy'; import { AppComponents, AppConfigLoader, @@ -75,7 +77,6 @@ import { AppRouteBinder, BackstageApp, SignInPageProps, - SignInResult, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultConfigLoader } from './defaultConfigLoader'; @@ -189,7 +190,7 @@ export class AppManager implements BackstageApp { private readonly defaultApis: Iterable; private readonly bindRoutes: AppOptions['bindRoutes']; - private readonly identityApi = new AppIdentity(); + private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; constructor(options: AppOptions) { @@ -344,14 +345,14 @@ export class AppManager implements BackstageApp { component: ComponentType; children: ReactElement; }) => { - const [result, setResult] = useState(); + const [identityApi, setIdentityApi] = useState(); - if (result) { - this.identityApi.setSignInResult(result); - return children; + if (!identityApi) { + return ; } - return ; + this.appIdentityProxy.setTarget(identityApi); + return children; }; const AppRouter = ({ children }: PropsWithChildren<{}>) => { @@ -360,13 +361,7 @@ export class AppManager implements BackstageApp { // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { - this.identityApi.setSignInResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, - }); + this.appIdentityProxy.setTarget(UserIdentity.createGuest()); return ( @@ -430,7 +425,7 @@ export class AppManager implements BackstageApp { this.apiFactoryRegistry.register('static', { api: identityApiRef, deps: {}, - factory: () => this.identityApi, + factory: () => this.appIdentityProxy, }); // It's possible to replace the feature flag API, but since we must have at least diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index f16c8ac656..a538ccd366 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -25,6 +25,7 @@ import { SubRouteRef, ExternalRouteRef, PluginOutput, + IdentityApi, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; @@ -42,6 +43,7 @@ export type BootErrorPageProps = { * The outcome of signing in on the sign-in page. * * @public + * @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. */ export type SignInResult = { /** @@ -69,9 +71,9 @@ export type SignInResult = { */ export type SignInPageProps = { /** - * Set the sign-in result for the app. This should only be called once. + * Set the IdentityApi on successful sign in. This should only be called once. */ - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi): void; }; /** diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index b4898fa82b..4eab9cea54 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/core-components +## 0.8.0 + +### Minor Changes + +- a036b65c2f: The `SignInPage` has been updated to use the new `onSignInSuccess` callback that was introduced in the same release. While existing code will usually continue to work, it is technically a breaking change because of the dependency on `SignInProps` from the `@backstage/core-plugin-api`. For more information on this change and instructions on how to migrate existing code, see the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). + + Added a new `UserIdentity` class which helps create implementations of the `IdentityApi`. It provides a couple of static factory methods such as the most relevant `create`, and `createGuest` to create an `IdentityApi` for a guest user. + + Also provides a deprecated `fromLegacy` method to create an `IdentityApi` from the now deprecated `SignInResult`. This method will be removed in the future when `SignInResult` is also removed. + +### Patch Changes + +- 9603827bb5: Addressed some peer dependency warnings +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- e839500286: Introduce new `LogViewer` component that can be used to display logs. It supports copying, searching, filtering, and displaying text with ANSI color escape codes. +- 1357ac30f1: Standardize on `classnames` instead of both that and `clsx`. +- e5976071ea: Use ellipsis style for overflowed text in sidebar menu +- Updated dependencies + - @backstage/core-plugin-api@0.3.0 + +## 0.7.6 + +### Patch Changes + +- e34f174fc5: Added `` and `` to enable building better sidebars. You can check out the storybook for more inspiration and how to get started. + + Added two new theme props for styling the sidebar too, `navItem.hoverBackground` and `submenu.background`. + +- Updated dependencies + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.7.5 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 0f1e3510ae..d098403113 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; @@ -20,6 +21,7 @@ import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; @@ -27,6 +29,7 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; +import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -35,6 +38,7 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; +import { SignInResult } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; @@ -605,6 +609,50 @@ export type LinkProps = LinkProps_2 & // @public (undocumented) export type LoginRequestListItemClassKey = 'root'; +// @public +export function LogViewer(props: LogViewerProps): JSX.Element; + +// @public +export type LogViewerClassKey = + | 'root' + | 'header' + | 'log' + | 'line' + | 'lineSelected' + | 'lineCopyButton' + | 'lineNumber' + | 'textHighlight' + | 'textSelectedHighlight' + | 'modifierBold' + | 'modifierItalic' + | 'modifierUnderline' + | 'modifierForegroundBlack' + | 'modifierForegroundRed' + | 'modifierForegroundGreen' + | 'modifierForegroundYellow' + | 'modifierForegroundBlue' + | 'modifierForegroundMagenta' + | 'modifierForegroundCyan' + | 'modifierForegroundWhite' + | 'modifierForegroundGrey' + | 'modifierBackgroundBlack' + | 'modifierBackgroundRed' + | 'modifierBackgroundGreen' + | 'modifierBackgroundYellow' + | 'modifierBackgroundBlue' + | 'modifierBackgroundMagenta' + | 'modifierBackgroundCyan' + | 'modifierBackgroundWhite' + | 'modifierBackgroundGrey'; + +// @public +export interface LogViewerProps { + classes?: { + root?: string; + }; + text: string; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "MarkdownContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -832,6 +880,7 @@ export const SidebarContext: Context; // @public (undocumented) export type SidebarContextType = { isOpen: boolean; + setOpen: (open: boolean) => void; }; // Warning: (ae-missing-release-tag) "SidebarDivider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1103,6 +1152,9 @@ export const SidebarDivider: React_2.ComponentType< } >; +// @public +export const SidebarExpandButton: () => JSX.Element | null; + // Warning: (ae-missing-release-tag) "SidebarIntro" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1977,6 +2029,34 @@ export const SidebarSpacer: React_2.ComponentType< } >; +// @public +export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; + +// @public +export const SidebarSubmenuItem: ( + props: SidebarSubmenuItemProps, +) => JSX.Element; + +// @public +export type SidebarSubmenuItemDropdownItem = { + title: string; + to: string; +}; + +// @public +export type SidebarSubmenuItemProps = { + title: string; + to: string; + icon: IconComponent; + dropdownItems?: SidebarSubmenuItemDropdownItem[]; +}; + +// @public +export type SidebarSubmenuProps = { + title?: string; + children: ReactNode; +}; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2282,6 +2362,33 @@ export function useQueryParamState( // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; +// @public +export class UserIdentity implements IdentityApi { + static create(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + profile?: ProfileInfo; + }): IdentityApi; + static createGuest(): IdentityApi; + static fromLegacy(result: SignInResult): IdentityApi; + // (undocumented) + getBackstageIdentity(): Promise; + // (undocumented) + getCredentials(): Promise<{ + token?: string | undefined; + }>; + // (undocumented) + getIdToken(): Promise; + // (undocumented) + getProfile(): ProfileInfo; + // (undocumented) + getProfileInfo(): Promise; + // (undocumented) + getUserId(): string; + // (undocumented) + signOut(): Promise; +} + // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 896295038c..f47335fce8 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.7.5", + "version": "0.8.0", "private": false, "publishConfig": { "access": "public", @@ -30,30 +30,28 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.5", - "@backstage/theme": "^0.2.13", + "@backstage/theme": "^0.2.14", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", + "ansi-regex": "^5.0.1", "classnames": "^2.2.6", - "clsx": "^1.1.0", "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", "dagre": "^0.8.5", + "history": "^5.0.0", "immer": "^9.0.1", "lodash": "^4.17.21", "pluralize": "^8.0.0", "prop-types": "^15.7.2", "qs": "^6.9.4", "rc-progress": "^3.0.0", - "react": "^16.12.0", - "react-dom": "^16.12.0", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-markdown": "^7.0.1", @@ -63,13 +61,20 @@ "react-syntax-highlighter": "^15.4.3", "react-text-truncate": "^0.16.0", "react-use": "^17.2.4", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", "remark-gfm": "^2.0.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/core-app-api": "^0.1.23", - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -78,12 +83,15 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", + "@types/ansi-regex": "^5.0.0", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react-helmet": "^6.1.0", "@types/react-syntax-highlighter": "^13.5.2", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", "@types/zen-observable": "^0.8.0" }, "files": [ diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx index a9532dd79b..ecb784f951 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -15,8 +15,7 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act, fireEvent } from '@testing-library/react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { CopyTextButton } from './CopyTextButton'; import { errorApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts new file mode 100644 index 0000000000..e957b28b7c --- /dev/null +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts @@ -0,0 +1,233 @@ +/* + * 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 { AnsiProcessor } from './AnsiProcessor'; + +describe('AnsiProcessor', () => { + it('should process a single line', () => { + const processor = new AnsiProcessor(); + expect(processor.process('foo\x1b[31mbar\x1b[39mbaz')).toEqual([ + { + chunks: [ + { + text: 'foo', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'red' }, + }, + { + text: 'baz', + modifiers: {}, + }, + ], + text: 'foobarbaz', + lineNumber: 1, + }, + ]); + + expect(processor.process(`foo bar: baz`)).toEqual([ + { + chunks: [ + { + text: 'foo ', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'green' }, + }, + { + text: ': baz', + modifiers: {}, + }, + ], + text: 'foo bar: baz', + lineNumber: 1, + }, + ]); + }); + + it('should process multiple lines', () => { + const processor = new AnsiProcessor(); + expect( + processor.process(` +a\x1b[34mb\x1b[39mc +x\x1b[44my\x1b[49mz +`), + ).toEqual([ + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'blue' }, + }, + { + text: 'c', + modifiers: {}, + }, + ], + text: 'abc', + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: {}, + }, + { + text: 'y', + modifiers: { background: 'blue' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + text: 'xyz', + lineNumber: 3, + }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 4 }, + ]); + }); + + it('should carry state across lines', () => { + const processor = new AnsiProcessor(); + expect( + processor.process(` +a\x1b[45mb\x1b[35mc +x\x1b[39my\x1b[49mz`), + ).toEqual([ + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { background: 'magenta' }, + }, + { + text: 'c', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + ], + text: 'abc', + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + { + text: 'y', + modifiers: { background: 'magenta' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + text: 'xyz', + lineNumber: 3, + }, + ]); + }); + + it('should carry forward state when appending lines', () => { + const processor = new AnsiProcessor(); + const out1 = processor.process(` +a\x1b[36mb\x1b[3mc`); + expect(out1).toEqual([ + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + text: 'abc', + lineNumber: 2, + }, + ]); + + const out2 = processor.process(` +a\x1b[36mb\x1b[3mc +x\x1b[39my\x1b[23mz`); + expect(out2).toEqual([ + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + text: 'abc', + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'cyan', italic: true }, + }, + { + text: 'y', + modifiers: { italic: true }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + text: 'xyz', + lineNumber: 3, + }, + ]); + + // Verifies that we appended rather than reprocessed + expect(out1[0]).toBe(out2[0]); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts new file mode 100644 index 0000000000..d0f835a70e --- /dev/null +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -0,0 +1,216 @@ +/* + * 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 ansiRegexMaker from 'ansi-regex'; + +const ansiRegex = ansiRegexMaker(); +const newlineRegex = /\n\r?/g; + +// A mapping of how each escape code changes the modifiers +const codeModifiers = Object.fromEntries( + Object.entries({ + 1: m => ({ ...m, bold: true }), + 3: m => ({ ...m, italic: true }), + 4: m => ({ ...m, underline: true }), + 22: ({ bold: _, ...m }) => m, + 23: ({ italic: _, ...m }) => m, + 24: ({ underline: _, ...m }) => m, + 30: m => ({ ...m, foreground: 'black' }), + 31: m => ({ ...m, foreground: 'red' }), + 32: m => ({ ...m, foreground: 'green' }), + 33: m => ({ ...m, foreground: 'yellow' }), + 34: m => ({ ...m, foreground: 'blue' }), + 35: m => ({ ...m, foreground: 'magenta' }), + 36: m => ({ ...m, foreground: 'cyan' }), + 37: m => ({ ...m, foreground: 'white' }), + 39: ({ foreground: _, ...m }) => m, + 90: m => ({ ...m, foreground: 'grey' }), + 40: m => ({ ...m, background: 'black' }), + 41: m => ({ ...m, background: 'red' }), + 42: m => ({ ...m, background: 'green' }), + 43: m => ({ ...m, background: 'yellow' }), + 44: m => ({ ...m, background: 'blue' }), + 45: m => ({ ...m, background: 'magenta' }), + 46: m => ({ ...m, background: 'cyan' }), + 47: m => ({ ...m, background: 'white' }), + 49: ({ background: _, ...m }) => m, + } as Record ChunkModifiers>).map( + ([code, modifier]) => [`\x1b[${code}m`, modifier], + ), +); + +export type AnsiColor = + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'grey'; + +export interface ChunkModifiers { + foreground?: AnsiColor; + background?: AnsiColor; + bold?: boolean; + italic?: boolean; + underline?: boolean; +} + +export interface AnsiChunk { + text: string; + modifiers: ChunkModifiers; +} + +export class AnsiLine { + text: string; + + constructor( + readonly lineNumber: number = 1, + readonly chunks: AnsiChunk[] = [], + ) { + this.text = chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); + } + + lastChunk(): AnsiChunk | undefined { + return this.chunks[this.chunks.length - 1]; + } + + replaceLastChunk(newChunks?: AnsiChunk[]) { + if (newChunks) { + this.chunks.splice(this.chunks.length - 1, 1, ...newChunks); + this.text = this.chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); + } + } +} + +export class AnsiProcessor { + private text: string = ''; + private lines: AnsiLine[] = []; + + /** + * Processes a chunk of text while keeping internal state that optimizes + * subsequent processing that appends to the text. + */ + process(text: string): AnsiLine[] { + if (this.text === text) { + return this.lines; + } + + if (text.startsWith(this.text)) { + const lastLineIndex = this.lines.length > 0 ? this.lines.length - 1 : 0; + const lastLine = this.lines[lastLineIndex] ?? new AnsiLine(); + const lastChunk = lastLine.lastChunk(); + + const newLines = this.processLines( + (lastChunk?.text ?? '') + text.slice(this.text.length), + lastChunk?.modifiers, + lastLine?.lineNumber, + ); + lastLine.replaceLastChunk(newLines[0]?.chunks); + + this.lines[lastLineIndex] = lastLine; + this.lines.push(...newLines.slice(1)); + } else { + this.lines = this.processLines(text); + } + this.text = text; + + return this.lines; + } + + // Split a chunk of text up into lines and process each line individually + private processLines = ( + text: string, + modifiers: ChunkModifiers = {}, + startingLineNumber: number = 1, + ): AnsiLine[] => { + const lines: AnsiLine[] = []; + + let currentModifiers = modifiers; + let currentLineNumber = startingLineNumber; + + let prevIndex = 0; + newlineRegex.lastIndex = 0; + for (;;) { + const match = newlineRegex.exec(text); + if (!match) { + const chunks = this.processText( + text.slice(prevIndex), + currentModifiers, + ); + lines.push(new AnsiLine(currentLineNumber, chunks)); + return lines; + } + + const line = text.slice(prevIndex, match.index); + prevIndex = match.index + match[0].length; + + const chunks = this.processText(line, currentModifiers); + lines.push(new AnsiLine(currentLineNumber, chunks)); + + // Modifiers that are active in the last chunk are carried over to the next line + currentModifiers = + chunks[chunks.length - 1].modifiers ?? currentModifiers; + currentLineNumber += 1; + } + }; + + // Processing of a one individual text chunk + private processText = ( + fullText: string, + modifiers: ChunkModifiers, + ): AnsiChunk[] => { + const chunks: AnsiChunk[] = []; + + let currentModifiers = modifiers; + + let prevIndex = 0; + ansiRegex.lastIndex = 0; + for (;;) { + const match = ansiRegex.exec(fullText); + if (!match) { + chunks.push({ + text: fullText.slice(prevIndex), + modifiers: currentModifiers, + }); + return chunks; + } + + const text = fullText.slice(prevIndex, match.index); + chunks.push({ text, modifiers: currentModifiers }); + + // For every escape code that we encounter we keep track of where the + // next chunk of text starts, and what modifiers it has + prevIndex = match.index + match[0].length; + currentModifiers = this.processCode(match[0], currentModifiers); + } + }; + + private processCode = ( + code: string, + modifiers: ChunkModifiers, + ): ChunkModifiers => { + return codeModifiers[code]?.(modifiers) ?? modifiers; + }; +} diff --git a/packages/core-components/src/components/LogViewer/LogLine.test.tsx b/packages/core-components/src/components/LogViewer/LogLine.test.tsx new file mode 100644 index 0000000000..c5fd6e39ca --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogLine.test.tsx @@ -0,0 +1,356 @@ +/* + * 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 { AnsiLine, ChunkModifiers } from './AnsiProcessor'; +import { + calculateHighlightedChunks, + findSearchResults, + getModifierClasses, +} from './LogLine'; + +describe('getModifierClasses', () => { + const classes = { + modifierBold: 'bold', + modifierItalic: 'italic', + modifierUnderline: 'underline', + modifierForegroundBlack: 'black', + modifierForegroundRed: 'red', + modifierForegroundGreen: 'green', + modifierForegroundYellow: 'yellow', + modifierForegroundBlue: 'blue', + modifierForegroundMagenta: 'magenta', + modifierForegroundCyan: 'cyan', + modifierForegroundWhite: 'white', + modifierForegroundGrey: 'grey', + modifierBackgroundBlack: 'bg-black', + modifierBackgroundRed: 'bg-red', + modifierBackgroundGreen: 'bg-green', + modifierBackgroundYellow: 'bg-yellow', + modifierBackgroundBlue: 'bg-blue', + modifierBackgroundMagenta: 'bg-magenta', + modifierBackgroundCyan: 'bg-cyan', + modifierBackgroundWhite: 'bg-white', + modifierBackgroundGrey: 'bg-grey', + }; + const curried = (modifiers: ChunkModifiers) => + getModifierClasses( + classes as Parameters[0], + modifiers, + ); + + it('should transform modifiers to classes', () => { + expect(curried({})).toEqual(undefined); + expect(curried({ bold: true })).toEqual('bold'); + expect(curried({ italic: true })).toEqual('italic'); + expect(curried({ underline: true })).toEqual('underline'); + expect(curried({ foreground: 'black' })).toEqual('black'); + expect(curried({ background: 'black' })).toEqual('bg-black'); + expect( + curried({ + bold: true, + italic: true, + underline: true, + foreground: 'red', + background: 'red', + }), + ).toEqual('bold italic underline red bg-red'); + }); +}); + +describe('findSearchResults', () => { + it('should not return results if there is no match', () => { + expect(findSearchResults('Foo', 'Bar')).toEqual(undefined); + expect(findSearchResults('Foo Bar', 'oof')).toEqual(undefined); + expect(findSearchResults('Foo Bar', '')).toEqual(undefined); + expect(findSearchResults('', '')).toEqual(undefined); + expect(findSearchResults('', 'Foo')).toEqual(undefined); + }); + + it('should find result indices', () => { + expect(findSearchResults('Foo', 'Foo')).toEqual([{ start: 0, end: 3 }]); + expect(findSearchResults('Foo', 'o')).toEqual([ + { start: 1, end: 2 }, + { start: 2, end: 3 }, + ]); + expect(findSearchResults('FooBarBaz', 'Bar')).toEqual([ + { start: 3, end: 6 }, + ]); + expect(findSearchResults('Foo Bar Baz', ' ')).toEqual([ + { start: 3, end: 4 }, + { start: 7, end: 8 }, + ]); + expect(findSearchResults('FooBarBazBarFoo', 'Bar')).toEqual([ + { start: 3, end: 6 }, + { start: 9, end: 12 }, + ]); + expect(findSearchResults('FooBarBazBarFoo', 'Foo')).toEqual([ + { start: 0, end: 3 }, + { start: 12, end: 15 }, + ]); + }); + + it('should not overlap search results', () => { + expect(findSearchResults('aaa', 'aa')).toEqual([{ start: 0, end: 2 }]); + expect(findSearchResults('aaaa', 'aa')).toEqual([ + { start: 0, end: 2 }, + { start: 2, end: 4 }, + ]); + expect(findSearchResults('aaaaa', 'aa')).toEqual([ + { start: 0, end: 2 }, + { start: 2, end: 4 }, + ]); + }); +}); + +describe('calculateHighlightedChunks', () => { + it('should pass through chunks if there are no results', () => { + const chunks = [{ text: 'Foo', modifiers: {} }]; + const line = new AnsiLine(0, chunks); + expect(calculateHighlightedChunks(line, 'bar')).toBe(chunks); + }); + + it('should highlight one result from plain text', () => { + const line = new AnsiLine(0, [{ text: 'FooBarBaz', modifiers: {} }]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: {}, + highlight: 0, + }, + { + text: 'BarBaz', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'bar')).toEqual([ + { + text: 'Foo', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: 0, + }, + { + text: 'Baz', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'baz')).toEqual([ + { + text: 'FooBar', + modifiers: {}, + }, + { + text: 'Baz', + modifiers: {}, + highlight: 0, + }, + ]); + }); + + it('should highlight multiple results from plain text', () => { + const line = new AnsiLine(0, [ + { text: 'FooBarBazBazBarFoo', modifiers: {} }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: {}, + highlight: 0, + }, + { + text: 'BarBazBazBar', + modifiers: {}, + }, + { + text: 'Foo', + modifiers: {}, + highlight: 1, + }, + ]); + expect(calculateHighlightedChunks(line, 'bar')).toEqual([ + { + text: 'Foo', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: 0, + }, + { + text: 'BazBaz', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: 1, + }, + { + text: 'Foo', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'baz')).toEqual([ + { + text: 'FooBar', + modifiers: {}, + }, + { + text: 'Baz', + modifiers: {}, + highlight: 0, + }, + { + text: 'Baz', + modifiers: {}, + highlight: 1, + }, + { + text: 'BarFoo', + modifiers: {}, + }, + ]); + }); + + it('should forward modifiers to result', () => { + const line = new AnsiLine(0, [ + { text: 'FooBarBazBazBarFoo', modifiers: { bold: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: { bold: true }, + highlight: 0, + }, + { + text: 'BarBazBazBar', + modifiers: { bold: true }, + }, + { + text: 'Foo', + modifiers: { bold: true }, + highlight: 1, + }, + ]); + }); + + it('should highlight full chunks', () => { + const line = new AnsiLine(0, [ + { text: 'Foo', modifiers: { bold: true } }, + { text: 'BarBaz', modifiers: { bold: true } }, + { text: 'BazBar', modifiers: { italic: true } }, + { text: 'Foo', modifiers: { italic: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: { bold: true }, + highlight: 0, + }, + { + text: 'BarBaz', + modifiers: { bold: true }, + }, + { + text: 'BazBar', + modifiers: { italic: true }, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: 1, + }, + ]); + }); + + it('should highlight partial chunks', () => { + const line = new AnsiLine(0, [ + { text: 'Fo', modifiers: { bold: true } }, + { text: 'oFooFo', modifiers: {} }, + { text: 'oBarBaz', modifiers: { italic: true } }, + { text: 'Foo', modifiers: { foreground: 'blue' } }, + { text: 'FooFoo', modifiers: { italic: true } }, + { text: 'F', modifiers: { bold: true } }, + { text: 'o', modifiers: {} }, + { text: 'o', modifiers: { bold: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Fo', + modifiers: { bold: true }, + highlight: 0, + }, + { + text: 'o', + modifiers: {}, + highlight: 0, + }, + { + text: 'Foo', + modifiers: {}, + highlight: 1, + }, + { + text: 'Fo', + modifiers: {}, + highlight: 2, + }, + { + text: 'o', + modifiers: { italic: true }, + highlight: 2, + }, + { + text: 'BarBaz', + modifiers: { italic: true }, + }, + { + text: 'Foo', + modifiers: { foreground: 'blue' }, + highlight: 3, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: 4, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: 5, + }, + { + text: 'F', + modifiers: { bold: true }, + highlight: 6, + }, + { + text: 'o', + modifiers: {}, + highlight: 6, + }, + { + text: 'o', + modifiers: { bold: true }, + highlight: 6, + }, + ]); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx new file mode 100644 index 0000000000..ed3396360e --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -0,0 +1,178 @@ +/* + * 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, { useMemo } from 'react'; +import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; +import startCase from 'lodash/startCase'; +import classnames from 'classnames'; +import { useStyles } from './styles'; + +export function getModifierClasses( + classes: ReturnType, + modifiers: ChunkModifiers, +) { + const classNames = new Array(); + if (modifiers.bold) { + classNames.push(classes.modifierBold); + } + if (modifiers.italic) { + classNames.push(classes.modifierItalic); + } + if (modifiers.underline) { + classNames.push(classes.modifierUnderline); + } + if (modifiers.foreground) { + const key = `modifierForeground${startCase( + modifiers.foreground, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + if (modifiers.background) { + const key = `modifierBackground${startCase( + modifiers.background, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + return classNames.length > 0 ? classNames.join(' ') : undefined; +} + +export function findSearchResults(text: string, searchText: string) { + if (!searchText || !text.includes(searchText)) { + return undefined; + } + const searchResults = new Array<{ start: number; end: number }>(); + let offset = 0; + for (;;) { + const start = text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + searchResults.push({ start, end }); + offset = end; + } + return searchResults; +} + +export interface HighlightAnsiChunk extends AnsiChunk { + highlight?: number; +} + +export function calculateHighlightedChunks( + line: AnsiLine, + searchText: string, +): HighlightAnsiChunk[] { + const results = findSearchResults(line.text, searchText); + if (!results) { + return line.chunks; + } + + const chunks = new Array(); + + let lineOffset = 0; + let resultIndex = 0; + let result = results[resultIndex]; + for (const chunk of line.chunks) { + const { text, modifiers } = chunk; + if (!result || lineOffset + text.length < result.start) { + chunks.push(chunk); + lineOffset += text.length; + continue; + } + + let localOffset = 0; + while (result) { + const localStart = Math.max(result.start - lineOffset, 0); + if (localStart > text.length) { + break; // The next result is not in this chunk + } + + const localEnd = Math.min(result.end - lineOffset, text.length); + + const hasTextBeforeResult = localStart > localOffset; + if (hasTextBeforeResult) { + chunks.push({ text: text.slice(localOffset, localStart), modifiers }); + } + const hasResultText = localEnd > localStart; + if (hasResultText) { + chunks.push({ + modifiers, + highlight: resultIndex, + text: text.slice(localStart, localEnd), + }); + } + + localOffset = localEnd; + + const foundCompleteResult = result.end - lineOffset === localEnd; + if (foundCompleteResult) { + resultIndex += 1; + result = results[resultIndex]; + } else { + break; // The rest of the result is in the following chunks + } + } + + const hasTextAfterResult = localOffset < text.length; + if (hasTextAfterResult) { + chunks.push({ text: text.slice(localOffset), modifiers }); + } + + lineOffset += text.length; + } + + return chunks; +} + +export interface LogLineProps { + line: AnsiLine; + classes: ReturnType; + searchText: string; + highlightResultIndex?: number; +} + +export function LogLine({ + line, + classes, + searchText, + highlightResultIndex, +}: LogLineProps) { + const chunks = useMemo( + () => calculateHighlightedChunks(line, searchText), + [line, searchText], + ); + + const elements = useMemo( + () => + chunks.map(({ text, modifiers, highlight }, index) => ( + + {text} + + )), + [chunks, highlightResultIndex, classes], + ); + + return <>{elements}; +} diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx new file mode 100644 index 0000000000..255e1d03b8 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -0,0 +1,83 @@ +/* + * 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 React, { ComponentType } from 'react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { LogViewer } from './LogViewer'; + +export default { + title: 'Data Display/LogViewer', + component: LogViewer, + decorators: [(Story: ComponentType<{}>) => wrapInTestApp()], +}; + +const exampleLog = `Starting up task with 3 steps +Beginning step Fetch Skeleton + Template +info: Fetching template content from remote URL {"timestamp":"2021-12-03T15:47:11.625Z"} +info: Listing files and directories in template {"timestamp":"2021-12-03T15:47:12.797Z"} +info: Processing 33 template files/directories with input values {"component_id":"srnthsrthntrhsn","description":"rnthsrtnhssrthnrsthn","destination":{"host":"github.com","owner":"rtshnsrtmhrstmh","repo":"srtmhsrtmhrsthms"},"owner":"rstnhrstnhsrthn","timestamp":"2021-12-03T15:47:12.801Z"} +info: Writing file .editorconfig to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.816Z"} +info: Writing file .eslintignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.818Z"} +info: Writing file .eslintrc.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.820Z"} +info: Writing directory .github/ to template output path. {"timestamp":"2021-12-03T15:47:12.823Z"} +info: Writing file .gitignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.824Z"} +info: Writing file README.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.827Z"} +info: Writing file babel.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.829Z"} +info: Writing file catalog-info.yaml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.831Z"} +info: Writing directory docs/ to template output path. {"timestamp":"2021-12-03T15:47:12.834Z"} +info: Writing file jest.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.836Z"} +info: Writing file mkdocs.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.838Z"} +info: Writing file next-env.d.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.841Z"} +info: Writing file next.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.844Z"} +info: Writing file package.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.845Z"} +info: Writing file prettier.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.848Z"} +info: Writing directory public/ to template output path. {"timestamp":"2021-12-03T15:47:12.849Z"} +info: Writing directory src/ to template output path. {"timestamp":"2021-12-03T15:47:12.850Z"} +info: Writing file tsconfig.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.851Z"} +info: Writing directory .github/workflows/ to template output path. {"timestamp":"2021-12-03T15:47:12.853Z"} +info: Writing file docs/index.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.854Z"} +info: Writing directory public/static/ to template output path. {"timestamp":"2021-12-03T15:47:12.857Z"} +info: Writing directory src/__tests__/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/components/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/pages/ to template output path. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Copying file/directory .github/workflows/build.yml without processing. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Writing file .github/workflows/build.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.860Z"} +info: Writing file public/static/fonts.css to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.861Z"} +info: Writing file src/components/Header.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.863Z"} +info: Writing file src/__tests__/index.test.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.865Z"} +info: Writing file src/pages/_app.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.868Z"} +info: Writing file src/pages/_document.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.871Z"} +info: Writing directory src/pages/api/ to template output path. {"timestamp":"2021-12-03T15:47:12.873Z"} +info: Writing file src/pages/index.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.874Z"} +info: Writing file src/pages/api/ping.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.877Z"} +info: Template result written to /var/folders/k6/9s7hd6w17115xlgwnsp0wsbr0000gn/T/5c9f8584-fded-4741-b6ef-46d94ff2cbdb {"timestamp":"2021-12-03T15:47:12.878Z"} +Finished step Fetch Skeleton + Template +Beginning step Publish +HttpError: Not Found + at /Users/patriko/dev/backstage/node_modules/@octokit/request/dist-node/index.js:86:21 + at runMicrotasks () + at processTicksAndRejections (internal/process/task_queues.js:95:5) + at async Object.handler (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts:156:20) + at async HandlebarsWorkflowRunner.execute (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts:254:11) + at async TaskWorker.runOneTask (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:110:13) + at async eval (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:100:9) +Run completed with status: failed`; + +export const ExampleLogViewer = () => ( +
+ +
+); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx new file mode 100644 index 0000000000..bdb3973015 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -0,0 +1,64 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import { useApp } from '@backstage/core-plugin-api'; + +const RealLogViewer = lazy(() => + import('./RealLogViewer').then(m => ({ default: m.RealLogViewer })), +); + +/** + * The properties for the LogViewer component. + * + * @public + */ +export interface LogViewerProps { + /** + * The text of the logs to display. + * + * The LogViewer component is optimized for appending content at the end of the text. + */ + text: string; + /** + * Styling overrides for classes within the LogViewer component. + */ + classes?: { + root?: string; + }; +} + +/** + * A component that displays logs in a scrollable text area. + * + * The LogViewer has support for search and filtering, as well as displaying + * text content with ANSI color escape codes. + * + * Since the LogViewer uses windowing to avoid rendering all contents at once, the + * log is sized automatically to fill the available vertical space. This means + * it may often be needed to wrap the LogViewer in a container that provides it + * with a fixed amount of space. + * + * @public + */ +export function LogViewer(props: LogViewerProps) { + const { Progress } = useApp().getComponents(); + return ( + }> + + + ); +} diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx new file mode 100644 index 0000000000..4d3baae68d --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -0,0 +1,74 @@ +/* + * 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 IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import { LogViewerSearch } from './useLogViewerSearch'; + +export interface LogViewerControlsProps extends LogViewerSearch {} + +export function LogViewerControls(props: LogViewerControlsProps) { + const { resultCount, resultIndexStep, toggleShouldFilter } = props; + const resultIndex = props.resultIndex ?? 0; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (event.metaKey || event.ctrlKey || event.altKey) { + toggleShouldFilter(); + } else { + resultIndexStep(event.shiftKey); + } + } + }; + + return ( + <> + {resultCount !== undefined && ( + <> + resultIndexStep(true)}> + + + + {Math.min(resultIndex + 1, resultCount)}/{resultCount} + + resultIndexStep()}> + + + + )} + props.setSearchInput(e.target.value)} + /> + + {props.shouldFilter ? ( + + ) : ( + + )} + + + ); +} diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx new file mode 100644 index 0000000000..8d5efb9724 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import UserEvent from '@testing-library/user-event'; +import { renderInTestApp } from '@backstage/test-utils'; +import { RealLogViewer } from './RealLogViewer'; +// eslint-disable-next-line import/no-extraneous-dependencies +import copyToClipboard from 'copy-to-clipboard'; + +// Used by useCopyToClipboard +jest.mock('copy-to-clipboard', () => ({ + __esModule: true, + default: jest.fn(), +})); + +// The inside needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}, +})); + +const testText = `Some Log Line +Derp +Foo + +Foo Foo +Wat`; + +describe('RealLogViewer', () => { + it('should render text with search and filtering and copying', async () => { + const rendered = await renderInTestApp(); + expect(rendered.getByText('Derp')).toBeInTheDocument(); + expect(rendered.getByText('Foo Foo')).toBeInTheDocument(); + + UserEvent.tab(); + UserEvent.keyboard('Foo'); + + expect(rendered.getByText('1/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('2/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('3/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('1/3')).toBeInTheDocument(); + UserEvent.keyboard('{shift}{enter}{/shift}'); + expect(rendered.getByText('3/3')).toBeInTheDocument(); + + expect(rendered.queryByText('Some Log Line')).toBeInTheDocument(); + UserEvent.keyboard('{meta}{enter}{/meta}'); + expect(rendered.queryByText('Some Log Line')).not.toBeInTheDocument(); + UserEvent.keyboard('{meta}{enter}{/meta}'); + expect(rendered.queryByText('Some Log Line')).toBeInTheDocument(); + + // Tab down to line #2 and click + UserEvent.tab(); + UserEvent.tab(); + UserEvent.tab(); + UserEvent.click(document.activeElement!); + UserEvent.click(rendered.getByTestId('copy-button')); + + expect(copyToClipboard).toHaveBeenCalledWith('Derp'); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx new file mode 100644 index 0000000000..cd30e96c92 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -0,0 +1,124 @@ +/* + * 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, { useEffect, useMemo, useRef } from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import CopyIcon from '@material-ui/icons/FileCopy'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList } from 'react-window'; +import { AnsiProcessor } from './AnsiProcessor'; +import { HEADER_SIZE, useStyles } from './styles'; +import classnames from 'classnames'; +import { LogLine } from './LogLine'; +import { LogViewerControls } from './LogViewerControls'; +import { useLogViewerSearch } from './useLogViewerSearch'; +import { useLogViewerSelection } from './useLogViewerSelection'; + +export interface RealLogViewerProps { + text: string; + classes?: { root?: string }; +} + +export function RealLogViewer(props: RealLogViewerProps) { + const classes = useStyles({ classes: props.classes }); + const listRef = useRef(null); + + // The processor keeps state that optimizes appending to the text + const processor = useMemo(() => new AnsiProcessor(), []); + const lines = processor.process(props.text); + + const search = useLogViewerSearch(lines); + const selection = useLogViewerSelection(lines); + + useEffect(() => { + if (search.resultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(search.resultLine - 1, 'center'); + } + }, [search.resultLine]); + + const handleSelectLine = ( + line: number, + event: { shiftKey: boolean; preventDefault: () => void }, + ) => { + event.preventDefault(); + selection.setSelection(line, event.shiftKey); + }; + + return ( + + {({ height, width }) => ( +
+ )} + + ); +} diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts new file mode 100644 index 0000000000..f99695f163 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { LogViewer } from './LogViewer'; +export type { LogViewerProps } from './LogViewer'; +export type { LogViewerClassKey } from './styles'; diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts new file mode 100644 index 0000000000..2028809543 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -0,0 +1,168 @@ +/* + * 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 { alpha, makeStyles } from '@material-ui/core/styles'; +import * as colors from '@material-ui/core/colors'; + +export const HEADER_SIZE = 40; + +/** @public Class keys for overriding LogViewer styles */ +export type LogViewerClassKey = + | 'root' + | 'header' + | 'log' + | 'line' + | 'lineSelected' + | 'lineCopyButton' + | 'lineNumber' + | 'textHighlight' + | 'textSelectedHighlight' + | 'modifierBold' + | 'modifierItalic' + | 'modifierUnderline' + | 'modifierForegroundBlack' + | 'modifierForegroundRed' + | 'modifierForegroundGreen' + | 'modifierForegroundYellow' + | 'modifierForegroundBlue' + | 'modifierForegroundMagenta' + | 'modifierForegroundCyan' + | 'modifierForegroundWhite' + | 'modifierForegroundGrey' + | 'modifierBackgroundBlack' + | 'modifierBackgroundRed' + | 'modifierBackgroundGreen' + | 'modifierBackgroundYellow' + | 'modifierBackgroundBlue' + | 'modifierBackgroundMagenta' + | 'modifierBackgroundCyan' + | 'modifierBackgroundWhite' + | 'modifierBackgroundGrey'; + +export const useStyles = makeStyles( + theme => ({ + root: { + background: theme.palette.background.paper, + }, + header: { + height: HEADER_SIZE, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + }, + log: { + fontFamily: '"Monaco", monospace', + fontSize: theme.typography.pxToRem(12), + }, + line: { + position: 'relative', + whiteSpace: 'pre', + + '&:hover': { + background: theme.palette.action.hover, + }, + }, + lineSelected: { + background: theme.palette.action.selected, + + '&:hover': { + background: theme.palette.action.selected, + }, + }, + lineCopyButton: { + position: 'absolute', + paddingTop: 0, + paddingBottom: 0, + }, + lineNumber: { + display: 'inline-block', + textAlign: 'end', + width: 60, + marginRight: theme.spacing(1), + cursor: 'pointer', + }, + textHighlight: { + background: alpha(theme.palette.info.main, 0.15), + }, + textSelectedHighlight: { + background: alpha(theme.palette.info.main, 0.4), + }, + modifierBold: { + fontWeight: theme.typography.fontWeightBold, + }, + modifierItalic: { + fontStyle: 'italic', + }, + modifierUnderline: { + textDecoration: 'underline', + }, + modifierForegroundBlack: { + color: colors.common.black, + }, + modifierForegroundRed: { + color: colors.red[500], + }, + modifierForegroundGreen: { + color: colors.green[500], + }, + modifierForegroundYellow: { + color: colors.yellow[500], + }, + modifierForegroundBlue: { + color: colors.blue[500], + }, + modifierForegroundMagenta: { + color: colors.purple[500], + }, + modifierForegroundCyan: { + color: colors.cyan[500], + }, + modifierForegroundWhite: { + color: colors.common.white, + }, + modifierForegroundGrey: { + color: colors.grey[500], + }, + modifierBackgroundBlack: { + background: colors.common.black, + }, + modifierBackgroundRed: { + background: colors.red[500], + }, + modifierBackgroundGreen: { + background: colors.green[500], + }, + modifierBackgroundYellow: { + background: colors.yellow[500], + }, + modifierBackgroundBlue: { + background: colors.blue[500], + }, + modifierBackgroundMagenta: { + background: colors.purple[500], + }, + modifierBackgroundCyan: { + background: colors.cyan[500], + }, + modifierBackgroundWhite: { + background: colors.common.white, + }, + modifierBackgroundGrey: { + background: colors.grey[500], + }, + }), + { name: 'BackstageLogViewer' }, +); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx new file mode 100644 index 0000000000..a64f22765b --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx @@ -0,0 +1,221 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { applySearchFilter, useLogViewerSearch } from './useLogViewerSearch'; +import { AnsiLine } from './AnsiProcessor'; + +const lines = [ + new AnsiLine(1, [{ text: 'FooBar', modifiers: {} }]), + new AnsiLine(2, [{ text: 'Baz', modifiers: {} }]), + new AnsiLine(3, [{ text: 'FooBarFoo', modifiers: {} }]), + new AnsiLine(4, [{ text: 'Baz', modifiers: {} }]), + new AnsiLine(5, [{ text: 'BazFoo', modifiers: {} }]), + new AnsiLine(6, [{ text: 'FooFooFoo', modifiers: {} }]), + new AnsiLine(7, [{ text: '', modifiers: {} }]), + new AnsiLine(8, [{ text: 'Bar', modifiers: {} }]), +]; + +describe('applySearchFilter', () => { + it('should find search results', () => { + expect(applySearchFilter(lines, '')).toEqual({ + lines: lines, + results: undefined, + }); + expect(applySearchFilter(lines, 'foo')).toEqual({ + lines: [lines[0], lines[2], lines[4], lines[5]], + results: [ + { lineNumber: 1, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 1 }, + { lineNumber: 5, lineIndex: 0 }, + { lineNumber: 6, lineIndex: 0 }, + { lineNumber: 6, lineIndex: 1 }, + { lineNumber: 6, lineIndex: 2 }, + ], + }); + expect(applySearchFilter(lines, 'bar')).toEqual({ + lines: [lines[0], lines[2], lines[7]], + results: [ + { lineNumber: 1, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 0 }, + { lineNumber: 8, lineIndex: 0 }, + ], + }); + expect(applySearchFilter(lines, 'baz')).toEqual({ + lines: [lines[1], lines[3], lines[4]], + results: [ + { lineNumber: 2, lineIndex: 0 }, + { lineNumber: 4, lineIndex: 0 }, + { lineNumber: 5, lineIndex: 0 }, + ], + }); + }); +}); + +describe('useLogViewerSearch', () => { + it('should provide search state', () => { + const rendered = renderHook(() => useLogViewerSearch(lines)); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: false, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + rendered.result.current.resultIndexStep(); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: false, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + act(() => rendered.result.current.toggleShouldFilter()); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: true, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + act(() => rendered.result.current.setSearchInput('BAR')); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + searchInput: 'BAR', + searchText: 'bar', + shouldFilter: true, + resultCount: 3, + resultIndex: 0, + resultLine: 1, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 1, + resultLine: 3, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 2, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 0, + resultLine: 1, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep(true)); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 2, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.setSearchInput('FOO')); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[4], lines[5]], + searchInput: 'FOO', + searchText: 'foo', + shouldFilter: true, + resultCount: 7, + resultIndex: 2, + resultLine: 3, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.toggleShouldFilter()); + expect(rendered.result.current).toMatchObject({ + lines, + shouldFilter: false, + resultCount: 7, + resultIndex: 2, + resultLine: 3, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines, + searchInput: 'FOO', + searchText: 'foo', + shouldFilter: false, + resultCount: 7, + resultIndex: 3, + resultLine: 5, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 4, + resultLine: 6, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 5, + resultLine: 6, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 6, + resultLine: 6, + resultLineIndex: 2, + }); + + act(() => rendered.result.current.setSearchInput('BAR')); + expect(rendered.result.current).toMatchObject({ + searchText: 'bar', + resultCount: 3, + resultIndex: 6, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep(true)); + expect(rendered.result.current).toMatchObject({ + searchText: 'bar', + resultCount: 3, + resultIndex: 1, + resultLine: 3, + resultLineIndex: 0, + }); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx new file mode 100644 index 0000000000..4462a3ff50 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx @@ -0,0 +1,117 @@ +/* + * 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 { useMemo, useState } from 'react'; +import { useToggle } from 'react-use'; +import { AnsiLine } from './AnsiProcessor'; + +export function applySearchFilter(lines: AnsiLine[], searchText: string) { + if (!searchText) { + return { lines }; + } + + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + let offset = 0; + let lineResultIndex = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + searchResults.push({ + lineNumber: line.lineNumber, + lineIndex: lineResultIndex++, + }); + offset = start + searchText.length; + } + } + } + + return { + lines: matchingLines, + results: searchResults, + }; +} + +export interface LogViewerSearch { + lines: AnsiLine[]; + + searchText: string; + searchInput: string; + setSearchInput: (searchInput: string) => void; + + shouldFilter: boolean; + toggleShouldFilter: () => void; + + resultCount: number | undefined; + resultIndex: number | undefined; + resultIndexStep: (decrement?: boolean) => void; + + resultLine: number | undefined; + resultLineIndex: number | undefined; +} + +export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { + const [searchInput, setSearchInput] = useState(''); + const searchText = searchInput.toLocaleLowerCase('en-US'); + + const [resultIndex, setResultIndex] = useState(0); + + const [shouldFilter, toggleShouldFilter] = useToggle(false); + + const filter = useMemo( + () => applySearchFilter(lines, searchText), + [lines, searchText], + ); + + const searchResult = filter.results + ? filter.results[Math.min(resultIndex, filter.results.length - 1)] + : undefined; + const resultCount = filter.results?.length; + + const resultIndexStep = (decrement?: boolean) => { + if (decrement) { + if (resultCount !== undefined) { + const next = Math.min(resultIndex - 1, resultCount - 2); + setResultIndex(next < 0 ? resultCount - 1 : next); + } + } else { + if (resultCount !== undefined) { + const next = resultIndex + 1; + setResultIndex(next >= resultCount ? 0 : next); + } + } + }; + + return { + lines: shouldFilter ? filter.lines : lines, + searchText, + searchInput, + setSearchInput, + shouldFilter, + toggleShouldFilter, + resultCount, + resultIndex, + resultIndexStep, + resultLine: searchResult?.lineNumber, + resultLineIndex: searchResult?.lineIndex, + }; +} diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx new file mode 100644 index 0000000000..7e00d50f33 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { AnsiLine } from './AnsiProcessor'; +import { useLogViewerSelection } from './useLogViewerSelection'; +// eslint-disable-next-line import/no-extraneous-dependencies +import copyToClipboard from 'copy-to-clipboard'; + +// Used by useCopyToClipboard +jest.mock('copy-to-clipboard', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const lines = [ + new AnsiLine(1, [{ text: '1', modifiers: {} }]), + new AnsiLine(2, [{ text: '2', modifiers: {} }]), + new AnsiLine(3, [{ text: '3', modifiers: {} }]), + new AnsiLine(4, [{ text: '4', modifiers: {} }]), + new AnsiLine(5, [{ text: '5', modifiers: {} }]), +]; + +describe('useLogViewerSelection', () => { + it('should manage a selection', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(false); + + act(() => rendered.result.current.setSelection(2, false)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(true); + expect(rendered.result.current.isSelected(3)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(true); + expect(rendered.result.current.shouldShowButton(3)).toBe(false); + + act(() => rendered.result.current.setSelection(3, false)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(false); + + act(() => rendered.result.current.setSelection(1, true)); + + expect(rendered.result.current.isSelected(1)).toBe(true); + expect(rendered.result.current.isSelected(2)).toBe(true); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(true); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(false); + + act(() => rendered.result.current.setSelection(4, true)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(true); + expect(rendered.result.current.isSelected(5)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(true); + expect(rendered.result.current.shouldShowButton(5)).toBe(false); + + expect(copyToClipboard).not.toHaveBeenCalled(); + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenLastCalledWith('3\n4'); + + act(() => rendered.result.current.setSelection(2, true)); + act(() => rendered.result.current.setSelection(4, true)); + + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenCalledWith('2\n3\n4'); + + act(() => rendered.result.current.setSelection(2, false)); + act(() => rendered.result.current.setSelection(4, false)); + act(() => rendered.result.current.setSelection(4, false)); + act(() => rendered.result.current.setSelection(5, true)); + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenCalledWith('5'); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx new file mode 100644 index 0000000000..a41f5e7159 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx @@ -0,0 +1,70 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { useEffect, useState } from 'react'; +import { useCopyToClipboard } from 'react-use'; +import { AnsiLine } from './AnsiProcessor'; + +export function useLogViewerSelection(lines: AnsiLine[]) { + const errorApi = useApi(errorApiRef); + const [sel, setSelection] = useState<{ start: number; end: number }>(); + const start = sel ? Math.min(sel.start, sel.end) : undefined; + const end = sel ? Math.max(sel.start, sel.end) : undefined; + + const [{ error }, copyToClipboard] = useCopyToClipboard(); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return { + shouldShowButton(line: number) { + return start === line || end === line; + }, + isSelected(line: number) { + if (!sel) { + return false; + } + return start! <= line && line <= end!; + }, + setSelection(line: number, add: boolean) { + if (add) { + setSelection(s => + s ? { start: s.start, end: line } : { start: line, end: line }, + ); + } else { + setSelection(s => + s?.start === line && s?.end === line + ? undefined + : { start: line, end: line }, + ); + } + }, + copySelection() { + if (sel) { + const copyText = lines + .slice(Math.min(sel.start, sel.end) - 1, Math.max(sel.start, sel.end)) + .map(l => l.chunks.map(c => c.text).join('')) + .join('\n'); + copyToClipboard(copyText); + setSelection(undefined); + } + }, + }; +} diff --git a/packages/core-components/src/components/Progress/Progress.test.tsx b/packages/core-components/src/components/Progress/Progress.test.tsx index 4f4e74ce08..d4057c1675 100644 --- a/packages/core-components/src/components/Progress/Progress.test.tsx +++ b/packages/core-components/src/components/Progress/Progress.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { Progress } from './Progress'; diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx index bbd48c127c..f76524fe50 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { RoutedTabs } from './RoutedTabs'; diff --git a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx index 2bc60783d7..55422faffc 100644 --- a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { TabbedLayout } from './TabbedLayout'; diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 3c9376a894..473fab8444 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -30,6 +30,7 @@ export * from './HeaderIconLinkRow'; export * from './HorizontalScrollGrid'; export * from './Lifecycle'; export * from './Link'; +export * from './LogViewer'; export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './OverflowTooltip'; diff --git a/packages/core-components/src/layout/Sidebar/Bar.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx new file mode 100644 index 0000000000..d288aa94c5 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx @@ -0,0 +1,108 @@ +/* + * 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 React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import BuildRoundedIcon from '@material-ui/icons/BuildRounded'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import MenuBookIcon from '@material-ui/icons/MenuBook'; +import AcUnitIcon from '@material-ui/icons/AcUnit'; +import { Sidebar, SidebarExpandButton } from './Bar'; +import { SidebarItem, SidebarSearchField } from './Items'; +import { SidebarSubmenuItem } from './SidebarSubmenuItem'; +import { SidebarSubmenu } from './SidebarSubmenu'; +import { SidebarPinStateContext } from '.'; + +async function renderScalableSidebar() { + await renderInTestApp( + {} }} + > + + {}} to="/search" /> + {}} text="Catalog"> + + + + + + + + + , + ); +} + +describe('Sidebar', () => { + beforeEach(async () => { + await renderScalableSidebar(); + }); + + describe('Click to Expand', () => { + it('Sidebar should show expanded items when expand button is clicked', async () => { + userEvent.click(screen.getByTestId('sidebar-expand-button')); + expect(await screen.findByText('Create...')).toBeInTheDocument(); + }); + it('Sidebar should not show expanded items when hovered on', async () => { + userEvent.hover(screen.getByTestId('sidebar-root')); + expect(screen.queryByText('Create...')).not.toBeInTheDocument(); + }); + }); + describe('Submenu Items', () => { + it('Extended sidebar with submenu content hidden by default', async () => { + expect(screen.queryByText('Tools')).not.toBeInTheDocument(); + expect(screen.queryByText('Misc')).not.toBeInTheDocument(); + }); + + it('Extended sidebar with submenu content visible when hover over submenu items', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + expect(await screen.findByText('Tools')).toBeInTheDocument(); + expect(await screen.findByText('Misc')).toBeInTheDocument(); + }); + + it('Multicategory item in submenu shows drop down on click', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + userEvent.click(screen.getByText('Misc')); + expect(screen.getByText('dropdown item 1')).toBeInTheDocument(); + expect(screen.getByText('dropdown item 2')).toBeInTheDocument(); + }); + + it('Dropdown item in submenu renders a link when `to` value is provided', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + userEvent.click(screen.getByText('Misc')); + expect(screen.getByText('dropdown item 1').closest('a')).toHaveAttribute( + 'href', + '/dropdownitemlink', + ); + }); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index e6fa3820ad..bada68a028 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -16,11 +16,13 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import clsx from 'clsx'; -import React, { useRef, useState, useContext, PropsWithChildren } from 'react'; +import classnames from 'classnames'; +import React, { useState, useContext, PropsWithChildren, useRef } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; +import DoubleArrowRight from './icons/DoubleArrowRight'; +import DoubleArrowLeft from './icons/DoubleArrowLeft'; export type SidebarClassKey = 'root' | 'drawer' | 'drawerOpen'; @@ -46,7 +48,6 @@ const useStyles = makeStyles( msOverflowStyle: 'none', scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, - borderRight: `1px solid #383838`, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, @@ -58,6 +59,19 @@ const useStyles = makeStyles( display: 'none', }, }, + expandButton: { + background: 'none', + border: 'none', + color: theme.palette.navigation.color, + width: '100%', + cursor: 'pointer', + position: 'relative', + height: 48, + }, + arrows: { + position: 'absolute', + right: 10, + }, drawerOpen: { width: sidebarConfig.drawerWidthOpen, transition: theme.transitions.create('width', { @@ -78,10 +92,12 @@ enum State { type Props = { openDelayMs?: number; closeDelayMs?: number; + disableExpandOnHover?: boolean; }; export function Sidebar(props: PropsWithChildren) { const { + disableExpandOnHover = false, openDelayMs = sidebarConfig.defaultOpenDelayMs, closeDelayMs = sidebarConfig.defaultCloseDelayMs, children, @@ -132,22 +148,31 @@ export function Sidebar(props: PropsWithChildren) { const isOpen = (state === State.Open && !isSmallScreen) || isPinned; + const setOpen = (open: boolean) => { + if (open) { + handleOpen(); + } else { + handleClose(); + } + }; + return (
{} : handleOpen} + onFocus={disableExpandOnHover ? () => {} : handleOpen} + onMouseLeave={disableExpandOnHover ? () => {} : handleClose} + onBlur={disableExpandOnHover ? () => {} : handleClose} >
@@ -157,3 +182,39 @@ export function Sidebar(props: PropsWithChildren) {
); } + +/** + * A button which allows you to expand the sidebar when clicked. + * Use optionally to replace sidebar's expand-on-hover feature with expand-on-click. + * + * @public + */ +export const SidebarExpandButton = () => { + const classes = useStyles(); + const { isOpen, setOpen } = useContext(SidebarContext); + const { isPinned } = useContext(SidebarPinStateContext); + const isSmallScreen = useMediaQuery(theme => + theme.breakpoints.down('md'), + ); + + const handleClick = () => { + setOpen(!isOpen); + }; + + if (isPinned || isSmallScreen) { + return null; + } + + return ( + + ); +}; diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 123c30ceae..f4d699d09c 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -20,7 +20,7 @@ import { createEvent, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import HomeIcon from '@material-ui/icons/Home'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import { Sidebar } from './Bar'; +import { Sidebar, SidebarExpandButton } from './Bar'; import { SidebarItem, SidebarSearchField } from './Items'; import { renderHook } from '@testing-library/react-hooks'; import { hexToRgb, makeStyles } from '@material-ui/core/styles'; @@ -44,6 +44,7 @@ async function renderSidebar() { text="Create..." className={result.current.spotlight} /> + , ); userEvent.hover(screen.getByTestId('sidebar-root')); diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 3f15ae4ea6..c8c6db13a2 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -14,18 +14,21 @@ * limitations under the License. */ -import { IconComponent } from '@backstage/core-plugin-api'; +import { IconComponent, useElementFilter } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, styled, Theme } from '@material-ui/core/styles'; import Badge from '@material-ui/core/Badge'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; import { CreateCSSProperties } from '@material-ui/core/styles/withStyles'; +import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import SearchIcon from '@material-ui/icons/Search'; -import clsx from 'clsx'; +import classnames from 'classnames'; import React, { + Children, forwardRef, KeyboardEventHandler, + PropsWithChildren, ReactNode, useContext, useState, @@ -33,10 +36,16 @@ import React, { import { Link, NavLinkProps, + resolvePath, useLocation, useResolvedPath, } from 'react-router-dom'; -import { sidebarConfig, SidebarContext } from './config'; +import { + sidebarConfig, + SidebarContext, + SidebarItemWithSubmenuContext, +} from './config'; +import { SidebarSubmenu } from './SidebarSubmenu'; export type SidebarItemClassKey = | 'root' @@ -85,6 +94,16 @@ const useStyles = makeStyles( open: { width: drawerWidthOpen, }, + highlightable: { + '&:hover': { + background: + theme.palette.navigation.navItem?.hoverBackground ?? '#404040', + }, + }, + highlighted: { + background: + theme.palette.navigation.navItem?.hoverBackground ?? '#404040', + }, label: { // XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs fontWeight: 'bold', @@ -123,13 +142,24 @@ const useStyles = makeStyles( textAlign: 'center', marginRight: theme.spacing(1), }, + closedItemIcon: { + width: '100%', + justifyContent: 'center', + }, + submenuArrow: { + position: 'absolute', + right: 0, + }, selected: { '&$root': { borderLeft: `solid ${selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, color: theme.palette.navigation.selectedColor, }, '&$closed': { - width: drawerWidthClosed - selectedIndicatorWidth, + width: drawerWidthClosed, + }, + '& $closedItemIcon': { + paddingRight: selectedIndicatorWidth, }, '& $iconContainer': { marginLeft: -selectedIndicatorWidth, @@ -140,16 +170,124 @@ const useStyles = makeStyles( { name: 'BackstageSidebarItem' }, ); +function isSidebarItemWithSubmenuActive( + submenu: ReactNode, + locationPathname: string, +) { + // Item is active if any of submenu items have active paths + const toPathnames: string[] = []; + let isActive = false; + let submenuItems: ReactNode; + Children.forEach(submenu, element => { + if (!React.isValidElement(element)) return; + submenuItems = element.props.children; + }); + Children.forEach(submenuItems, element => { + if (!React.isValidElement(element)) return; + if (element.props.dropdownItems) { + element.props.dropdownItems.map((item: { to: string }) => + toPathnames.push(item.to), + ); + } else if (element.props.to) { + toPathnames.push(element.props.to); + } + }); + isActive = toPathnames.some(to => { + const toPathname = resolvePath(to); + return locationPathname === toPathname.pathname; + }); + return isActive; +} + +const SidebarItemWithSubmenu = ({ + text, + hasNotifications = false, + icon: Icon, + children, +}: PropsWithChildren) => { + const classes = useStyles(); + const [isHoveredOn, setIsHoveredOn] = useState(false); + const { pathname: locationPathname } = useLocation(); + const isActive = isSidebarItemWithSubmenuActive(children, locationPathname); + + const handleMouseEnter = () => { + setIsHoveredOn(true); + }; + const handleMouseLeave = () => { + setIsHoveredOn(false); + }; + + const { isOpen } = useContext(SidebarContext); + const itemIcon = ( + + + + ); + const openContent = ( + <> +
+ {itemIcon} +
+ {text && ( + + {text} + + )} +
{}
+ + ); + const closedContent = itemIcon; + + return ( + +
+
+ {isOpen ? openContent : closedContent} + {!isHoveredOn && ( + + )} +
+ {isHoveredOn && children} +
+
+ ); +}; + type SidebarItemBaseProps = { icon: IconComponent; text?: string; hasNotifications?: boolean; - children?: ReactNode; + disableHighlight?: boolean; className?: string; }; type SidebarItemButtonProps = SidebarItemBaseProps & { onClick: (ev: React.MouseEvent) => void; + children?: ReactNode; }; type SidebarItemLinkProps = SidebarItemBaseProps & { @@ -157,7 +295,21 @@ type SidebarItemLinkProps = SidebarItemBaseProps & { onClick?: (ev: React.MouseEvent) => void; } & NavLinkProps; -type SidebarItemProps = SidebarItemButtonProps | SidebarItemLinkProps; +type SidebarItemWithSubmenuProps = SidebarItemBaseProps & { + to?: string; + onClick?: (ev: React.MouseEvent) => void; + children: ReactNode; +}; + +/** + * SidebarItem with 'to' property will be a clickable link. + * SidebarItem with 'onClick' property and without 'to' property will be a clickable button. + * SidebarItem which wraps a SidebarSubmenu will be a clickable button which opens a submenu. + */ +type SidebarItemProps = + | SidebarItemLinkProps + | SidebarItemButtonProps + | SidebarItemWithSubmenuProps; function isButtonItem( props: SidebarItemProps, @@ -208,7 +360,10 @@ export const WorkaroundNavLink = React.forwardRef< ref={ref} aria-current={ariaCurrent} style={{ ...style, ...(isActive ? activeStyle : undefined) }} - className={clsx([className, isActive ? activeClassName : undefined])} + className={classnames([ + className, + isActive ? activeClassName : undefined, + ])} /> ); }); @@ -218,6 +373,7 @@ export const SidebarItem = forwardRef((props, ref) => { icon: Icon, text, hasNotifications = false, + disableHighlight = false, onClick, children, className, @@ -235,6 +391,7 @@ export const SidebarItem = forwardRef((props, ref) => { variant="dot" overlap="circular" invisible={!hasNotifications} + className={classnames({ [classes.closedItemIcon]: !isOpen })} > @@ -260,14 +417,48 @@ export const SidebarItem = forwardRef((props, ref) => { const childProps = { onClick, - className: clsx( + className: classnames( className, classes.root, isOpen ? classes.open : classes.closed, isButtonItem(props) && classes.buttonItem, + { [classes.highlightable]: !disableHighlight }, ), }; + let hasSubmenu = false; + let submenu: ReactNode; + const componentType = ( + + <> + + ).type; + // Filter children for SidebarSubmenu components + const submenus = useElementFilter(children, elements => + elements.getElements().filter(child => child.type === componentType), + ); + // Error thrown if more than one SidebarSubmenu in a SidebarItem + if (submenus.length > 1) { + throw new Error( + 'Cannot render more than one SidebarSubmenu inside a SidebarItem', + ); + } else if (submenus.length === 1) { + hasSubmenu = true; + submenu = submenus[0]; + } + + if (hasSubmenu) { + return ( + + {submenu} + + ); + } + if (isButtonItem(props)) { return ( + {dropdownItems && showDropDown && ( +
+ {dropdownItems.map((object, key) => ( + + + {object.title} + + + ))} +
+ )} +
+ ); + } + + return ( +
+ + + + {title} + + +
+ ); +}; diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index d2a690e38f..dbcdb7d788 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createContext } from 'react'; +import { createContext, Dispatch, SetStateAction } from 'react'; const drawerWidthClosed = 72; const iconPadding = 24; @@ -37,13 +37,43 @@ export const sidebarConfig = { userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2, }; +export const submenuConfig = { + drawerWidthClosed: 0, + drawerWidthOpen: 202, + // As per NN/g's guidance on timing for exposing hidden content + // See https://www.nngroup.com/articles/timing-exposing-content/ + defaultOpenDelayMs: 100, + defaultCloseDelayMs: 0, + defaultFadeDuration: 200, + logoHeight: 32, + iconContainerWidth: drawerWidthClosed, + iconSize: drawerWidthClosed - iconPadding * 2, + iconPadding, + selectedIndicatorWidth: 3, + userBadgePadding, + userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2, +}; + export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; export type SidebarContextType = { isOpen: boolean; + setOpen: (open: boolean) => void; }; export const SidebarContext = createContext({ isOpen: false, + setOpen: _open => {}, }); + +export type SidebarItemWithSubmenuContextType = { + isHoveredOn: boolean; + setIsHoveredOn: Dispatch>; +}; + +export const SidebarItemWithSubmenuContext = + createContext({ + isHoveredOn: false, + setIsHoveredOn: () => {}, + }); diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx new file mode 100644 index 0000000000..eb487940a8 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx @@ -0,0 +1,48 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; + +const useStyles = makeStyles({ + iconContainer: { + display: 'flex', + position: 'relative', + width: '100%', + }, + arrow1: { + right: '6px', + position: 'absolute', + }, +}); + +const DoubleArrowLeft = () => { + const classes = useStyles(); + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default DoubleArrowLeft; diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx new file mode 100644 index 0000000000..e1e9b9476d --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx @@ -0,0 +1,48 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; + +const useStyles = makeStyles({ + iconContainer: { + display: 'flex', + position: 'relative', + width: '100%', + }, + arrow1: { + right: '6px', + position: 'absolute', + }, +}); + +const DoubleArrowRight = () => { + const classes = useStyles(); + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default DoubleArrowRight; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index 067a9bf2eb..79a86023ee 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -export { Sidebar } from './Bar'; +export { Sidebar, SidebarExpandButton } from './Bar'; +export { SidebarSubmenuItem } from './SidebarSubmenuItem'; +export { SidebarSubmenu } from './SidebarSubmenu'; +export type { SidebarSubmenuProps } from './SidebarSubmenu'; +export type { + SidebarSubmenuItemProps, + SidebarSubmenuItemDropdownItem, +} from './SidebarSubmenuItem'; export type { SidebarClassKey } from './Bar'; export { SidebarPage, SidebarPinStateContext } from './Page'; export type { SidebarPinStateContextType, SidebarPageClassKey } from './Page'; diff --git a/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts new file mode 100644 index 0000000000..db4731704c --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts @@ -0,0 +1,60 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +export class GuestUserIdentity implements IdentityApi { + getUserId(): string { + return 'guest'; + } + + async getIdToken(): Promise { + return undefined; + } + + getProfile(): ProfileInfo { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getProfileInfo(): Promise { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getBackstageIdentity(): Promise { + const userEntityRef = `user:default/guest`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + return {}; + } + + async signOut(): Promise {} +} diff --git a/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts new file mode 100644 index 0000000000..ec45bc7399 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts @@ -0,0 +1,65 @@ +/* + * 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 { + BackstageUserIdentity, + IdentityApi, + ProfileInfo, +} from '@backstage/core-plugin-api'; + +export class IdentityApiSignOutProxy implements IdentityApi { + private constructor( + private readonly config: { + identityApi: IdentityApi; + signOut: IdentityApi['signOut']; + }, + ) {} + + static from(config: { + identityApi: IdentityApi; + signOut: IdentityApi['signOut']; + }): IdentityApi { + return new IdentityApiSignOutProxy(config); + } + + getUserId(): string { + return this.config.identityApi.getUserId(); + } + + getIdToken(): Promise { + return this.config.identityApi.getIdToken(); + } + + getProfile(): ProfileInfo { + return this.config.identityApi.getProfile(); + } + + getProfileInfo(): Promise { + return this.config.identityApi.getProfileInfo(); + } + + getBackstageIdentity(): Promise { + return this.config.identityApi.getBackstageIdentity(); + } + + getCredentials(): Promise<{ token?: string | undefined }> { + return this.config.identityApi.getCredentials(); + } + + signOut(): Promise { + return this.config.signOut(); + } +} diff --git a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts new file mode 100644 index 0000000000..e28f94072e --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts @@ -0,0 +1,80 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, + SignInResult, +} from '@backstage/core-plugin-api'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(atob(payload)); +} + +export class LegacyUserIdentity implements IdentityApi { + private constructor(private readonly result: SignInResult) {} + + getUserId(): string { + return this.result.userId; + } + + static fromResult(result: SignInResult): LegacyUserIdentity { + return new LegacyUserIdentity(result); + } + + async getIdToken(): Promise { + return this.result.getIdToken?.(); + } + + getProfile(): ProfileInfo { + return this.result.profile; + } + + async getProfileInfo(): Promise { + return this.result.profile; + } + + async getBackstageIdentity(): Promise { + const token = await this.getIdToken(); + + if (!token) { + const userEntityRef = `user:default/${this.getUserId()}`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + const { sub, ent } = parseJwtPayload(token); + return { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + const token = await this.result.getIdToken?.(); + return { token }; + } + + async signOut(): Promise { + return this.result.signOut?.(); + } +} diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index bd4fe37480..304e427f33 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -15,11 +15,12 @@ */ import { - BackstageIdentity, + BackstageIdentityResponse, configApiRef, SignInPageProps, useApi, } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; @@ -49,7 +50,7 @@ type SingleSignInPageProps = SignInPageProps & { export type Props = MultiSignInPageProps | SingleSignInPageProps; export const MultiSignInPage = ({ - onResult, + onSignInSuccess, providers = [], title, align = 'left', @@ -60,7 +61,7 @@ export const MultiSignInPage = ({ const signInProviders = getSignInProviders(providers); const [loading, providerElements] = useSignInProviders( signInProviders, - onResult, + onSignInSuccess, ); if (loading) { @@ -87,9 +88,9 @@ export const MultiSignInPage = ({ }; export const SingleSignInPage = ({ - onResult, provider, auto, + onSignInSuccess, }: SingleSignInPageProps) => { const classes = useStyles(); const authApi = useApi(provider.apiRef); @@ -105,47 +106,42 @@ export const SingleSignInPage = ({ type LoginOpts = { checkExisting?: boolean; showPopup?: boolean }; const login = async ({ checkExisting, showPopup }: LoginOpts) => { try { - let identity: BackstageIdentity | undefined; + let identityResponse: BackstageIdentityResponse | undefined; if (checkExisting) { // Do an initial check if any logged-in session exists - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ optional: true, }); } // If no session exists, show the sign-in page - if (!identity && (showPopup || auto)) { + if (!identityResponse && (showPopup || auto)) { // Unless auto is set to true, this step should not happen. // When user intentionally clicks the Sign In button, autoShowPopup is set to true setShowLoginPage(true); - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${provider.title} provider is not configured to support sign-in`, ); } } - if (!identity) { + if (!identityResponse) { setShowLoginPage(true); return; } const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + authApi, + profile, + }), + ); } catch (err: any) { // User closed the sign-in modal setError(err); diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts new file mode 100644 index 0000000000..6b136ed65d --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { BackstageUserIdentity, ProfileInfo } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; + +describe('UserIdentity', () => { + it('should cache a successful response from the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest.fn().mockResolvedValue(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await userIdentity.getProfileInfo(); + await userIdentity.getProfileInfo(); + + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(1); + + expect(response).toEqual(mockProfileInfo); + }); + + it('should not cache failures for the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest + .fn() + .mockRejectedValueOnce(new Error('boop')) + .mockResolvedValueOnce(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await expect(() => userIdentity.getProfileInfo()).rejects.toThrow('boop'); + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(2); + + expect(response).toEqual(mockProfileInfo); + }); +}); diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts new file mode 100644 index 0000000000..7781c79154 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -0,0 +1,142 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + ProfileInfoApi, + BackstageUserIdentity, + BackstageIdentityApi, + SessionApi, + SignInResult, +} from '@backstage/core-plugin-api'; + +import { GuestUserIdentity } from './GuestUserIdentity'; +import { LegacyUserIdentity } from './LegacyUserIdentity'; + +/** + * An implementation of the IdentityApi that is constructed using + * various backstage user identity representations. + * + * @public + */ +export class UserIdentity implements IdentityApi { + private profilePromise?: Promise; + /** + * Creates a new IdentityApi that acts as a Guest User. + * + * @public + */ + static createGuest(): IdentityApi { + return new GuestUserIdentity(); + } + + /** + * Creates a new IdentityApi using a legacy SignInResult object. + * + * @public + */ + static fromLegacy(result: SignInResult): IdentityApi { + return LegacyUserIdentity.fromResult(result); + } + + /** + * Creates a new IdentityApi implementation using a user identity + * and an auth API that will be used to request backstage tokens. + * + * @public + */ + static create(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + /** + * Passing a profile synchronously allows the deprecated `getProfile` method to be + * called by consumers of the {@link @backstage/core-plugin-api#IdentityApi}. If you + * do not have any consumers of that method then this is safe to leave out. + * + * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. + */ + profile?: ProfileInfo; + }): IdentityApi { + return new UserIdentity(options.identity, options.authApi, options.profile); + } + + private constructor( + private readonly identity: BackstageUserIdentity, + private readonly authApi: ProfileInfoApi & + BackstageIdentityApi & + SessionApi, + private readonly profile?: ProfileInfo, + ) {} + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ + getUserId(): string { + const ref = this.identity.userEntityRef; + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); + if (!match) { + throw new TypeError(`Invalid user entity reference "${ref}"`); + } + + return match[3]; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ + async getIdToken(): Promise { + const identity = await this.authApi.getBackstageIdentity(); + return identity!.token; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ + getProfile(): ProfileInfo { + if (!this.profile) { + throw new Error( + 'The identity API does not implement synchronous profile fetching, use getProfileInfo() instead', + ); + } + return this.profile; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ + async getProfileInfo(): Promise { + if (this.profilePromise) { + return await this.profilePromise; + } + + try { + this.profilePromise = this.authApi.getProfile() as Promise; + return await this.profilePromise; + } catch (ex) { + this.profilePromise = undefined; + throw ex; + } + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ + async getBackstageIdentity(): Promise { + return this.identity; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ + async getCredentials(): Promise<{ token?: string | undefined }> { + const identity = await this.authApi.getBackstageIdentity(); + return { token: identity!.token }; + } + + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ + async signOut(): Promise { + return this.authApi.signOut(); + } +} diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx index e1372a2362..739c3709a9 100644 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx @@ -26,17 +26,18 @@ import { errorApiRef, } from '@backstage/core-plugin-api'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const auth0AuthApi = useApi(auth0AuthApiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( 'The Auth0 provider is not configured to support sign-in', ); @@ -44,15 +45,13 @@ const Component: ProviderComponent = ({ onResult }) => { const profile = await auth0AuthApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Auth0 login failed', error)); } @@ -77,25 +76,20 @@ const Component: ProviderComponent = ({ onResult }) => { const loader: ProviderLoader = async apis => { const auth0AuthApi = apis.get(auth0AuthApiRef)!; - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await auth0AuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }; + return UserIdentity.create({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }); }; export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 515ae01e4d..8f426a3907 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -27,36 +27,33 @@ import { import { useApi, errorApiRef } from '@backstage/core-plugin-api'; import { GridItem } from './styles'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ config, onResult }) => { +const Component: ProviderComponent = ({ config, onSignInSuccess }) => { const { apiRef, title, message } = config as SignInProviderConfig; const authApi = useApi(apiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${title} provider is not configured to support sign-in`, ); } const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + + onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + profile, + authApi, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Login failed', error)); } @@ -82,25 +79,21 @@ const Component: ProviderComponent = ({ config, onResult }) => { const loader: ProviderLoader = async (apis, apiRef) => { const authApi = apis.get(apiRef)!; - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await authApi.getProfile(); - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - authApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await authApi.signOut(); - }, - }; + return UserIdentity.create({ + identity: identityResponse.identity, + profile, + authApi, + }); }; export const commonProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index da1848ac05..13ba906079 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -26,6 +26,7 @@ import isEmpty from 'lodash/isEmpty'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { GridItem } from './styles'; +import { UserIdentity } from './UserIdentity'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; @@ -60,7 +61,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => { }; }; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const classes = useFormStyles(); const { register, handleSubmit, formState } = useForm({ mode: 'onChange', @@ -68,14 +69,15 @@ const Component: ProviderComponent = ({ onResult }) => { const { errors } = formState; - const handleResult = ({ userId, idToken }: Data) => { - onResult({ - userId, - profile: { - email: `${userId}@example.com`, - }, - getIdToken: idToken ? async () => idToken : undefined, - }); + const handleResult = ({ userId }: Data) => { + onSignInSuccess( + UserIdentity.fromLegacy({ + userId, + profile: { + email: `${userId}@example.com`, + }, + }), + ); }; return ( diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index e91d9cbc75..b2370a98d5 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,16 +20,9 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { GuestUserIdentity } from './GuestUserIdentity'; -const result = { - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, -}; - -const Component: ProviderComponent = ({ onResult }) => ( +const Component: ProviderComponent = ({ onSignInSuccess }) => ( ( @@ -56,7 +49,7 @@ const Component: ProviderComponent = ({ onResult }) => ( ); const loader: ProviderLoader = async () => { - return result; + return new GuestUserIdentity(); }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/index.ts b/packages/core-components/src/layout/SignInPage/index.ts index caa8506399..b3c7618f50 100644 --- a/packages/core-components/src/layout/SignInPage/index.ts +++ b/packages/core-components/src/layout/SignInPage/index.ts @@ -18,3 +18,4 @@ export type { SignInProviderConfig } from './types'; export { SignInPage } from './SignInPage'; export type { SignInPageClassKey } from './styles'; export type { CustomProviderClassKey } from './customProvider'; +export { UserIdentity } from './UserIdentity'; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 19915e6aa9..23d6c95b3c 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -17,10 +17,10 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { SignInPageProps, - SignInResult, useApi, useApiHolder, errorApiRef, + IdentityApi, } from '@backstage/core-plugin-api'; import { IdentityProviders, @@ -30,6 +30,7 @@ import { import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; +import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -81,7 +82,7 @@ export function getSignInProviders( export const useSignInProviders = ( providers: SignInProviderType, - onResult: SignInPageProps['onResult'], + onSignInSuccess: SignInPageProps['onSignInSuccess'], ) => { const errorApi = useApi(errorApiRef); const apiHolder = useApiHolder(); @@ -89,16 +90,18 @@ export const useSignInProviders = ( // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( - (result: SignInResult) => { - onResult({ - ...result, - signOut: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.signOut?.(); - }, - }); + (identityApi: IdentityApi) => { + onSignInSuccess( + IdentityApiSignOutProxy.from({ + identityApi, + signOut: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await identityApi.signOut?.(); + }, + }), + ); }, - [onResult], + [onSignInSuccess], ); // In this effect we check if the user has already selected an existing login @@ -151,7 +154,14 @@ export const useSignInProviders = ( return () => { didCancel = true; }; - }, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]); + }, [ + loading, + errorApi, + onSignInSuccess, + apiHolder, + providers, + handleWrappedResult, + ]); // This renders all available sign-in providers const elements = useMemo( @@ -161,7 +171,7 @@ export const useSignInProviders = ( const { Component } = provider.components; - const handleResult = (result: SignInResult) => { + const handleSignInSuccess = (result: IdentityApi) => { localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id); handleWrappedResult(result); @@ -171,7 +181,7 @@ export const useSignInProviders = ( ); }), diff --git a/packages/core-components/src/layout/SignInPage/types.ts b/packages/core-components/src/layout/SignInPage/types.ts index 1e4e4a2997..5b458448bf 100644 --- a/packages/core-components/src/layout/SignInPage/types.ts +++ b/packages/core-components/src/layout/SignInPage/types.ts @@ -17,12 +17,12 @@ import { ComponentType } from 'react'; import { SignInPageProps, - SignInResult, ApiHolder, ApiRef, ProfileInfoApi, BackstageIdentityApi, SessionApi, + IdentityApi, } from '@backstage/core-plugin-api'; export type SignInProviderConfig = { @@ -41,7 +41,7 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, apiRef: ApiRef, -) => Promise; +) => Promise; export type SignInProvider = { Component: ProviderComponent; diff --git a/packages/core-components/src/overridableComponents.ts b/packages/core-components/src/overridableComponents.ts index af35445e24..98b8f83abd 100644 --- a/packages/core-components/src/overridableComponents.ts +++ b/packages/core-components/src/overridableComponents.ts @@ -34,6 +34,7 @@ import { LifecycleClassKey, MarkdownContentClassKey, LoginRequestListItemClassKey, + LogViewerClassKey, OAuthRequestDialogClassKey, OverflowTooltipClassKey, GaugeClassKey, @@ -110,6 +111,7 @@ type BackstageComponentsNameToClassKey = { BackstageLifecycle: LifecycleClassKey; BackstageMarkdownContent: MarkdownContentClassKey; BackstageLoginRequestListItem: LoginRequestListItemClassKey; + BackstageLogViewer: LogViewerClassKey; OAuthRequestDialog: OAuthRequestDialogClassKey; BackstageOverflowTooltip: OverflowTooltipClassKey; BackstageGauge: GaugeClassKey; diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 0022c99c68..fdf64779fb 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/core-plugin-api +## 0.3.0 + +### Minor Changes + +- a036b65c2f: The `IdentityApi` has received several updates. The `getUserId`, `getProfile`, and `getIdToken` have all been deprecated. + + The replacement for `getUserId` is the new `getBackstageIdentity` method, which provides both the `userEntityRef` as well as the `ownershipEntityRefs` that are used to resolve ownership. Existing usage of the user ID would typically be using a fixed entity kind and namespace, for example `` `user:default/${identityApi.getUserId()}` ``, this kind of usage should now instead use the `userEntityRef` directly. + + The replacement for `getProfile` is the new async `getProfileInfo`. + + The replacement for `getIdToken` is the new `getCredentials` method, which provides an optional token to the caller like before, but it is now wrapped in an object for forwards compatibility. + + The deprecated `idToken` field of the `BackstageIdentity` type has been removed, leaving only the new `token` field, which should be used instead. The `BackstageIdentity` also received a new `identity` field, which is a decoded version of the information within the token. Furthermore the `BackstageIdentity` has been renamed to `BackstageIdentityResponse`, with the old name being deprecated. + + We expect most of the breaking changes in this update to have low impact since the `IdentityApi` implementation is provided by the app, but it is likely that some tests need to be updated. + + Another breaking change is that the `SignInPage` props have been updated, and the `SignInResult` type is now deprecated. This is unlikely to have any impact on the usage of this package, but it is an important change that you can find more information about in the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/version-bridge@0.1.1 + +## 0.2.2 + +### Patch Changes + +- b291d0ed7e: Tweaked the logged deprecation warning for `createRouteRef` to hopefully make it more clear. +- bacb94ea8f: Documented the options of each of the extension creation functions. +- Updated dependencies + - @backstage/theme@0.2.14 + ## 0.2.1 ### Patch Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 560071e1fa..d0ac0b5352 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -10,6 +10,7 @@ import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; +import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; import { Observer as Observer_2 } from '@backstage/types'; import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; @@ -43,10 +44,7 @@ export type AnalyticsApi = { export const analyticsApiRef: ApiRef; // @public -export const AnalyticsContext: ({ - attributes, - children, -}: { +export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; }) => JSX.Element; @@ -236,18 +234,21 @@ export type AuthRequestOptions = { instantPopup?: boolean; }; -// @public -export type BackstageIdentity = { - id: string; - idToken: string; - token: string; -}; +// @public @deprecated +export type BackstageIdentity = BackstageIdentityResponse; // @public export type BackstageIdentityApi = { getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; +}; + +// @public +export type BackstageIdentityResponse = { + id: string; + token: string; + identity: BackstageUserIdentity; }; // @public @@ -264,6 +265,13 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; + // @public export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -531,8 +539,13 @@ export type IconComponent = ComponentType<{ // @public export type IdentityApi = { getUserId(): string; - getProfile(): ProfileInfo; getIdToken(): Promise; + getProfile(): ProfileInfo; + getProfileInfo(): Promise; + getBackstageIdentity(): Promise; + getCredentials(): Promise<{ + token?: string; + }>; signOut(): Promise; }; @@ -745,10 +758,10 @@ export enum SessionState { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi_2): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo_2; diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index b6567dcb89..72827bde73 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.2.1", + "version": "0.3.0", "private": false, "publishConfig": { "access": "public", @@ -30,22 +30,24 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/theme": "^0.2.13", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.0", + "@backstage/version-bridge": "^0.1.1", "@material-ui/core": "^4.12.2", - "@types/react": "*", "history": "^5.0.0", "prop-types": "^15.7.2", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx index bb2141d47f..075cdff3ff 100644 --- a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx @@ -62,13 +62,12 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { * * @public */ -export const AnalyticsContext = ({ - attributes, - children, -}: { +export const AnalyticsContext = (options: { attributes: Partial; children: ReactNode; }) => { + const { attributes, children } = options; + const parentValues = useAnalyticsContext(); const combinedValue = { ...parentValues, diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 9f68a8b2cc..fe4de89106 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { ApiRef, createApiRef } from '../system'; -import { ProfileInfo } from './auth'; +import { BackstageUserIdentity, ProfileInfo } from './auth'; /** * The Identity API used to identify and get information about the signed in user. @@ -26,25 +26,45 @@ export type IdentityApi = { * The ID of the signed in user. This ID is not meant to be presented to the user, but used * as an opaque string to pass on to backends or use in frontend logic. * - * TODO: The intention of the user ID is to be able to tie the user to an identity - * that is known by the catalog and/or identity backend. It should for example - * be possible to fetch all owned components using this ID. + * @deprecated use {@link IdentityApi.getBackstageIdentity} instead. */ getUserId(): string; - /** - * The profile of the signed in user. - */ - getProfile(): ProfileInfo; - /** * An OpenID Connect ID Token which proves the identity of the signed in user. * * The ID token will be undefined if the signed in user does not have a verified * identity, such as a demo user or mocked user for e2e tests. + * + * @deprecated use {@link IdentityApi.getCredentials} instead. */ getIdToken(): Promise; + /** + * The profile of the signed in user. + * + * @deprecated use {@link IdentityApi.getProfileInfo} instead. + */ + getProfile(): ProfileInfo; + + /** + * The profile of the signed in user. + */ + getProfileInfo(): Promise; + + /** + * User identity information within Backstage. + */ + getBackstageIdentity(): Promise; + + /** + * Provides credentials in the form of a token which proves the identity of the signed in user. + * + * The token will be undefined if the signed in user does not have a verified + * identity, such as a demo user or mocked user for e2e tests. + */ + getCredentials(): Promise<{ token?: string }>; + /** * Sign out the current user */ diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 8fe532f3a6..37308ec29b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -160,31 +160,65 @@ export type BackstageIdentityApi = { */ getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; }; /** - * A (user id, token) pair. + * User identity information within Backstage. * * @public */ -export type BackstageIdentity = { +export type BackstageUserIdentity = { /** - * The backstage user ID. + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. */ - id: string; + type: 'user'; /** - * @deprecated This is deprecated, use `token` instead. + * The entityRef of the user in the catalog. + * For example User:default/sandra */ - idToken: string; + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + +/** + * Token and Identity response, with the users claims in the Identity. + * + * @public + */ +export type BackstageIdentityResponse = { + /** + * The backstage user ID. + * + * @deprecated The identity is now provided via the `identity` field instead. + */ + id: string; /** * The token used to authenticate the user within Backstage. */ token: string; + + /** + * Identity information derived from the token. + */ + identity: BackstageUserIdentity; }; +/** + * The old exported symbol for {@link BackstageIdentityResponse}. + * + * @public + * @deprecated use {@link BackstageIdentityResponse} instead. + */ +export type BackstageIdentity = BackstageIdentityResponse; + /** * Profile information of the user. * diff --git a/packages/core-plugin-api/src/extensions/extensions.tsx b/packages/core-plugin-api/src/extensions/extensions.tsx index ca5ada8478..19da082ee1 100644 --- a/packages/core-plugin-api/src/extensions/extensions.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.tsx @@ -51,8 +51,27 @@ export type ComponentLoader = export function createRoutableExtension< T extends (props: any) => JSX.Element | null, >(options: { + /** + * A loader for the component that is rendered by this extension. + */ component: () => Promise; + + /** + * The mount point to bind this routable extension to. + * + * If this extension is placed somewhere in the app element tree of a Backstage + * app, callers will be able to route to this extensions by calling, + * `useRouteRef` with this mount point. + */ mountPoint: RouteRef; + + /** + * The name of this extension that will represent it at runtime. It is for example + * used to identify this extension in analytics data. + * + * If possible the name should always be the same as the name of the exported + * variable for this extension. + */ name?: string; }): Extension { const { component, mountPoint, name } = options; @@ -127,7 +146,21 @@ export function createRoutableExtension< */ export function createComponentExtension< T extends (props: any) => JSX.Element | null, ->(options: { component: ComponentLoader; name?: string }): Extension { +>(options: { + /** + * A loader or synchronously supplied component that is rendered by this extension. + */ + component: ComponentLoader; + + /** + * The name of this extension that will represent it at runtime. It is for example + * used to identify this extension in analytics data. + * + * If possible the name should always be the same as the name of the exported + * variable for this extension. + */ + name?: string; +}): Extension { const { component, name } = options; return createReactExtension({ component, name }); } @@ -148,8 +181,23 @@ export function createComponentExtension< export function createReactExtension< T extends (props: any) => JSX.Element | null, >(options: { + /** + * A loader or synchronously supplied component that is rendered by this extension. + */ component: ComponentLoader; + + /** + * Additional component data that is attached to the top-level extension component. + */ data?: Record; + + /** + * The name of this extension that will represent it at runtime. It is for example + * used to identify this extension in analytics data. + * + * If possible the name should always be the same as the name of the exported + * variable for this extension. + */ name?: string; }): Extension { const { data = {}, name } = options; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 97a5ef9588..cdcf7f4952 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,119 @@ # @backstage/create-app +## 0.4.7 + +### Patch Changes + +- 9603827bb5: Addressed some peer dependency warnings +- 1bada775a9: TechDocs Backend may now (optionally) leverage a cache store to improve + performance when reading content from a cloud storage provider. + + To apply this change to an existing app, pass the cache manager from the plugin + environment to the `createRouter` function in your backend: + + ```diff + // packages/backend/src/plugins/techdocs.ts + + export default async function createPlugin({ + logger, + config, + discovery, + reader, + + cache, + }: PluginEnvironment): Promise { + + // ... + + return await createRouter({ + preparers, + generators, + publisher, + logger, + config, + discovery, + + cache, + }); + ``` + + If your `PluginEnvironment` does not include a cache manager, be sure you've + applied [the cache management change][cm-change] to your backend as well. + + [Additional configuration][td-rec-arch] is required if you wish to enable + caching in TechDocs. + + [cm-change]: https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#patch-changes-6 + [td-rec-arch]: https://backstage.io/docs/features/techdocs/architecture#recommended-deployment + +- 4862fbc64f: Bump @spotify/prettier-config +- 36bb4fb2e9: Removed the `scaffolder.github.visibility` configuration that is no longer used from the default app template. + +## 0.4.6 + +### Patch Changes + +- 24d2ce03f3: Search Modal now relies on the Search Context to access state and state setter. If you use the SidebarSearchModal as described in the [getting started documentation](https://backstage.io/docs/features/search/getting-started#using-the-search-modal), make sure to update your code with the SearchContextProvider. + + ```diff + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + - + + + + + + + + ... + ``` + +- 905dd952ac: Incorporate usage of the tokenManager into the backend created using `create-app`. + + In existing backends, update the `PluginEnvironment` to include a `tokenManager`: + + ```diff + // packages/backend/src/types.ts + + ... + import { + ... + + TokenManager, + } from '@backstage/backend-common'; + + export type PluginEnvironment = { + ... + + tokenManager: TokenManager; + }; + ``` + + Then, create a `ServerTokenManager`. This can either be a `noop` that requires no secret and validates all requests by default, or one that uses a secret from your `app-config.yaml` to generate and validate tokens. + + ```diff + // packages/backend/src/index.ts + + ... + import { + ... + + ServerTokenManager, + } from '@backstage/backend-common'; + ... + + function makeCreateEnv(config: Config) { + ... + // CHOOSE ONE + // TokenManager not requiring a secret + + const tokenManager = ServerTokenManager.noop(); + // OR TokenManager requiring a secret + + const tokenManager = ServerTokenManager.fromConfig(config); + + ... + return (plugin: string): PluginEnvironment => { + ... + - return { logger, cache, database, config, reader, discovery }; + + return { logger, cache, database, config, reader, discovery, tokenManager }; + }; + } + ``` + ## 0.4.5 ### Patch Changes diff --git a/packages/create-app/README.md b/packages/create-app/README.md index 9f86cdb448..d5b5039d4e 100644 --- a/packages/create-app/README.md +++ b/packages/create-app/README.md @@ -22,4 +22,4 @@ yarn backstage-create-app ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Documentation](https://backstage.io/docs/) diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 49f6e87376..ec739820b9 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.5", + "version": "0.4.7", "private": false, "publishConfig": { "access": "public" @@ -42,6 +42,7 @@ "devDependencies": { "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", + "@types/node": "^14.14.32", "@types/recursive-readdir": "^2.2.0", "mock-fs": "^5.1.1", "ts-node": "^10.0.0" diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d301792380..ac42ed0ff3 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -65,7 +65,7 @@ export default async (cmd: Command, version: string): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - // Use `--path` argument as applicaiton directory when specified, otherwise + // Use `--path` argument as application directory when specified, otherwise // create a directory using `answers.name` const appDir = cmd.path ? resolvePath(paths.targetDir, cmd.path) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index be144d91f7..c804b4b561 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -77,9 +77,7 @@ auth: providers: {} scaffolder: - github: - token: ${GITHUB_TOKEN} - visibility: public # or 'internal' or 'private' + # see https://backstage.io/docs/features/software-templates/configuration for software template options catalog: rules: diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index bdae71d9e3..b0747c4454 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -31,7 +31,7 @@ }, "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", - "@spotify/prettier-config": "^11.0.0", + "@spotify/prettier-config": "^12.0.0", "concurrently": "^6.0.0", "lerna": "^4.0.0", "prettier": "^2.3.2" diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 9e15608a01..68848d2de9 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -22,7 +22,6 @@ "@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}", "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", "@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}", - "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", "@backstage/theme": "^{{version '@backstage/theme'}}", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,6 +33,7 @@ "react-use": "^15.3.3" }, "devDependencies": { + "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", @@ -48,11 +48,11 @@ "scripts": { "start": "backstage-cli app:serve", "build": "backstage-cli app:build", - "test": "backstage-cli test", - "lint": "backstage-cli lint", "clean": "backstage-cli clean", + "test": "backstage-cli test", "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", + "lint": "backstage-cli lint", "cy:dev": "cypress open", "cy:run": "cypress run" }, diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index ec59b0b116..b4fa04f1fc 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -25,7 +25,10 @@ import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearchModal } from '@backstage/plugin-search'; +import { + SidebarSearchModal, + SearchContextProvider, +} from '@backstage/plugin-search'; import { Sidebar, SidebarPage, @@ -74,7 +77,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + + + {/* Global nav, not org-specific */} diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts index 906d86d4a2..054c64db65 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -14,6 +14,7 @@ export default async function createPlugin({ config, discovery, reader, + cache, }: PluginEnvironment): Promise { // Preparers are responsible for fetching source files for documentation. const preparers = await Preparers.fromConfig(config, { @@ -49,5 +50,6 @@ export default async function createPlugin({ logger, config, discovery, + cache, }); } diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index b882155a3d..e15249bf91 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/dev-utils +## 0.2.14 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/core-app-api@0.2.0 + - @backstage/app-defaults@0.1.2 + - @backstage/integration-react@0.1.15 + - @backstage/test-utils@0.1.24 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.13 ### Patch Changes diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md index c10de7d529..e1cb93313f 100644 --- a/packages/dev-utils/api-report.md +++ b/packages/dev-utils/api-report.md @@ -44,11 +44,9 @@ export type DevAppPageOptions = { }; // @public (undocumented) -export const EntityGridItem: ({ - entity, - classes, - ...rest -}: Omit, 'container' | 'item'> & { - entity: Entity; -}) => JSX.Element; +export const EntityGridItem: ( + props: Omit & { + entity: Entity; + }, +) => JSX.Element; ``` diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 32d287d35d..8f77f4a72d 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.13", + "version": "0.2.14", "private": false, "publishConfig": { "access": "public", @@ -29,31 +29,33 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/app-defaults": "^0.1.1", - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/app-defaults": "^0.1.2", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/catalog-model": "^0.9.7", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/test-utils": "^0.1.22", - "@backstage/theme": "^0.2.11", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/test-utils": "^0.1.24", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", - "@types/react": "*", - "react": "^16.12.0", "react-use": "^17.2.4", - "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx index 55327f35e5..a66550a4d2 100644 --- a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx +++ b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx @@ -35,11 +35,10 @@ const useStyles = makeStyles(theme => ({ })); /** @public */ -export const EntityGridItem = ({ - entity, - classes, - ...rest -}: Omit & { entity: Entity }): JSX.Element => { +export const EntityGridItem = ( + props: Omit & { entity: Entity }, +): JSX.Element => { + const { entity, classes, ...rest } = props; const itemClasses = useStyles({ entity }); return ( diff --git a/packages/embedded-techdocs-app/CHANGELOG.md b/packages/embedded-techdocs-app/CHANGELOG.md index 6a9583c2dc..f1ae8abdf5 100644 --- a/packages/embedded-techdocs-app/CHANGELOG.md +++ b/packages/embedded-techdocs-app/CHANGELOG.md @@ -1,5 +1,32 @@ # embedded-techdocs-app +## 0.2.56 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog@0.7.4 + - @backstage/core-app-api@0.2.0 + - @backstage/app-defaults@0.1.2 + - @backstage/integration-react@0.1.15 + - @backstage/test-utils@0.1.24 + - @backstage/plugin-techdocs@0.12.9 + - @backstage/cli@0.10.1 + +## 0.2.55 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/cli@0.10.0 + - @backstage/core-plugin-api@0.2.2 + - @backstage/core-app-api@0.1.24 + - @backstage/plugin-techdocs@0.12.8 + ## 0.2.53 ### Patch Changes diff --git a/packages/embedded-techdocs-app/package.json b/packages/embedded-techdocs-app/package.json index b2d7882f8f..6a13acf37f 100644 --- a/packages/embedded-techdocs-app/package.json +++ b/packages/embedded-techdocs-app/package.json @@ -1,21 +1,21 @@ { "name": "embedded-techdocs-app", - "version": "0.2.53", + "version": "0.2.56", "private": true, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.1", + "@backstage/app-defaults": "^0.1.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@backstage/config": "^0.1.10", - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog": "^0.7.3", - "@backstage/plugin-techdocs": "^0.12.6", - "@backstage/test-utils": "^0.1.22", - "@backstage/theme": "^0.2.11", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/plugin-techdocs": "^0.12.9", + "@backstage/test-utils": "^0.1.24", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", @@ -26,7 +26,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/errors/package.json b/packages/errors/package.json index f49451a6a8..d0d59ffd6a 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -35,7 +35,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index e972b4d4e8..b56d4f52d1 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 0.1.15 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.1.14 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index f3c79cc56c..988fd14032 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.14", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,21 +22,22 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/integration": "^0.6.9", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/integration": "^0.6.10", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.10.1", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index b9c6601e6c..e3380082f7 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/integration +## 0.6.10 + +### Patch Changes + +- 47619da24c: Narrow the types returned by the request option functions, to only the specifics that they actually do return. The reason for this change is that a full `RequestInit` is unfortunate to return because it's different between `cross-fetch` and `node-fetch`. + ## 0.6.9 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 4ccde52a70..a6d91511c3 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.6.9", + "version": "0.6.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,8 +39,8 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/config-loader": "^0.8.0", + "@backstage/cli": "^0.10.0", + "@backstage/config-loader": "^0.8.1", "@backstage/test-utils": "^0.1.22", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", diff --git a/packages/search-common/package.json b/packages/search-common/package.json index d65e9bcbdc..51cea5a28e 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -39,7 +39,7 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.0" }, "jest": { "roots": [ diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 2750324c17..4c0813a8e9 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -10,6 +10,7 @@ import { OktaAuth, Auth0Auth, ConfigReader, + LocalStorageFeatureFlags, } from '@backstage/core-app-api'; import { @@ -24,9 +25,11 @@ import { oktaAuthApiRef, auth0AuthApiRef, configApiRef, + featureFlagsApiRef, } from '@backstage/core-plugin-api'; const configApi = new ConfigReader({}); +const featureFlagsApi = new LocalStorageFeatureFlags(); const alertApi = new AlertApiForwarder(); const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); const identityApi = { @@ -69,6 +72,7 @@ const oauth2Api = OAuth2.create({ export const apis = [ [configApiRef, configApi], + [featureFlagsApiRef, featureFlagsApi], [alertApiRef, alertApi], [errorApiRef, errorApi], [identityApiRef, identityApi], diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 736afbb8d8..5df0c06632 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -9,8 +9,8 @@ }, "dependencies": { "@backstage/theme": "^0.2.0", - "react": "^16.12.0", - "react-dom": "^16.12.0" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "devDependencies": { "@storybook/addon-a11y": "^6.3.4", diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 6d69e452ad..cf94063b89 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,21 @@ # @techdocs/cli +## 0.8.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.13 + - @backstage/techdocs-common@0.11.0 + +## 0.8.7 + +### Patch Changes + +- e7230ef814: Bump react-dev-utils to v12 +- Updated dependencies + - @backstage/backend-common@0.9.12 + ## 0.8.6 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 05759e39d1..292db9820f 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "0.8.6", + "version": "0.8.8", "private": false, "publishConfig": { "access": "public" @@ -32,7 +32,7 @@ "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -41,7 +41,7 @@ "@types/react-dev-utils": "^9.0.4", "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", - "embedded-techdocs-app": "0.2.53", + "embedded-techdocs-app": "0.2.56", "find-process": "^1.4.5", "nodemon": "^2.0.2", "ts-node": "^10.0.0" @@ -56,10 +56,10 @@ "ext": "ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/techdocs-common": "^0.10.7", + "@backstage/techdocs-common": "^0.11.0", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 386c584196..e925ad35e9 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/techdocs-common +## 0.11.0 + +### Minor Changes + +- 1bada775a9: Added the ability for the TechDocs Backend to (optionally) leverage a cache + store to improve performance when reading files from a cloud storage provider. + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + ## 0.10.8 ### Patch Changes diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 61371525e5..5c0a9f87f6 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -49,22 +49,6 @@ export type GeneratorBuilder = { get(entity: Entity): GeneratorBase; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (ae-missing-release-tag) "GeneratorRunOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GeneratorRunOptions = { inputDir: string; @@ -82,10 +66,7 @@ export class Generators implements GeneratorBuilder { // (undocumented) static fromConfig( config: Config, - { - logger, - containerRunner, - }: { + options: { logger: Logger_2; containerRunner: ContainerRunner; }, @@ -185,13 +166,10 @@ export class Publisher { ): Promise; } -// Warning: (ae-missing-release-tag) "PublisherBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface PublisherBase { docsRouter(): express.Handler; fetchTechDocsMetadata(entityName: EntityName): Promise; - // Warning: (ae-forgotten-export) The symbol "ReadinessResponse" needs to be exported by the entry point index.d.ts getReadiness(): Promise; hasDocsBeenGenerated(entityName: Entity): Promise; // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag @@ -202,7 +180,6 @@ export interface PublisherBase { // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // Warning: (ae-forgotten-export) The symbol "MigrateRequest" needs to be exported by the entry point index.d.ts migrateDocsCase?(migrateRequest: MigrateRequest): Promise; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-forgotten-export) The symbol "PublishRequest" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublishResponse" needs to be exported by the entry point index.d.ts publish(request: PublishRequest): Promise; @@ -218,6 +195,11 @@ export type PublisherType = | 'azureBlobStorage' | 'openStackSwift'; +// @public +export type ReadinessResponse = { + isAvailable: boolean; +}; + // Warning: (ae-missing-release-tag) "RemoteProtocol" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -245,12 +227,7 @@ export interface TechDocsDocument extends IndexableDocument { // // @public (undocumented) export class TechdocsGenerator implements GeneratorBase { - constructor({ - logger, - containerRunner, - config, - scmIntegrations, - }: { + constructor(options: { logger: Logger_2; containerRunner: ContainerRunner; config: Config; @@ -260,26 +237,15 @@ export class TechdocsGenerator implements GeneratorBase { // (undocumented) static fromConfig( config: Config, - { - containerRunner, - logger, - }: { + options: { containerRunner: ContainerRunner; logger: Logger_2; }, ): TechdocsGenerator; // (undocumented) - run({ - inputDir, - outputDir, - parsedLocationAnnotation, - etag, - logger: childLogger, - logStream, - }: GeneratorRunOptions): Promise; + run(options: GeneratorRunOptions): Promise; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "TechDocsMetadata" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -287,6 +253,8 @@ export type TechDocsMetadata = { site_name: string; site_description: string; etag: string; + build_timestamp: number; + files?: string[]; }; // Warning: (ae-missing-release-tag) "transformDirLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -317,7 +285,7 @@ export class UrlPreparer implements PreparerBase { // Warnings were encountered during analysis: // -// src/stages/generate/types.d.ts:44:5 - (ae-forgotten-export) The symbol "SupportedGeneratorKey" needs to be exported by the entry point index.d.ts +// src/stages/generate/types.d.ts:45:5 - (ae-forgotten-export) The symbol "SupportedGeneratorKey" needs to be exported by the entry point index.d.ts // src/stages/prepare/types.d.ts:18:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/stages/prepare/types.d.ts:19:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters // src/stages/prepare/types.d.ts:21:33 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 3f0d0edf5b..c9c67881f7 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.10.8", + "version": "0.11.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,12 +38,12 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/search-common": "^0.2.1", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.5", "@types/express": "^4.17.6", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index 3d1ad2b73b..de39d4e7c3 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -31,17 +31,11 @@ export class Generators implements GeneratorBuilder { static async fromConfig( config: Config, - { - logger, - containerRunner, - }: { logger: Logger; containerRunner: ContainerRunner }, + options: { logger: Logger; containerRunner: ContainerRunner }, ): Promise { const generators = new Generators(); - const techdocsGenerator = TechdocsGenerator.fromConfig(config, { - logger, - containerRunner, - }); + const techdocsGenerator = TechdocsGenerator.fromConfig(config, options); generators.register('techdocs', techdocsGenerator); return generators; diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 9e01c3e2ad..9323ab4f18 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -23,7 +23,7 @@ import os from 'os'; import path, { resolve as resolvePath } from 'path'; import { ParsedLocationAnnotation } from '../../helpers'; import { - addBuildTimestampMetadata, + createOrUpdateMetadata, getGeneratorKey, getMkdocsYml, getRepoUrlFromLocationAnnotation, @@ -369,13 +369,15 @@ describe('helpers', () => { }); describe('addBuildTimestampMetadata', () => { + const mockFiles = { + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', + }; + beforeEach(() => { mockFs.restore(); mockFs({ - [rootDir]: { - 'invalid_techdocs_metadata.json': 'dsds', - 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', - }, + [rootDir]: mockFiles, }); }); @@ -385,7 +387,7 @@ describe('helpers', () => { it('should create the file if it does not exist', async () => { const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json'); - await addBuildTimestampMetadata(filePath, mockLogger); + await createOrUpdateMetadata(filePath, mockLogger); // Check if the file exists await expect( @@ -397,18 +399,28 @@ describe('helpers', () => { const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); await expect( - addBuildTimestampMetadata(filePath, mockLogger), + createOrUpdateMetadata(filePath, mockLogger), ).rejects.toThrowError('Unexpected token d in JSON at position 0'); }); it('should add build timestamp to the metadata json', async () => { const filePath = path.join(rootDir, 'techdocs_metadata.json'); - await addBuildTimestampMetadata(filePath, mockLogger); + await createOrUpdateMetadata(filePath, mockLogger); const json = await fs.readJson(filePath); expect(json.build_timestamp).toBeLessThanOrEqual(Date.now()); }); + + it('should add list of files to the metadata json', async () => { + const filePath = path.join(rootDir, 'techdocs_metadata.json'); + + await createOrUpdateMetadata(filePath, mockLogger); + + const json = await fs.readJson(filePath); + expect(json.files[0]).toEqual(Object.keys(mockFiles)[0]); + expect(json.files[1]).toEqual(Object.keys(mockFiles)[1]); + }); }); describe('storeEtagMetadata', () => { diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index f77b557076..7c46f4e966 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -18,7 +18,7 @@ import { isChildPath } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { assertError, ForwardedError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { spawn } from 'child_process'; +import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; import fs from 'fs-extra'; import gitUrlParse from 'git-url-parse'; import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml'; @@ -27,6 +27,7 @@ import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; import { SupportedGeneratorKey } from './types'; +import { getFileTreeRecursively } from '../publish/helpers'; // TODO: Implement proper support for more generators. export function getGeneratorKey(entity: Entity): SupportedGeneratorKey { @@ -38,19 +39,18 @@ export function getGeneratorKey(entity: Entity): SupportedGeneratorKey { } export type RunCommandOptions = { + /** command to run */ command: string; + /** arguments to pass the command */ args: string[]; - options: object; + /** options to pass to spawn */ + options: SpawnOptionsWithoutStdio; + /** stream to capture stdout and stderr output */ logStream?: Writable; }; /** - * - * @param options the options object - * @param options.command the command to run - * @param options.args the arguments to pass the command - * @param options.options options used in spawn - * @param options.logStream the log streamer to capture log messages + * Run a command in a sub-process, normally a shell command. */ export const runCommand = async ({ command, @@ -345,14 +345,20 @@ export const patchIndexPreBuild = async ({ }; /** - * Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist. + * Create or update the techdocs_metadata.json. Values initialized/updated are: + * - The build_timestamp (now) + * - The list of files generated * * @param {string} techdocsMetadataPath File path to techdocs_metadata.json */ -export const addBuildTimestampMetadata = async ( +export const createOrUpdateMetadata = async ( techdocsMetadataPath: string, logger: Logger, ): Promise => { + const techdocsMetadataDir = techdocsMetadataPath + .split(path.sep) + .slice(0, -1) + .join(path.sep); // check if file exists, create if it does not. try { await fs.access(techdocsMetadataPath, fs.constants.F_OK); @@ -372,6 +378,19 @@ export const addBuildTimestampMetadata = async ( } json.build_timestamp = Date.now(); + + // Get and write generated files to the metadata JSON. Each file string is in + // a form appropriate for invalidating the associated object from cache. + try { + json.files = (await getFileTreeRecursively(techdocsMetadataDir)).map(file => + file.replace(`${techdocsMetadataDir}${path.sep}`, ''), + ); + } catch (err) { + assertError(err); + json.files = []; + logger.warn(`Unable to add files list to metadata: ${err.message}`); + } + await fs.writeJson(techdocsMetadataPath, json); return; }; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 728cab5a81..a6eb8d38d2 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -23,7 +23,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { - addBuildTimestampMetadata, + createOrUpdateMetadata, getMkdocsYml, patchIndexPreBuild, patchMkdocsYmlPreBuild, @@ -52,11 +52,9 @@ export class TechdocsGenerator implements GeneratorBase { static fromConfig( config: Config, - { - containerRunner, - logger, - }: { containerRunner: ContainerRunner; logger: Logger }, + options: { containerRunner: ContainerRunner; logger: Logger }, ) { + const { containerRunner, logger } = options; const scmIntegrations = ScmIntegrations.fromConfig(config); return new TechdocsGenerator({ logger, @@ -66,31 +64,28 @@ export class TechdocsGenerator implements GeneratorBase { }); } - constructor({ - logger, - containerRunner, - config, - scmIntegrations, - }: { + constructor(options: { logger: Logger; containerRunner: ContainerRunner; config: Config; scmIntegrations: ScmIntegrationRegistry; }) { - this.logger = logger; - this.options = readGeneratorConfig(config, logger); - this.containerRunner = containerRunner; - this.scmIntegrations = scmIntegrations; + this.logger = options.logger; + this.options = readGeneratorConfig(options.config, options.logger); + this.containerRunner = options.containerRunner; + this.scmIntegrations = options.scmIntegrations; } - public async run({ - inputDir, - outputDir, - parsedLocationAnnotation, - etag, - logger: childLogger, - logStream, - }: GeneratorRunOptions): Promise { + public async run(options: GeneratorRunOptions): Promise { + const { + inputDir, + outputDir, + parsedLocationAnnotation, + etag, + logger: childLogger, + logStream, + } = options; + // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); @@ -164,9 +159,9 @@ export class TechdocsGenerator implements GeneratorBase { * Post Generate steps */ - // Add build timestamp to techdocs_metadata.json + // Add build timestamp and files to techdocs_metadata.json // Creates techdocs_metadata.json if file does not exist. - await addBuildTimestampMetadata( + await createOrUpdateMetadata( path.join(outputDir, 'techdocs_metadata.json'), childLogger, ); diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 3559716be0..2a090d5584 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -34,12 +34,13 @@ export type GeneratorConfig = { /** * The values that the generator will receive. * - * @param {string} inputDir The directory of the uncompiled documentation, with the values from the frontend - * @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory. - * @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity - * @param {string} etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. - * @param {Logger} [logger] A logger that forwards the messages to the caller to be displayed outside of the backend. - * @param {Writable} [logStream] A log stream that can send raw log messages to the caller to be displayed outside of the backend.. + * @public + * @param inputDir - The directory of the uncompiled documentation, with the values from the frontend + * @param outputDir - Directory to store generated docs in. Usually - a newly created temporary directory. + * @param parsedLocationAnnotation - backstage.io/techdocs-ref annotation of an entity + * @param etag - A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + * @param logger - A logger that forwards the messages to the caller to be displayed outside of the backend. + * @param logStream - A log stream that can send raw log messages to the caller to be displayed outside of the backend. */ export type GeneratorRunOptions = { inputDir: string; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 1f49668d35..3e4c4c705b 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -95,6 +95,7 @@ describe('AwsS3Publish', () => { site_name: 'backstage', site_description: 'site_content', etag: 'etag', + build_timestamp: 612741599, }; const directory = getEntityRootDir(entity); @@ -149,21 +150,39 @@ describe('AwsS3Publish', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/component/backstage/404.html', + `default/component/backstage/index.html`, + `default/component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/Component/backstage/404.html', + `default/Component/backstage/index.html`, + `default/Component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory when root path is specified', async () => { const publisher = createPublisherFromConfig({ bucketRootPath: 'backstage-data/techdocs', }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'backstage-data/techdocs/default/component/backstage/404.html', + `backstage-data/techdocs/default/component/backstage/index.html`, + `backstage-data/techdocs/default/component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory when root path is specified and legacy casing is used', async () => { @@ -171,14 +190,26 @@ describe('AwsS3Publish', () => { bucketRootPath: 'backstage-data/techdocs', legacyUseCaseSensitiveTripletPaths: true, }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'backstage-data/techdocs/default/Component/backstage/404.html', + `backstage-data/techdocs/default/Component/backstage/index.html`, + `backstage-data/techdocs/default/Component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory when sse is specified', async () => { const publisher = createPublisherFromConfig({ sse: 'aws:kms', }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/component/backstage/404.html', + 'default/component/backstage/index.html', + 'default/component/backstage/assets/main.css', + ]), + }); }); it('should fail to publish a directory', async () => { diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index abfa8ddd24..ed76edc0cb 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -39,6 +39,7 @@ import { import { PublisherBase, PublishRequest, + PublishResponse, ReadinessResponse, TechDocsMetadata, } from './types'; @@ -214,7 +215,11 @@ export class AwsS3Publish implements PublisherBase { * Upload all the files from the generated `directory` to the S3 bucket. * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ - async publish({ entity, directory }: PublishRequest): Promise { + async publish({ + entity, + directory, + }: PublishRequest): Promise { + const objects: string[] = []; const useLegacyPathCasing = this.legacyPathCasing; const bucketRootPath = this.bucketRootPath; const sse = this.sse; @@ -263,6 +268,7 @@ export class AwsS3Publish implements PublisherBase { ...(sse && { ServerSideEncryption: sse }), } as aws.S3.PutObjectRequest; + objects.push(params.Key); return this.storageClient.upload(params).promise(); }, absoluteFilesToUpload, @@ -311,6 +317,7 @@ export class AwsS3Publish implements PublisherBase { const errorMessage = `Unable to delete file(s) from AWS S3. ${error}`; this.logger.error(errorMessage); } + return { objects }; } async fetchTechDocsMetadata( diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index a2503bd7b2..e6fd45eed7 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -89,6 +89,7 @@ describe('AzureBlobStoragePublish', () => { site_name: 'backstage', site_description: 'site_content', etag: 'etag', + build_timestamp: 612741599, }; const directory = getEntityRootDir(entity); @@ -154,14 +155,26 @@ describe('AzureBlobStoragePublish', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/component/backstage/404.html', + `default/component/backstage/index.html`, + `default/component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/Component/backstage/404.html', + `default/Component/backstage/index.html`, + `default/Component/backstage/assets/main.css`, + ]), + }); }); it('should fail to publish a directory', async () => { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 7a58d92095..b082079be2 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -39,6 +39,7 @@ import { import { PublisherBase, PublishRequest, + PublishResponse, ReadinessResponse, TechDocsMetadata, } from './types'; @@ -156,7 +157,11 @@ export class AzureBlobStoragePublish implements PublisherBase { * Upload all the files from the generated `directory` to the Azure Blob Storage container. * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html */ - async publish({ entity, directory }: PublishRequest): Promise { + async publish({ + entity, + directory, + }: PublishRequest): Promise { + const objects: string[] = []; const useLegacyPathCasing = this.legacyPathCasing; // First, try to retrieve a list of all individual files currently existing @@ -194,14 +199,14 @@ export class AzureBlobStoragePublish implements PublisherBase { const relativeFilePath = path.normalize( path.relative(directory, absoluteFilePath), ); + const remotePath = getCloudPathForLocalPath( + entity, + relativeFilePath, + useLegacyPathCasing, + ); + objects.push(remotePath); const response = await container - .getBlockBlobClient( - getCloudPathForLocalPath( - entity, - relativeFilePath, - useLegacyPathCasing, - ), - ) + .getBlockBlobClient(remotePath) .uploadFile(absoluteFilePath); if (response._response.status >= 400) { @@ -264,6 +269,8 @@ export class AzureBlobStoragePublish implements PublisherBase { const errorMessage = `Unable to delete file(s) from Azure. ${error}`; this.logger.error(errorMessage); } + + return { objects }; } private download(containerName: string, blobPath: string): Promise { diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index a52fd0f0bd..ed35f2e445 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -88,6 +88,7 @@ describe('GoogleGCSPublish', () => { site_name: 'backstage', site_description: 'site_content', etag: 'etag', + build_timestamp: 612741599, }; const directory = getEntityRootDir(entity); @@ -142,21 +143,39 @@ describe('GoogleGCSPublish', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/component/backstage/404.html', + `default/component/backstage/index.html`, + `default/component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'default/Component/backstage/404.html', + `default/Component/backstage/index.html`, + `default/Component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory when root path is specified', async () => { const publisher = createPublisherFromConfig({ bucketRootPath: 'backstage-data/techdocs', }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'backstage-data/techdocs/default/component/backstage/404.html', + `backstage-data/techdocs/default/component/backstage/index.html`, + `backstage-data/techdocs/default/component/backstage/assets/main.css`, + ]), + }); }); it('should publish a directory when root path is specified and legacy casing is used', async () => { @@ -164,7 +183,13 @@ describe('GoogleGCSPublish', () => { bucketRootPath: 'backstage-data/techdocs', legacyUseCaseSensitiveTripletPaths: true, }); - expect(await publisher.publish({ entity, directory })).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toMatchObject({ + objects: expect.arrayContaining([ + 'backstage-data/techdocs/default/Component/backstage/404.html', + `backstage-data/techdocs/default/Component/backstage/index.html`, + `backstage-data/techdocs/default/Component/backstage/assets/main.css`, + ]), + }); }); it('should fail to publish a directory', async () => { diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index f072412c20..ba70c36e27 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -36,6 +36,7 @@ import { MigrateWriteStream } from './migrations'; import { PublisherBase, PublishRequest, + PublishResponse, ReadinessResponse, TechDocsMetadata, } from './types'; @@ -145,7 +146,11 @@ export class GoogleGCSPublish implements PublisherBase { * Upload all the files from the generated `directory` to the GCS bucket. * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ - async publish({ entity, directory }: PublishRequest): Promise { + async publish({ + entity, + directory, + }: PublishRequest): Promise { + const objects: string[] = []; const useLegacyPathCasing = this.legacyPathCasing; const bucket = this.storageClient.bucket(this.bucketName); const bucketRootPath = this.bucketRootPath; @@ -178,14 +183,14 @@ export class GoogleGCSPublish implements PublisherBase { await bulkStorageOperation( async absoluteFilePath => { const relativeFilePath = path.relative(directory, absoluteFilePath); - return await bucket.upload(absoluteFilePath, { - destination: getCloudPathForLocalPath( - entity, - relativeFilePath, - useLegacyPathCasing, - bucketRootPath, - ), - }); + const destination = getCloudPathForLocalPath( + entity, + relativeFilePath, + useLegacyPathCasing, + bucketRootPath, + ); + objects.push(destination); + return await bucket.upload(absoluteFilePath, { destination }); }, absoluteFilesToUpload, { concurrencyLimit: 10 }, @@ -228,6 +233,8 @@ export class GoogleGCSPublish implements PublisherBase { const errorMessage = `Unable to delete file(s) from Google Cloud Storage. ${error}`; this.logger.error(errorMessage); } + + return { objects }; } fetchTechDocsMetadata(entityName: EntityName): Promise { diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/packages/techdocs-common/src/stages/publish/index.ts index b342f33287..083cf2b8ff 100644 --- a/packages/techdocs-common/src/stages/publish/index.ts +++ b/packages/techdocs-common/src/stages/publish/index.ts @@ -14,4 +14,9 @@ * limitations under the License. */ export { Publisher } from './publish'; -export type { PublisherBase, PublisherType, TechDocsMetadata } from './types'; +export type { + PublisherBase, + PublisherType, + TechDocsMetadata, + ReadinessResponse, +} from './types'; diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index 70b4eb3ff2..9c146d2935 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -98,7 +98,10 @@ export class LocalPublish implements PublisherBase { }; } - publish({ entity, directory }: PublishRequest): Promise { + async publish({ + entity, + directory, + }: PublishRequest): Promise { const entityNamespace = entity.metadata.namespace ?? 'default'; const publishDir = this.staticEntityPathJoin( @@ -112,27 +115,30 @@ export class LocalPublish implements PublisherBase { fs.mkdirSync(publishDir, { recursive: true }); } - return new Promise((resolve, reject) => { - fs.copy(directory, publishDir, err => { - if (err) { - this.logger.debug( - `Failed to copy docs from ${directory} to ${publishDir}`, - ); - reject(err); - } - this.logger.info(`Published site stored at ${publishDir}`); - this.discovery - .getBaseUrl('techdocs') - .then(techdocsApiUrl => { - resolve({ - remoteUrl: `${techdocsApiUrl}/static/docs/${entity.metadata.name}`, - }); - }) - .catch(reason => { - reject(reason); - }); - }); - }); + try { + await fs.copy(directory, publishDir); + this.logger.info(`Published site stored at ${publishDir}`); + } catch (error) { + this.logger.debug( + `Failed to copy docs from ${directory} to ${publishDir}`, + ); + throw error; + } + + // Generate publish response. + const techdocsApiUrl = await this.discovery.getBaseUrl('techdocs'); + const publishedFilePaths = (await getFileTreeRecursively(publishDir)).map( + abs => { + return abs.split(`${staticDocsDir}/`)[1]; + }, + ); + + return { + remoteUrl: `${techdocsApiUrl}/static/docs/${encodeURIComponent( + entity.metadata.name, + )}`, + objects: publishedFilePaths, + }; } async fetchTechDocsMetadata( diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts index c2236fb880..ef06e26cfa 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -170,7 +170,13 @@ describe('OpenStackSwiftPublish', () => { entity, directory: entityRootDir, }), - ).toBeUndefined(); + ).toMatchObject({ + objects: expect.arrayContaining([ + 'test-namespace/TestKind/test-component-name/404.html', + `test-namespace/TestKind/test-component-name/index.html`, + `test-namespace/TestKind/test-component-name/assets/main.css`, + ]), + }); }); it('should fail to publish a directory', async () => { @@ -241,7 +247,7 @@ describe('OpenStackSwiftPublish', () => { mockFs({ [entityRootDir]: { 'techdocs_metadata.json': - '{"site_name": "backstage", "site_description": "site_content", "etag": "etag"}', + '{"site_name": "backstage", "site_description": "site_content", "etag": "etag", "build_timestamp": 612741599}', }, }); @@ -249,6 +255,7 @@ describe('OpenStackSwiftPublish', () => { site_name: 'backstage', site_description: 'site_content', etag: 'etag', + build_timestamp: 612741599, }; expect( await publisher.fetchTechDocsMetadata(entityNameMock), @@ -263,7 +270,7 @@ describe('OpenStackSwiftPublish', () => { mockFs({ [entityRootDir]: { - 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}`, + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag', 'build_timestamp': 612741599}`, }, }); @@ -271,6 +278,7 @@ describe('OpenStackSwiftPublish', () => { site_name: 'backstage', site_description: 'site_content', etag: 'etag', + build_timestamp: 612741599, }; expect( await publisher.fetchTechDocsMetadata(entityNameMock), diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 7b623933e3..62b40f9a76 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -32,6 +32,7 @@ import { import { PublisherBase, PublishRequest, + PublishResponse, ReadinessResponse, TechDocsMetadata, } from './types'; @@ -139,8 +140,13 @@ export class OpenStackSwiftPublish implements PublisherBase { * Upload all the files from the generated `directory` to the OpenStack Swift container. * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ - async publish({ entity, directory }: PublishRequest): Promise { + async publish({ + entity, + directory, + }: PublishRequest): Promise { try { + const objects: string[] = []; + // Note: OpenStack Swift manages creation of parent directories if they do not exist. // So collecting path of only the files is good enough. const allFilesToUpload = await getFileTreeRecursively(directory); @@ -161,6 +167,7 @@ export class OpenStackSwiftPublish implements PublisherBase { // The / delimiter is intentional since it represents the cloud storage and not the local file system. const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; const destination = `${entityRootDir}/${relativeFilePathPosix}`; // Swift container file relative path + objects.push(destination); // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) const uploadFile = limiter(async () => { @@ -178,7 +185,7 @@ export class OpenStackSwiftPublish implements PublisherBase { this.logger.info( `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, ); - return; + return { objects }; } catch (e) { const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${e}`; this.logger.error(errorMessage); diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 229c853427..b7415722d6 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -32,13 +32,27 @@ export type PublishRequest = { directory: string; }; -/* `remoteUrl` is the URL which serves files from the local publisher's static directory. */ +/** + * Response containing metadata about where files were published and what may + * have been published or updated. + */ export type PublishResponse = { + /** + * The URL which serves files from the local publisher's static directory. + */ remoteUrl?: string; + /** + * The list of objects (specifically their paths) that were published. + * Objects do not have a preceding slash, and match how one would load the + * object over the `/static/docs/*` TechDocs Backend Plugin endpoint. + */ + objects?: string[]; } | void; /** * Result for the validation check. + * + * @public */ export type ReadinessResponse = { /** If true, the publisher is able to interact with the backing storage. */ @@ -47,12 +61,14 @@ export type ReadinessResponse = { /** * Type to hold metadata found in techdocs_metadata.json and associated with each site - * @param etag ETag of the resource used to generate the site. Usually the latest commit sha of the source repository. + * @param etag - ETag of the resource used to generate the site. Usually the latest commit sha of the source repository. */ export type TechDocsMetadata = { site_name: string; site_description: string; etag: string; + build_timestamp: number; + files?: string[]; }; export type MigrateRequest = { @@ -72,6 +88,8 @@ export type MigrateRequest = { * Base class for a TechDocs publisher (e.g. Local, Google GCS Bucket, AWS S3, etc.) * The publisher handles publishing of the generated static files after the prepare and generate steps of TechDocs. * It also provides APIs to communicate with the storage service. + * + * @public */ export interface PublisherBase { /** @@ -85,8 +103,8 @@ export interface PublisherBase { /** * Store the generated static files onto a storage service (either local filesystem or external service). * - * @param request Object containing the entity from the service - * catalog, and the directory that contains the generated static files from TechDocs. + * @param request - Object containing the entity from the service + * catalog, and the directory that contains the generated static files from TechDocs. */ publish(request: PublishRequest): Promise; diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index fd8ce852db..4da8ef195f 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/test-utils +## 0.1.24 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-plugin-api@0.3.0 + - @backstage/core-app-api@0.2.0 + ## 0.1.23 ### Patch Changes diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 83c209122d..a01f306fab 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -89,23 +89,13 @@ export type LogFuncs = 'log' | 'warn' | 'error'; // @public export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) - captureEvent({ - action, - subject, - value, - attributes, - context, - }: AnalyticsEvent): void; + captureEvent(event: AnalyticsEvent): void; // (undocumented) getEvents(): AnalyticsEvent[]; } // @public -export function mockBreakpoint({ - matches, -}: { - matches?: boolean | undefined; -}): void; +export function mockBreakpoint(options: { matches: boolean }): void; // @public export class MockErrorApi implements ErrorApi { @@ -178,10 +168,9 @@ export function setupRequestMockHandlers(worker: { export type SyncLogCollector = () => void; // @public -export const TestApiProvider: ({ - apis, - children, -}: TestApiProviderProps) => JSX.Element; +export const TestApiProvider: ( + props: TestApiProviderProps, +) => JSX.Element; // @public export type TestApiProviderProps = { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 7e684adbfc..a8519160df 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.23", + "version": "0.1.24", "private": false, "publishConfig": { "access": "public", @@ -29,24 +29,25 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.23", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", - "@types/react": "*", - "react": "^16.12.0", - "react-dom": "^16.12.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/test-utils/src/testUtils/TestApiProvider.tsx b/packages/test-utils/src/testUtils/TestApiProvider.tsx index b1499b0cde..6e0be81d53 100644 --- a/packages/test-utils/src/testUtils/TestApiProvider.tsx +++ b/packages/test-utils/src/testUtils/TestApiProvider.tsx @@ -120,11 +120,13 @@ export class TestApiRegistry implements ApiHolder { * * @public **/ -export const TestApiProvider = ({ - apis, - children, -}: TestApiProviderProps) => { +export const TestApiProvider = ( + props: TestApiProviderProps, +) => { return ( - + ); }; diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 3e2fc2a01c..6da225df1c 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -19,18 +19,15 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; /** * Mock implementation of {@link core-plugin-api#AnalyticsApi} with helpers to ensure that events are sent correctly. * Use getEvents in tests to verify captured events. + * * @public */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; - captureEvent({ - action, - subject, - value, - attributes, - context, - }: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent) { + const { action, subject, value, attributes, context } = event; + this.events.push({ action, subject, diff --git a/packages/test-utils/src/testUtils/mockBreakpoint.ts b/packages/test-utils/src/testUtils/mockBreakpoint.ts index 3bc285a81d..ee759c07a5 100644 --- a/packages/test-utils/src/testUtils/mockBreakpoint.ts +++ b/packages/test-utils/src/testUtils/mockBreakpoint.ts @@ -15,8 +15,8 @@ */ /** - * This is a mocking method suggested in the Jest Doc's, as it is not implemented in JSDOM yet. - * It can be used to mock values when the MUI `useMediaQuery` hook if it is used in a tested component. + * This is a mocking method suggested in the Jest docs, as it is not implemented in JSDOM yet. + * It can be used to mock values for the MUI `useMediaQuery` hook if it is used in a tested component. * * For issues checkout the documentation: * https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom @@ -26,11 +26,11 @@ * * @public */ -export default function mockBreakpoint({ matches = false }) { +export default function mockBreakpoint(options: { matches: boolean }) { Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ - matches: matches, + matches: options.matches ?? false, media: query, onchange: null, addListener: jest.fn(), // deprecated diff --git a/packages/test-utils/src/testUtils/testingLibrary.ts b/packages/test-utils/src/testUtils/testingLibrary.ts index 44ea20a4cc..9fb47cbc90 100644 --- a/packages/test-utils/src/testUtils/testingLibrary.ts +++ b/packages/test-utils/src/testUtils/testingLibrary.ts @@ -15,8 +15,7 @@ */ import { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import { render, RenderResult } from '@testing-library/react'; +import { act, render, RenderResult } from '@testing-library/react'; /** * @public diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 1c8661d7cc..cfa6ee3633 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/theme +## 0.2.14 + +### Patch Changes + +- e34f174fc5: Added `` and `` to enable building better sidebars. You can check out the storybook for more inspiration and how to get started. + + Added two new theme props for styling the sidebar too, `navItem.hoverBackground` and `submenu.background`. + +- 59677fadb1: Improvements to API Reference documentation + ## 0.2.13 ### Patch Changes diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 2dc6ab7b55..f650636710 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -41,6 +41,12 @@ export type BackstagePaletteAdditions = { indicator: string; color: string; selectedColor: string; + navItem?: { + hoverBackground: string; + }; + submenu?: { + background: string; + }; }; tabbar: { indicator: string; diff --git a/packages/theme/package.json b/packages/theme/package.json index 28a05af98d..08477dccd0 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.13", + "version": "0.2.14", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.0" }, "files": [ "dist" diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 93fec977f9..41f827bf43 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -15,7 +15,7 @@ */ /** - * material-ui theme for use with Backstage. + * {@link https://mui.com | material-ui} theme for use with Backstage * * @packageDocumentation */ diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 7e99ce8e8a..b048c70fec 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -76,6 +76,12 @@ export const lightTheme = createTheme({ indicator: '#9BF0E1', color: '#b5b5b5', selectedColor: '#FFF', + navItem: { + hoverBackground: '#404040', + }, + submenu: { + background: '#404040', + }, }, pinSidebarButton: { icon: '#181818', @@ -151,6 +157,12 @@ export const darkTheme = createTheme({ indicator: '#9BF0E1', color: '#b5b5b5', selectedColor: '#FFF', + navItem: { + hoverBackground: '#404040', + }, + submenu: { + background: '#404040', + }, }, pinSidebarButton: { icon: '#404040', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index e1acba3a01..3d1a64c38f 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -53,6 +53,12 @@ export type BackstagePaletteAdditions = { indicator: string; color: string; selectedColor: string; + navItem?: { + hoverBackground: string; + }; + submenu?: { + background: string; + }; }; tabbar: { indicator: string; diff --git a/packages/types/package.json b/packages/types/package.json index 197d51b061..822b2d678e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -31,7 +31,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/CHANGELOG.md b/packages/version-bridge/CHANGELOG.md new file mode 100644 index 0000000000..e079cd6c61 --- /dev/null +++ b/packages/version-bridge/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/version-bridge + +## 0.1.1 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 35a6e8f71d..0291cdc208 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/version-bridge", "description": "Utilities used by @backstage packages to support multiple concurrent versions", - "version": "0.1.0", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -28,12 +28,12 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, - "dependencies": { - "@types/react": "*", - "react": "^16.12.0" + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2" diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index f769b6e133..6b8336b251 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-allure +## 0.1.8 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.7 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 33fc4db9d9..8b3ee3b882 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,23 +23,24 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index acc5c235f9..77a6089118 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-analytics-module-ga +## 0.1.4 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- 704235786a: Support self hosted analytics.js script via `scriptSrc` config option +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md index 7ca3b1646f..b0f3b736c3 100644 --- a/plugins/analytics-module-ga/api-report.md +++ b/plugins/analytics-module-ga/api-report.md @@ -17,13 +17,7 @@ export const analyticsModuleGA: BackstagePlugin<{}, {}>; // // @public export class GoogleAnalytics implements AnalyticsApi { - captureEvent({ - context, - action, - subject, - value, - attributes, - }: AnalyticsEvent): void; + captureEvent(event: AnalyticsEvent): void; static fromConfig(config: Config): GoogleAnalytics; } diff --git a/plugins/analytics-module-ga/config.d.ts b/plugins/analytics-module-ga/config.d.ts index 4ccd66e1dd..ac534daae8 100644 --- a/plugins/analytics-module-ga/config.d.ts +++ b/plugins/analytics-module-ga/config.d.ts @@ -27,6 +27,13 @@ export interface Config { */ trackingId: string; + /** + * URL to Google Analytics analytics.js script + * Defaults to fetching from GA source (eg. https://www.google-analytics.com/analytics.js) + * @visibility frontend + */ + scriptSrc?: string; + /** * Whether or not to log analytics debug statements to the console. * Defaults to false. diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index ffd6dd364c..15055b2a4f 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,22 +22,23 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-ga": "^3.3.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 1e6216936c..28202607be 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -39,21 +39,24 @@ export class GoogleAnalytics implements AnalyticsApi { /** * Instantiate the implementation and initialize ReactGA. */ - private constructor({ - cdmConfig, - trackingId, - testMode, - debug, - }: { + private constructor(options: { cdmConfig: CustomDimensionOrMetricConfig[]; trackingId: string; + scriptSrc?: string; testMode: boolean; debug: boolean; }) { + const { cdmConfig, trackingId, scriptSrc, testMode, debug } = options; + this.cdmConfig = cdmConfig; // Initialize Google Analytics. - ReactGA.initialize(trackingId, { testMode, debug, titleCase: false }); + ReactGA.initialize(trackingId, { + testMode, + debug, + gaAddress: scriptSrc, + titleCase: false, + }); } /** @@ -62,6 +65,7 @@ export class GoogleAnalytics implements AnalyticsApi { static fromConfig(config: Config) { // Get all necessary configuration. const trackingId = config.getString('app.analytics.ga.trackingId'); + const scriptSrc = config.getOptionalString('app.analytics.ga.scriptSrc'); const debug = config.getOptionalBoolean('app.analytics.ga.debug') ?? false; const testMode = config.getOptionalBoolean('app.analytics.ga.testMode') ?? false; @@ -82,6 +86,7 @@ export class GoogleAnalytics implements AnalyticsApi { // Return an implementation instance. return new GoogleAnalytics({ trackingId, + scriptSrc, cdmConfig, testMode, debug, @@ -93,13 +98,8 @@ export class GoogleAnalytics implements AnalyticsApi { * pageview and the rest as custom events. All custom dimensions/metrics are * applied as they should be (set on pageview, merged object on events). */ - captureEvent({ - context, - action, - subject, - value, - attributes, - }: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent) { + const { context, action, subject, value, attributes } = event; const customMetadata = this.getCustomDimensionMetrics(context, attributes); if (action === 'navigate' && context.extension === 'App') { diff --git a/plugins/apache-airflow/.eslintrc.js b/plugins/apache-airflow/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/apache-airflow/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/apache-airflow/.gitignore b/plugins/apache-airflow/.gitignore new file mode 100644 index 0000000000..352a14e3cc --- /dev/null +++ b/plugins/apache-airflow/.gitignore @@ -0,0 +1,2 @@ +airflow/ +node_modules/ diff --git a/plugins/apache-airflow/README.md b/plugins/apache-airflow/README.md new file mode 100644 index 0000000000..c57ab9da43 --- /dev/null +++ b/plugins/apache-airflow/README.md @@ -0,0 +1,124 @@ +# Apache Airflow Plugin + +Welcome to the apache-airflow plugin! + +## Feature Requests & Ideas + +- [ ] Add support for running multiple instances of Airflow for monitoring + various deployment stages or business domains. ([Suggested by @JGoldman110](https://github.com/backstage/backstage/issues/735#issuecomment-985063468)) +- [ ] Make owner chips in the DAG table clickable, resolving to a user or group + in the entity catalog. ([Suggested by @julioz](https://github.com/backstage/backstage/pull/8348#discussion_r764766295)) + +## Installation + +1. Install the plugin with `yarn` in the root of your Backstage directory + +```sh +yarn --cwd packages/app add @backstage/plugin-apache-airflow +``` + +2. Import and use the plugin extension in `spp/src/App.tsx` + +```diff +--- a/packages/app/src/App.tsx ++++ b/packages/app/src/App.tsx +@@ -86,6 +86,7 @@ import { providers } from './identityProviders'; + import * as plugins from './plugins'; + + import { techDocsPage } from './components/techdocs/TechDocsPage'; ++import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; + + const app = createApp({ + apis, +@@ -203,6 +204,7 @@ const routes = ( + element={} + /> + } /> ++ } /> + + ); +``` + +## Configuration + +For links to the Airflow instance, the `baseUrl` must be defined in +`app-config.yaml`. + +```yaml +apacheAirflow: + baseUrl: https://your.airflow.instance.com +``` + +This plugin uses the Backstage proxy to securely communicate with the Apache +Airflow API. Add the following to your `app-config.yaml` to enable this +configuration: + +```yaml +proxy: + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} +``` + +In your production deployment of Backstage, you would also need to ensure that +you've set the `AIRFLOW_BASIC_AUTH_HEADER` environment variable before starting +the backend. + +While working locally, you may wish to hard-code your API key in your +`app-config.local.yaml` like this: + +```yaml +# app-config.local.yaml +proxy: + '/airflow': + target: http://localhost:8080/api/v1 + headers: + Authorization: Basic YWlyZmxvdzphaXJmbG93 +``` + +Where the basic authorization token is the base64 encoding of the username and +password of your instance. + +```sh +echo -n "airflow:airflow" | base64 -w0 +``` + +## Development + +For local development, you can setup a local Airflow instance for development +purposes by [running Airflow with Docker Compose][2]. + +To verify that Airflow is running, and the API is functioning as expected, you +can run the following `curl` command: + +```sh +curl -X GET \ + --user "airflow:airflow" \ + localhost:8080/api/v1/dags +``` + +To run the Backstage proxy, you will have to run start the `example-backend` +plugin. + +```sh +yarn workspace example-backend start +``` + +To verify that the proxy is configured correctly, you can curl the Backstage +proxy endpoint. If using basic authentication, you will have to base64 encode +the username and password: + +```sh +curl http://localhost:7007/api/proxy/airflow/dags +``` + +And finally, to run an instance of this plugin, you can run: + +```sh +yarn start +``` + +[1]: https://airflow.apache.org/docs/apache-airflow/stable/security/api.html +[2]: https://airflow.apache.org/docs/apache-airflow/stable/start/docker.html +[3]: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md new file mode 100644 index 0000000000..ca151f0734 --- /dev/null +++ b/plugins/apache-airflow/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-apache-airflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// 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) +export const ApacheAirflowPage: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "apacheAirflowPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const apacheAirflowPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; +``` diff --git a/plugins/apache-airflow/config.d.ts b/plugins/apache-airflow/config.d.ts new file mode 100644 index 0000000000..3282116491 --- /dev/null +++ b/plugins/apache-airflow/config.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Configurations for the Apache Airflow plugin */ + apacheAirflow: { + /** + * The base url of the Apache Airflow installation. + * @visibility frontend + */ + baseUrl: string; + }; +} diff --git a/plugins/apache-airflow/dev/index.tsx b/plugins/apache-airflow/dev/index.tsx new file mode 100644 index 0000000000..bc8ce977a5 --- /dev/null +++ b/plugins/apache-airflow/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { apacheAirflowPlugin, ApacheAirflowPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(apacheAirflowPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/apache-airflow', + }) + .render(); diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json new file mode 100644 index 0000000000..a361961d12 --- /dev/null +++ b/plugins/apache-airflow/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-apache-airflow", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "cross-fetch": "^3.0.6", + "qs": "^6.10.1", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "msw": "^0.35.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts new file mode 100644 index 0000000000..b0a0229df4 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -0,0 +1,32 @@ +/* + * 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 { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; +import { Dag, InstanceStatus, InstanceVersion } from './types'; + +export const apacheAirflowApiRef = createApiRef({ + id: 'plugin.apacheairflow.service', + description: 'Used by the Apache Airflow plugin to make requests', +}); + +export type ApacheAirflowApi = { + discoveryApi: DiscoveryApi; + baseUrl: string; + listDags(options?: { objectsPerRequest: number }): Promise; + 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 new file mode 100644 index 0000000000..704b17eb34 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { UrlPatternDiscovery } from '@backstage/core-app-api'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ApacheAirflowClient } from './index'; +import { Dag } from './types'; + +const server = setupServer(); + +const dags: Dag[] = [ + { + dag_id: 'mock_dag_1', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_2', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_3', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_4', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_5', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, +]; + +describe('ApacheAirflowClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const setupHandlers = () => { + server.use( + rest.get(`${mockBaseUrl}/airflow/dags`, (req, res, ctx) => { + expect(req.url.searchParams.get('limit')).toBe('2'); + + // emulate paging to check if everything is requested + if (req.url.searchParams.get('offset') === '0') { + return res( + ctx.json({ + dags: dags.slice(0, 2), + total_entries: dags.length, + }), + ); + } + + // page offset 2 + if (req.url.searchParams.get('offset') === '2') { + return res( + ctx.json({ + dags: dags.slice(2, 4), + total_entries: dags.length, + }), + ); + } + + // page offset 4 + expect(req.url.searchParams.get('offset')).toBe('4'); + return res( + ctx.json({ + dags: dags.slice(4), + total_entries: dags.length, + }), + ); + }), + + rest.patch(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { + const { dag_id } = req.params; + const body = JSON.parse(req.body as string); + expect(body.is_paused).toBeDefined(); + return res( + ctx.json({ + dag_id: dag_id, + root_dag_id: 'string', + is_paused: body.is_paused, + is_active: true, + is_subdag: true, + fileloc: 'string', + file_token: 'string', + owners: ['string'], + description: 'string', + schedule_interval: { + __type: 'string', + days: 0, + seconds: 0, + microseconds: 0, + }, + tags: [{}], + }), + ); + }), + ); + }; + + it('list dags should return all dags with emulated pagination', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + + // call with limit of 2, to force two paginations in requesting all dags + // as our mocked response has 4 total entries + const responseDags = await client.listDags({ objectsPerRequest: 2 }); + expect(responseDags.length).toEqual(5); + expect(responseDags).toEqual(dags); + }); + + it('update dag should return dag information with updated paused attribute', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagId = 'mock_dag_1'; + const response: Dag = await client.updateDag(dagId, true); + expect(response.dag_id).toEqual(dagId); + expect(response.is_paused).toEqual(true); + }); +}); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts new file mode 100644 index 0000000000..c184e304b4 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -0,0 +1,100 @@ +/* + * 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 { DiscoveryApi } from '@backstage/core-plugin-api'; +import fetch from 'cross-fetch'; +import qs from 'qs'; +import { ApacheAirflowApi } from './ApacheAirflowApi'; +import { + Dag, + Dags, + InstanceStatus, + InstanceVersion, + ListDagsParams, +} from './types'; + +export class ApacheAirflowClient implements ApacheAirflowApi { + discoveryApi: DiscoveryApi; + baseUrl: string; + + constructor({ + discoveryApi, + baseUrl = 'http://localhost:8080', + }: { + discoveryApi: DiscoveryApi; + baseUrl: string; + }) { + this.discoveryApi = discoveryApi; + this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + } + + /** + * List all DAGs in the Airflow instance + * + * @remarks + * All DAGs with a limit of 100 results per request are returned; this may be + * bogged-down for instances with many DAGs, in which case table pagination + * should be implemented + * + * @param {number} objectsPerRequest records returned per request in pagination + * @returns {Promise} + */ + async listDags(options = { objectsPerRequest: 100 }): Promise { + const dags: Dag[] = []; + const searchParams: ListDagsParams = { + limit: options.objectsPerRequest, + offset: 0, + }; + + for (;;) { + const response = await this.fetch( + `/dags?${qs.stringify(searchParams)}`, + ); + dags.push(...response.dags); + + if (dags.length >= response.total_entries) { + break; + } + if (typeof searchParams.offset !== 'undefined') { + searchParams.offset += options.objectsPerRequest; + } + } + return dags; + } + + async updateDag(dagId: string, isPaused: boolean): Promise { + const init = { + method: 'PATCH', + body: JSON.stringify({ is_paused: isPaused }), + }; + return await this.fetch(`/dags/${dagId}`, init); + } + + async getInstanceStatus(): Promise { + return await this.fetch('/health'); + } + + async getInstanceVersion(): Promise { + return await this.fetch('/version'); + } + + private async fetch(input: string, init?: RequestInit): Promise { + const proxyUri = `${await this.discoveryApi.getBaseUrl('proxy')}/airflow`; + const response = await fetch(`${proxyUri}${input}`, init); + if (!response.ok) throw new Error(response.statusText); + return await response.json(); + } +} diff --git a/plugins/apache-airflow/src/api/index.ts b/plugins/apache-airflow/src/api/index.ts new file mode 100644 index 0000000000..c6dfe2205e --- /dev/null +++ b/plugins/apache-airflow/src/api/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An API definition and client for integrating with the Apache Airflow v2+ + * API + * + * @packageDocumentation + */ + +export * from './ApacheAirflowApi'; +export * from './ApacheAirflowClient'; diff --git a/plugins/apache-airflow/src/api/types/Dags.ts b/plugins/apache-airflow/src/api/types/Dags.ts new file mode 100644 index 0000000000..8f1d68fb2d --- /dev/null +++ b/plugins/apache-airflow/src/api/types/Dags.ts @@ -0,0 +1,83 @@ +/* + * 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. + */ + +/** + * Query parameters for listing DAGs + */ +export interface ListDagsParams { + limit?: number; + offset?: number; + order_by?: string; + tags?: Tag[]; + only_active?: boolean; +} + +export interface Dags { + dags: Dag[]; + total_entries: number; +} + +export interface Dag { + dag_id: string; + root_dag_id?: string; + is_paused?: boolean; + is_active?: boolean; + is_subdag?: boolean; + fileloc: string; + file_token: string; + owners: string[]; + description?: string; + schedule_interval: ScheduleInterval; + tags: Tag[]; +} + +export interface TimeDelta { + __type: 'TimeDelta'; + days: number; + seconds: number; + microseconds: number; +} + +export interface RelativeDelta { + __type: 'RelativeDelta'; + years: number; + months: number; + days: number; + leapdays: number; + hours: number; + minutes: number; + seconds: number; + microseconds: number; + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + microsecond: number; +} + +export interface CronExpression { + __type: 'CronExpression'; + value: string; +} + +// discrimant union of possible schedule interval types +export type ScheduleInterval = TimeDelta | RelativeDelta | CronExpression; + +export interface Tag { + name: string; +} diff --git a/plugins/apache-airflow/src/api/types/InstanceStatus.ts b/plugins/apache-airflow/src/api/types/InstanceStatus.ts new file mode 100644 index 0000000000..3b7a5ce416 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/InstanceStatus.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface InstanceStatus { + metadatabase: MetadatabaseStatus; + scheduler: SchedulerStatus; +} + +export interface MetadatabaseStatus { + status: 'healthy' | 'unhealthy'; +} + +export interface SchedulerStatus { + status: 'healthy' | 'unhealthy'; + latest_scheduler_heartbeat?: string; +} diff --git a/plugins/apache-airflow/src/api/types/InstanceVersion.ts b/plugins/apache-airflow/src/api/types/InstanceVersion.ts new file mode 100644 index 0000000000..b8d2727229 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/InstanceVersion.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface InstanceVersion { + version: string; + git_version?: string; +} diff --git a/plugins/apache-airflow/src/api/types/index.ts b/plugins/apache-airflow/src/api/types/index.ts new file mode 100644 index 0000000000..67c2d293a2 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + ListDagsParams, + Dag, + Dags, + Tag, + TimeDelta, + RelativeDelta, + CronExpression, + ScheduleInterval, +} from './Dags'; +export type { InstanceStatus } from './InstanceStatus'; +export type { InstanceVersion } from './InstanceVersion'; diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx new file mode 100644 index 0000000000..13122ca552 --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -0,0 +1,151 @@ +/* + * 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 { + Progress, + StatusError, + StatusOK, + Table, + TableColumn, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Box from '@material-ui/core/Box'; +import Chip from '@material-ui/core/Chip'; +import IconButton from '@material-ui/core/IconButton'; +import Switch from '@material-ui/core/Switch'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { Dag } from '../../api/types'; +import { ScheduleIntervalLabel } from '../ScheduleIntervalLabel'; + +type DagTableRow = Dag & { + id: string; + dagUrl: string; +}; + +const columns: TableColumn[] = [ + { + title: 'Paused', + field: 'is_paused', + render: (row: Partial) => ( + + + + ), + width: '5%', + }, + { + title: 'DAG', + field: 'id', + render: (row: Partial) => ( +
+ + {row.id} + + + {row.tags?.map((tag, ix) => ( + + ))} + +
+ ), + width: '60%', + }, + { + title: 'Owner', + field: 'owners', + render: (row: Partial) => ( + + {row.owners?.map((owner, ix) => ( + + ))} + + ), + width: '10%', + }, + { + title: 'Active', + render: (row: Partial) => ( + + {row.is_active ? : } + + {row.is_active ? 'Active' : 'Inactive'} + + + ), + width: '10%', + }, + { + title: 'Schedule', + render: (row: Partial) => ( + + ), + width: '10%', + }, + { + title: 'Link', + field: 'dagUrl', + render: (row: Partial) => ( + + + + + + ), + width: '5%', + }, +]; + +type DenseTableProps = { + dags: Dag[]; +}; + +export const DenseTable = ({ dags }: DenseTableProps) => { + return ( + + ); +}; + +export const DagTableComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = useAsync(async (): Promise => { + return await apiClient.listDags(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const data = value?.map(el => ({ + ...el, + 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 ; +}; diff --git a/plugins/apache-airflow/src/components/DagTableComponent/index.ts b/plugins/apache-airflow/src/components/DagTableComponent/index.ts new file mode 100644 index 0000000000..714cbce47d --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DagTableComponent } from './DagTableComponent'; diff --git a/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx b/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx new file mode 100644 index 0000000000..23afdd0a1e --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { ApacheAirflowApi, apacheAirflowApiRef } from '../../api'; +import { HomePage } from './HomePage'; + +describe('', () => { + const mockApi: jest.Mocked = { + getInstanceStatus: jest.fn().mockResolvedValue({ + metadatabase: { status: 'healthy' }, + scheduler: { status: 'healthy' }, + }), + getInstanceVersion: jest.fn().mockResolvedValue({ + version: 'v2.0.0', + }), + listDags: jest.fn().mockResolvedValue([ + { + dag_id: 'mock_dag_1', + }, + ]), + } as any; + + it('homepage should render', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + expect(getByText('Apache Airflow')).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/HomePage/HomePage.tsx b/plugins/apache-airflow/src/components/HomePage/HomePage.tsx new file mode 100644 index 0000000000..442f1bf4a0 --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/HomePage.tsx @@ -0,0 +1,55 @@ +/* + * 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 { + Content, + ContentHeader, + Header, + HeaderLabel, + Page, + SupportButton, +} from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { DagTableComponent } from '../DagTableComponent'; +import { StatusComponent } from '../StatusComponent'; +import { VersionComponent } from '../VersionComponent'; + +export const HomePage = () => ( + +
+ +
+ + + + See an overview of your Apache Airflow instance, and manage workflows + + + + + + + + + + + + + + +
+); diff --git a/plugins/apache-airflow/src/components/HomePage/index.ts b/plugins/apache-airflow/src/components/HomePage/index.ts new file mode 100644 index 0000000000..41d7bc5d03 --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HomePage } from './HomePage'; diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx new file mode 100644 index 0000000000..bfe4119bcd --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { CronExpression, RelativeDelta, TimeDelta } from '../../api/types'; +import { ScheduleIntervalLabel } from './ScheduleIntervalLabel'; + +describe('ScheduleIntervalLabel', () => { + it('should render with cronexpression interval', async () => { + const interval = { + __type: 'CronExpression', + value: '0 0 * * *', + } as CronExpression; + const { findByText } = render( + , + ); + expect(await findByText('0 0 * * *')).toBeInTheDocument(); + }); + + it('should render with time delta interval', async () => { + const interval = { + __type: 'TimeDelta', + days: 5, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('5 days 00:12:30')).toBeInTheDocument(); + }); + + it('should not render days with time delta interval with 0 days', async () => { + const interval = { + __type: 'TimeDelta', + days: 0, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('00:12:30')).toBeInTheDocument(); + }); + + it('should render singular day with time delta interval and 1 day', async () => { + const interval = { + __type: 'TimeDelta', + days: 1, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('1 day 00:12:30')).toBeInTheDocument(); + }); + + it('should render with relative delta interval', async () => { + const interval = { + __type: 'RelativeDelta', + days: 15, + months: 6, + } as RelativeDelta; + const { findByText } = render( + , + ); + expect( + await findByText('relativedelta(days=+15,months=+6)'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx new file mode 100644 index 0000000000..3ae72098d0 --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx @@ -0,0 +1,63 @@ +/* + * 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 Chip from '@material-ui/core/Chip'; +import React from 'react'; +import { ScheduleInterval, TimeDelta, RelativeDelta } from '../../api/types'; + +interface Props { + interval: ScheduleInterval | undefined; +} + +const timeDeltaToLabel = (delta: TimeDelta): String => { + let label = ''; + const date = new Date(0); + date.setSeconds(delta.seconds); + const time = date.toISOString().substr(11, 8); + if (delta.days === 0) { + label = `${time}`; + } else if (delta.days === 1) { + label = `1 day ${time}`; + } else { + label = `${delta.days} days ${time}`; + } + return label; +}; + +const relativeDeltaToLabel = (delta: RelativeDelta) => { + const params = Object.entries(delta) + .filter(o => o[0] !== '__type' && o[1] !== null && o[1] !== 0) + .map(o => (o[1] > 0 ? `${o[0]}=+${o[1]}` : `${o[0]}=-${o[1]}`)); + return `relativedelta(${params})`; +}; + +export const ScheduleIntervalLabel = ({ interval }: Props) => { + let label: String = ''; + switch (interval?.__type) { + case 'TimeDelta': + label = timeDeltaToLabel(interval); + break; + case 'RelativeDelta': + label = relativeDeltaToLabel(interval); + break; + case 'CronExpression': + label = interval.value; + break; + default: + label = 'None'; + } + return ; +}; diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts new file mode 100644 index 0000000000..cb6b2092f5 --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ScheduleIntervalLabel } from './ScheduleIntervalLabel'; diff --git a/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx b/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx new file mode 100644 index 0000000000..326be4b450 --- /dev/null +++ b/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx @@ -0,0 +1,60 @@ +/* + * 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 { + InfoCard, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { InstanceStatus } from '../../api/types'; + +export const StatusComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = + useAsync(async (): Promise => { + return await apiClient.getInstanceStatus(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (value) { + const metadata = { + 'Metadatabase Status': value.metadatabase.status, + 'Scheduler Status': value.scheduler.status, + 'Latest Scheduler Heartbeat': value.scheduler.latest_scheduler_heartbeat, + }; + + return ( + + + + ); + } + return No status information found...; +}; diff --git a/plugins/apache-airflow/src/components/StatusComponent/index.ts b/plugins/apache-airflow/src/components/StatusComponent/index.ts new file mode 100644 index 0000000000..4898c15e26 --- /dev/null +++ b/plugins/apache-airflow/src/components/StatusComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StatusComponent } from './StatusComponent'; diff --git a/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx b/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx new file mode 100644 index 0000000000..482d1ff0a7 --- /dev/null +++ b/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx @@ -0,0 +1,55 @@ +/* + * 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 { + InfoCard, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { InstanceVersion } from '../../api/types'; + +export const VersionComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = + useAsync(async (): Promise => { + return await apiClient.getInstanceVersion(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (value) { + const metadata = { + Version: value.version, + 'Git Version': value.git_version, + }; + + return ( + + + + ); + } + return No status information found...; +}; diff --git a/plugins/apache-airflow/src/components/VersionComponent/index.ts b/plugins/apache-airflow/src/components/VersionComponent/index.ts new file mode 100644 index 0000000000..4ace64d320 --- /dev/null +++ b/plugins/apache-airflow/src/components/VersionComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { VersionComponent } from './VersionComponent'; diff --git a/plugins/apache-airflow/src/index.ts b/plugins/apache-airflow/src/index.ts new file mode 100644 index 0000000000..7186839c0a --- /dev/null +++ b/plugins/apache-airflow/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A Backstage plugin that integrates towards Apache Airflow + * + * @packageDocumentation + */ + +export { apacheAirflowPlugin, ApacheAirflowPage } from './plugin'; diff --git a/plugins/apache-airflow/src/plugin.test.ts b/plugins/apache-airflow/src/plugin.test.ts new file mode 100644 index 0000000000..26997d74d8 --- /dev/null +++ b/plugins/apache-airflow/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { apacheAirflowPlugin } from './plugin'; + +describe('apache-airflow', () => { + it('should export plugin', () => { + expect(apacheAirflowPlugin).toBeDefined(); + }); +}); diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts new file mode 100644 index 0000000000..04ef1974d4 --- /dev/null +++ b/plugins/apache-airflow/src/plugin.ts @@ -0,0 +1,51 @@ +/* + * 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 { rootRouteRef } from './routes'; +import { apacheAirflowApiRef, ApacheAirflowClient } from './api'; +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + configApiRef, +} from '@backstage/core-plugin-api'; + +export const apacheAirflowPlugin = createPlugin({ + id: 'apache-airflow', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: apacheAirflowApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new ApacheAirflowClient({ + discoveryApi, + baseUrl: configApi.getString('apacheAirflow.baseUrl'), + }), + }), + ], +}); + +export const ApacheAirflowPage = apacheAirflowPlugin.provide( + createRoutableExtension({ + name: 'ApacheAirflowPage', + component: () => import('./components/HomePage').then(m => m.HomePage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/apache-airflow/src/routes.ts b/plugins/apache-airflow/src/routes.ts new file mode 100644 index 0000000000..2a0872eba4 --- /dev/null +++ b/plugins/apache-airflow/src/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'apache-airflow', +}); diff --git a/plugins/apache-airflow/src/setupTests.ts b/plugins/apache-airflow/src/setupTests.ts new file mode 100644 index 0000000000..427556fe26 --- /dev/null +++ b/plugins/apache-airflow/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index c880dc983c..833976fc2b 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-api-docs +## 0.6.18 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog@0.7.4 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.6.17 + +### Patch Changes + +- dde1681f33: chore(dependencies): bump `graphiql` package to latest +- ef64a444ca: Update AsyncAPI component to 1.0.0-x releases + +## 0.6.16 + +### Patch Changes + +- 752a53d94e: Improve theme integration for OpenApi definition +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.6.15 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 9f8b5fbf84..93df7e68aa 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.6.15", + "version": "0.6.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,33 +30,34 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@asyncapi/react-component": "^0.23.0", + "@asyncapi/react-component": "^1.0.0-next.25", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.7.3", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", - "graphiql": "^1.0.0-alpha.10", - "graphql": "^15.3.0", + "graphiql": "^1.5.12", + "graphql": "^16.0.0", "isomorphic-form-data": "^2.0.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "swagger-client": "3.16.1", "swagger-ui-react": "^4.0.0-rc.3" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx index cb89476c03..c9f979edad 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx @@ -18,6 +18,15 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { AsyncApiDefinition } from './AsyncApiDefinition'; +jest.mock('use-resize-observer', () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })), +})); + describe('', () => { it('renders asyncapi spec', async () => { const definition = ` @@ -45,15 +54,7 @@ components: expect(getByText(/Account Service/i)).toBeInTheDocument(); expect(getByText(/user\/signedup/i)).toBeInTheDocument(); - expect(getByText(/UserSignedUp/i)).toBeInTheDocument(); - expect(getAllByText(/displayName/i)).toHaveLength(4); - }); - - it('renders error if definition is missing', async () => { - const { getByText } = await renderInTestApp( - , - ); - expect(getByText(/Error/i)).toBeInTheDocument(); - expect(getByText(/Document can't be null or falsey/i)).toBeInTheDocument(); + expect(getAllByText(/UserSignedUp/i)).toHaveLength(2); + expect(getAllByText(/displayName/i)).toHaveLength(3); }); }); diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx index 1c993f67d7..3f9375921d 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx @@ -15,23 +15,72 @@ */ import AsyncApi from '@asyncapi/react-component'; -import '@asyncapi/react-component/lib/styles/fiori.css'; -import { alpha, makeStyles } from '@material-ui/core/styles'; +import '@asyncapi/react-component/styles/default.css'; +import { makeStyles, alpha, darken } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; import React from 'react'; +import { useTheme } from '@material-ui/core'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme: BackstageTheme) => ({ root: { - '& .asyncapi': { - 'font-family': 'inherit', + fontFamily: 'inherit', + '& .bg-white': { background: 'none', }, - '& h2': { - ...theme.typography.h6, + '& .text-4xl': { + ...theme.typography.h3, }, - '& .text-teal': { - color: theme.palette.primary.main, + ' & h2': { + ...theme.typography.h4, + }, + '& .border': { + borderColor: alpha(theme.palette.border, 0.1), + }, + '& .min-w-min': { + minWidth: 'fit-content', + }, + '& .examples': { + padding: '1rem', + }, + '& .bg-teal-500': { + backgroundColor: theme.palette.status.ok, + }, + '& .bg-blue-500': { + backgroundColor: theme.palette.info.main, + }, + '& .bg-blue-400': { + backgroundColor: theme.palette.info.light, + }, + '& .bg-indigo-400': { + backgroundColor: theme.palette.warning.main, + }, + '& .text-teal-50': { + color: theme.palette.status.ok, + }, + '& .text-red-600': { + color: theme.palette.error.main, + }, + '& .text-orange-600': { + color: theme.palette.warning.main, + }, + '& .text-teal-500': { + color: theme.palette.status.ok, + }, + '& .text-blue-500': { + color: theme.palette.info.main, + }, + '& .-rotate-90': { + '--tw-rotate': '0deg', }, '& button': { + ...theme.typography.button, + borderRadius: theme.shape.borderRadius, + color: theme.palette.primary.main, + }, + '& a': { + color: theme.palette.link, + }, + '& a.no-underline': { ...theme.typography.button, background: 'none', boxSizing: 'border-box', @@ -48,84 +97,47 @@ const useStyles = makeStyles(theme => ({ border: `1px solid ${alpha(theme.palette.primary.main, 0.5)}`, '&:hover': { textDecoration: 'none', - '&.Mui-disabled': { - backgroundColor: 'transparent', - }, border: `1px solid ${theme.palette.primary.main}`, backgroundColor: alpha( theme.palette.primary.main, theme.palette.action.hoverOpacity, ), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - '&.Mui-disabled': { - color: theme.palette.action.disabled, }, }, - '& .asyncapi__collapse-button:hover': { - color: theme.palette.primary.main, - }, - '& button.asyncapi__toggle-button': { - 'min-width': 'inherit', - }, - '& .asyncapi__info-list li': { - 'border-color': theme.palette.primary.main, - '&:hover': { - color: theme.palette.text.primary, - 'border-color': theme.palette.primary.main, - 'background-color': theme.palette.primary.main, - }, - }, - '& .asyncapi__info-list li a': { - color: theme.palette.primary.main, - '&:hover': { + '& li.no-underline': { + '& a': { + textDecoration: 'none', color: theme.palette.getContrastText(theme.palette.primary.main), }, }, - '& .asyncapi__enum': { - color: theme.palette.secondary.main, + }, + dark: { + '& svg': { + fill: theme.palette.text.primary, }, - '& .asyncapi__info, .asyncapi__channel, .asyncapi__channels > div, .asyncapi__schema, .asyncapi__channel-operations-list .asyncapi__messages-list-item .asyncapi__message, .asyncapi__message, .asyncapi__server, .asyncapi__servers > div, .asyncapi__messages > div, .asyncapi__schemas > div': - { - 'background-color': 'inherit', - }, - '& .asyncapi__channel-parameters-header, .asyncapi__channel-operations-header, .asyncapi__channel-operation-oneOf-subscribe-header, .asyncapi__channel-operation-oneOf-publish-header, .asyncapi__channel-operation-message-header, .asyncapi__message-header, .asyncapi__message-header-title, .asyncapi__message-header-title > h3, .asyncapi__bindings, .asyncapi__bindings-header, .asyncapi__bindings-header > h4': - { - 'background-color': 'inherit', + '& .prose': { + color: theme.palette.text.secondary, + '& h3': { color: theme.palette.text.primary, }, - '& .asyncapi__additional-properties-notice': { - color: theme.palette.text.hint, }, - '& .asyncapi__code, .asyncapi__code-pre': { - background: theme.palette.background.default, + '& .bg-gray-100, .bg-gray-200': { + backgroundColor: theme.palette.background.default, }, - '& .asyncapi__schema-example-header-title': { - color: theme.palette.text.secondary, + '& .text-gray-600': { + color: theme.palette.grey['50'], }, - '& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header': - { - 'background-color': 'inherit', - color: theme.palette.text.secondary, + '& .text-gray-700': { + color: theme.palette.grey['100'], + }, + '& .panel--right': { + background: darken(theme.palette.navigation.background, 0.1), + }, + '& .examples': { + backgroundColor: darken(theme.palette.navigation.background, 0.1), + '& pre': { + backgroundColor: darken(theme.palette.background.default, 0.2), }, - '& .asyncapi__table-header': { - background: theme.palette.background.default, - }, - '& .asyncapi__table-body': { - color: theme.palette.text.primary, - }, - '& .asyncapi__server-security-flow': { - background: theme.palette.background.default, - border: 'none', - }, - '& .asyncapi__server-security-flows-list a': { - color: theme.palette.primary.main, - }, - '& .asyncapi__table-row--nested': { - color: theme.palette.text.secondary, }, }, })); @@ -134,11 +146,15 @@ type Props = { definition: string; }; -export const AsyncApiDefinition = ({ definition }: Props) => { +export const AsyncApiDefinition = ({ definition }: Props): JSX.Element => { const classes = useStyles(); + const theme = useTheme(); + const classNames = `${classes.root} ${ + theme.palette.type === 'dark' ? classes.dark : '' + }`; return ( -
+
); diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx index 8d0accb254..601352f21a 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -22,18 +22,9 @@ import 'swagger-ui-react/swagger-ui.css'; const useStyles = makeStyles(theme => ({ root: { '& .swagger-ui': { - fontFamily: 'inherit', + fontFamily: theme.typography.fontFamily, color: theme.palette.text.primary, - [`& .info h1, - .info h2, - .info h3, - .info h4, - .info h5, - .info h6`]: { - fontFamily: 'inherit', - color: theme.palette.text.primary, - }, [`& .scheme-container`]: { backgroundColor: theme.palette.background.default, }, @@ -41,7 +32,7 @@ const useStyles = makeStyles(theme => ({ .opblock-tag small, table thead tr td, table thead tr th`]: { - fontFamily: 'inherit', + fontFamily: theme.typography.fontFamily, color: theme.palette.text.primary, borderColor: theme.palette.divider, }, @@ -49,35 +40,30 @@ const useStyles = makeStyles(theme => ({ section.models.is-open h4`]: { borderColor: theme.palette.divider, }, - [`& .opblock .opblock-summary-description, - .parameter__type, - table.headers td, - .model-title, - .model .property.primitive, - section h3`]: { - fontFamily: 'inherit', - color: theme.palette.text.secondary, + [`& .model-title, + .model .renderedMarkdown, + .model .description`]: { + fontFamily: theme.typography.fontFamily, + fontWeight: theme.typography.fontWeightRegular, }, - [`& .opblock .opblock-summary-operation-id, - .opblock .opblock-summary-path, - .opblock .opblock-summary-path__deprecated, - .opblock h4, - .opblock h5, - .opblock a, - .opblock li, + [`& h1, h2, h3, h4, h5, h6, + .errors h4, .error h4, .opblock h4, section.models h4, + .response-control-media-type__accept-message, + .opblock-summary-description, + .opblock-summary-operation-id, + .opblock-summary-path, + .opblock-summary-path__deprecated, + .opblock-external-docs-wrapper, + .opblock-section-header .btn, + .opblock-section-header>label, + .scheme-container .schemes>label,a.nostyle, .parameter__name, .response-col_status, .response-col_links, - .opblock-section-header .btn, - .tab li, - .info li, - .info p, - .info table, - section.models h4, + .error .btn, .info .title, - table.model tr.description, - .property-row`]: { - fontFamily: 'inherit', + .info .base-url`]: { + fontFamily: theme.typography.fontFamily, color: theme.palette.text.primary, }, [`& .opblock .opblock-section-header, @@ -89,23 +75,57 @@ const useStyles = makeStyles(theme => ({ .parameter__in`]: { color: theme.palette.text.disabled, }, - [`& .opblock-description-wrapper p, - .opblock-description-wrapper li, - .opblock-external-docs-wrapper p, - .opblock-title_normal p, - .response-control-media-type__accept-message, - .opblock .opblock-section-header>label, - .scheme-container .schemes>label, - .info .base-url, - .model`]: { - color: theme.palette.text.hint, + [`& table.model, + .parameter__type, + .model.model-title, + .model-title, + .model span, + .model .brace-open, + .model .brace-close, + .model .property.primitive, + .model .renderedMarkdown, + .model .description, + .errors small`]: { + color: theme.palette.text.secondary, }, [`& .parameter__name.required:after`]: { color: theme.palette.warning.dark, }, - [`& .prop-type`]: { + [`& table.model, + table.model .model, + .opblock-external-docs-wrapper`]: { + fontSize: theme.typography.fontSize, + }, + [`& table.headers td`]: { + color: theme.palette.text.primary, + fontWeight: theme.typography.fontWeightRegular, + }, + [`& .model-hint`]: { + color: theme.palette.text.hint, + backgroundColor: theme.palette.background.paper, + }, + [`& .opblock-summary-method, + .info a`]: { + fontFamily: theme.typography.fontFamily, + }, + [`& .info, .opblock, .tab`]: { + [`& li, p`]: { + fontFamily: theme.typography.fontFamily, + color: theme.palette.text.primary, + }, + }, + [`& a`]: { color: theme.palette.primary.main, }, + [`& .renderedMarkdown code`]: { + color: theme.palette.secondary.light, + }, + [`& .property-row td:first-child`]: { + color: theme.palette.text.primary, + }, + [`& span.prop-type`]: { + color: theme.palette.success.light, + }, }, }, })); diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index ebb0393192..927eae7022 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/config-loader": "^0.8.0", + "@backstage/backend-common": "^0.9.12", + "@backstage/config-loader": "^0.8.1", "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", @@ -42,7 +42,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 38eaf268e6..dc45804449 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-auth-backend +## 0.5.0 + +### Minor Changes + +- a036b65c2f: **BREAKING CHANGE** The `idToken` field of `BackstageIdentity` has been removed, with the `token` taking its place. This means you may need to update existing `signIn.resolver` implementations to return an `token` rather than an `idToken`. This also applies to custom auth providers. + + The `BackstageIdentity` type has been deprecated and will be removed in the future. Taking its place is the new `BackstageSignInResult` type with the same shape. + + This change also introduces the new `BackstageIdentityResponse` that mirrors the type with the same name from `@backstage/core-plugin-api`. The `BackstageIdentityResponse` type is different from the `BackstageSignInResult` in that it also has a `identity` field which is of type `BackstageUserIdentity` and is a decoded version of the information within the token. + + When implementing a custom auth provider that is not based on the `OAuthAdapter` you may need to convert `BackstageSignInResult` into a `BackstageIdentityResponse`, this can be done using the new `prepareBackstageIdentityResponse` function. + +### Patch Changes + +- 8f461e6043: Fixes potential bug introduced in `0.4.10` which causes `OAuth2AuthProvider` to authenticate using credentials in both POST payload and headers. + This might break some stricter OAuth2 implementations so there is now a `includeBasicAuth` config option that can manually be set to `true` to enable this behavior. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/test-utils@0.1.24 + - @backstage/backend-common@0.9.13 + +## 0.4.10 + +### Patch Changes + +- 4bf4111902: Migrated the SAML provider to implement the `authHandler` and `signIn.resolver` options. +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- 36fa32216f: Added signIn and authHandler resolver for oidc provider +- 7071dce02d: Expose catalog lib in plugin-auth-backend, i.e `CatalogIdentityClient` class is exposed now. +- 1b69ed44f2: Added custom OAuth2.0 authorization header for generic oauth2 provider. +- Updated dependencies + - @backstage/backend-common@0.9.12 + ## 0.4.9 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index eb7c53bd6b..a637118130 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -14,7 +14,9 @@ import { Logger as Logger_2 } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Profile } from 'passport'; +import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; +import { UserinfoResponse } from 'openid-client'; // Warning: (ae-missing-release-tag) "AtlassianAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -101,7 +103,7 @@ export interface AuthProviderRouteHandlers { export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; // Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -114,14 +116,28 @@ export type AwsAlbProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type BackstageIdentity = { - id: string; - idToken?: string; - token?: string; +// @public @deprecated +export type BackstageIdentity = BackstageSignInResult; + +// @public +export interface BackstageIdentityResponse extends BackstageSignInResult { + identity: BackstageUserIdentity; +} + +// @public +export interface BackstageSignInResult { + // @deprecated entity?: Entity; + // @deprecated + id: string; + token: string; +} + +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; }; // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -183,10 +199,7 @@ export class CatalogIdentityClient { // Warning: (ae-forgotten-export) The symbol "UserQuery" needs to be exported by the entry point index.d.ts findUser(query: UserQuery): Promise; // Warning: (ae-forgotten-export) The symbol "MemberClaimQuery" needs to be exported by the entry point index.d.ts - resolveCatalogMembership({ - entityRefs, - logger, - }: MemberClaimQuery): Promise; + resolveCatalogMembership(query: MemberClaimQuery): Promise; } // Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -245,6 +258,14 @@ export const createOAuth2Provider: ( options?: OAuth2ProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-forgotten-export) The symbol "OidcProviderOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createOidcProvider: ( + options?: OidcProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createOktaProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -260,13 +281,12 @@ export function createOriginFilter(config: Config): (origin: string) => boolean; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createRouter({ - logger, - config, - discovery, - database, - providerFactories, -}: RouterOptions): Promise; +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export const createSamlProvider: ( + options?: SamlProviderOptions | undefined, +) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -346,7 +366,7 @@ export type GoogleProviderOptions = { // @public export class IdentityClient { constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); - authenticate(token: string | undefined): Promise; + authenticate(token: string | undefined): Promise; static getBearerToken( authorizationHeader: string | undefined, ): string | undefined; @@ -433,7 +453,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; logout?(): Promise; @@ -441,7 +461,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -476,10 +496,12 @@ export type OAuthRefreshRequest = express.Request<{}> & { refreshToken: string; }; -// Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type OAuthResponse = AuthResponse; +// @public +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; +}; // Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -536,8 +558,11 @@ export const postMessageResponse: ( response: WebMessageResponse, ) => void; -// Warning: (ae-missing-release-tag) "ProfileInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse; + // @public export type ProfileInfo = { email?: string; @@ -568,6 +593,19 @@ export interface RouterOptions { providerFactories?: ProviderFactories; } +// @public (undocumented) +export type SamlAuthResult = { + fullProfile: any; +}; + +// @public (undocumented) +export type SamlProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver?: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -609,5 +647,4 @@ export type WebMessageResponse = // src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:122:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index fecabf8ad1..b2822c4f33 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.9", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", - "@backstage/test-utils": "^0.1.23", + "@backstage/test-utils": "^0.1.24", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-backend/src/identity/IdentityClient.test.ts index 5eca6bf8cd..9f5e1ce489 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.test.ts @@ -96,7 +96,15 @@ describe('IdentityClient', () => { it('should accept fresh token', async () => { const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should throw on incorrect issuer', async () => { @@ -159,7 +167,15 @@ describe('IdentityClient', () => { jest.spyOn(Date, 'now').mockImplementation(() => fixedTime); const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should not be fooled by the none algorithm', async () => { diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-backend/src/identity/IdentityClient.ts index d60829bf59..552d231e1f 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.ts @@ -16,8 +16,9 @@ import fetch from 'node-fetch'; import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; -import { BackstageIdentity } from '../providers'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { AuthenticationError } from '@backstage/errors'; +import { BackstageIdentityResponse } from '../providers/types'; const CLOCK_MARGIN_S = 10; @@ -45,15 +46,17 @@ export class IdentityClient { * Returns a BackstageIdentity (user) matching the token. * The method throws an error if verification fails. */ - async authenticate(token: string | undefined): Promise { + async authenticate( + token: string | undefined, + ): Promise { // Extract token from header if (!token) { - throw new Error('No token specified'); + throw new AuthenticationError('No token specified'); } // Get signing key matching token const key = await this.getKey(token); if (!key) { - throw new Error('No signing key matching token found'); + throw new AuthenticationError('No signing key matching token found'); } // Verify token claims and signature // Note: Claims must match those set by TokenFactory when issuing tokens @@ -62,12 +65,21 @@ export class IdentityClient { algorithms: ['ES256'], audience: 'backstage', issuer: this.issuer, - }) as { sub: string }; + }) as { sub: string; ent: string[] }; // Verified, return the matching user as BackstageIdentity // TODO: Settle internal user format/properties - const user: BackstageIdentity = { + if (!decoded.sub) { + throw new AuthenticationError('No user sub found in token'); + } + + const user: BackstageIdentityResponse = { id: decoded.sub, - idToken: token, + token, + identity: { + type: 'user', + userEntityRef: decoded.sub, + ownershipEntityRefs: decoded.ent ?? [], + }, }; return user; } diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 894f8c1e55..66c1b42dd9 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -29,7 +29,7 @@ export * from './providers'; // ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup export * from './lib/flow'; -// OAuth wrapper over a passport or a custom `startegy`. +// OAuth wrapper over a passport or a custom `strategy`. export * from './lib/oauth'; export * from './lib/catalog'; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 02fe9818cb..a1f915d152 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -84,10 +84,8 @@ export class CatalogIdentityClient { * * Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`. */ - async resolveCatalogMembership({ - entityRefs, - logger, - }: MemberClaimQuery): Promise { + async resolveCatalogMembership(query: MemberClaimQuery): Promise { + const { entityRefs, logger } = query; const resolvedEntityRefs = entityRefs .map((ref: string) => { try { diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 117f1b86e4..07c8196dd2 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -51,7 +51,12 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -106,7 +111,12 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -148,7 +158,12 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index fa61d69d80..92c76b04b7 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -31,6 +31,8 @@ const mockResponseData = { }, backstageIdentity: { id: 'foo', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', }, }; @@ -217,7 +219,13 @@ describe('OAuthAdapter', () => { ...mockResponseData, backstageIdentity: { id: mockResponseData.backstageIdentity.id, - token: 'my-id-token', + token: mockResponseData.backstageIdentity.token, + idToken: mockResponseData.backstageIdentity.token, + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, }); }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index e1a6fdf2f9..eb3f7efa42 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -19,8 +19,9 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - BackstageIdentity, AuthProviderConfig, + BackstageIdentityResponse, + BackstageSignInResult, } from '../../providers/types'; import { AuthenticationError, @@ -37,6 +38,7 @@ import { OAuthRefreshRequest, OAuthState, } from './types'; +import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -150,12 +152,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - await this.populateIdentity(response.backstageIdentity); + const identity = await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful return postMessageResponse(res, appOrigin, { type: 'authorization_response', - response, + response: { ...response, backstageIdentity: identity }, }); } catch (error) { const { name, message } = isError(error) @@ -209,7 +211,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { forwardReq as OAuthRefreshRequest, ); - await this.populateIdentity(response.backstageIdentity); + const backstageIdentity = await this.populateIdentity( + response.backstageIdentity, + ); if ( response.providerInfo.refreshToken && @@ -218,7 +222,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); } - res.status(200).json(response); + res.status(200).json({ ...response, backstageIdentity }); } catch (error) { throw new AuthenticationError('Refresh failed', error); } @@ -228,18 +232,22 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. */ - private async populateIdentity(identity?: BackstageIdentity) { + private async populateIdentity( + identity?: BackstageSignInResult, + ): Promise { if (!identity) { - return; + return undefined; } - if (!(identity.token || identity.idToken)) { - identity.token = await this.options.tokenIssuer.issueToken({ - claims: { sub: identity.id }, - }); - } else if (!identity.token && identity.idToken) { - identity.token = identity.idToken; + if (identity.token) { + return prepareBackstageIdentityResponse(identity); } + + const token = await this.options.tokenIssuer.issueToken({ + claims: { sub: identity.id }, + }); + + return prepareBackstageIdentityResponse({ ...identity, token }); } private setNonceCookie = (res: express.Response, nonce: string) => { diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 7912dd16a5..cd1439b399 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,7 +16,11 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { AuthResponse, RedirectInfo } from '../../providers/types'; +import { + RedirectInfo, + BackstageSignInResult, + ProfileInfo, +} from '../../providers/types'; /** * Common options for passport.js-based OAuth providers @@ -47,7 +51,16 @@ export type OAuthResult = { refreshToken?: string; }; -export type OAuthResponse = AuthResponse; +/** + * The expected response from an OAuth flow. + * + * @public + */ +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; +}; export type OAuthProviderInfo = { /** @@ -108,7 +121,7 @@ export interface OAuthHandlers { * @param {express.Request} req */ handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; @@ -117,7 +130,7 @@ export interface OAuthHandlers { * @param {string} refreshToken * @param {string} scope */ - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; /** * (Optional) Sign out of the auth provider. diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 4cc3f33499..7aa98c3e65 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -151,7 +151,7 @@ export class Auth0AuthProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 3f72004d48..048128f942 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -122,7 +122,11 @@ describe('AwsAlbAuthProvider', () => { profile: makeProfileInfo(fullProfile), }), signInResolver: async () => { - return { id: 'user.name', token: 'TOKEN' }; + return { + id: 'user.name', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + }; }, }); @@ -133,7 +137,15 @@ describe('AwsAlbAuthProvider', () => { expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { id: 'user.name', - token: 'TOKEN', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + idToken: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, profile: { displayName: 'User Name', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index b0b9070d50..114bc9a204 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -32,6 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -198,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { accessToken: result.accessToken, expiresInSeconds: result.expiresInSeconds, }, - backstageIdentity, + backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), profile, }; } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 4a9221ce21..4ecbb98fd2 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -19,10 +19,12 @@ export * from './gitlab'; export * from './google'; export * from './microsoft'; export * from './oauth2'; +export * from './oidc'; export * from './okta'; export * from './bitbucket'; export * from './atlassian'; export * from './aws-alb'; +export * from './saml'; export { factories as defaultAuthProviderFactories } from './factories'; @@ -36,4 +38,13 @@ export type { // These types are needed for a postMessage from the login pop-up // to the frontend -export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types'; +export type { + AuthResponse, + BackstageIdentity, + BackstageUserIdentity, + BackstageIdentityResponse, + BackstageSignInResult, + ProfileInfo, +} from './types'; + +export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index cf70580664..2f9c739860 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -59,6 +59,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & { tokenUrl: string; scope?: string; logger: Logger; + includeBasicAuth?: boolean; }; export class OAuth2AuthProvider implements OAuthHandlers { @@ -85,6 +86,14 @@ export class OAuth2AuthProvider implements OAuthHandlers { tokenURL: options.tokenUrl, passReqToCallback: false as true, scope: options.scope, + customHeaders: options.includeBasicAuth + ? { + Authorization: `Basic ${this.encodeClientCredentials( + options.clientId, + options.clientSecret, + )}`, + } + : undefined, }, ( accessToken: any, @@ -187,6 +196,10 @@ export class OAuth2AuthProvider implements OAuthHandlers { return response; } + + encodeClientCredentials(clientID: string, clientSecret: string): string { + return Buffer.from(`${clientID}:${clientSecret}`).toString('base64'); + } } export const oAuth2DefaultSignInResolver: SignInResolver = async ( @@ -234,6 +247,7 @@ export const createOAuth2Provider = ( const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); const scope = envConfig.getOptionalString('scope'); + const includeBasicAuth = envConfig.getOptionalBoolean('includeBasicAuth'); const disableRefresh = envConfig.getOptionalBoolean('disableRefresh') ?? false; @@ -270,6 +284,7 @@ export const createOAuth2Provider = ( tokenUrl, scope, logger, + includeBasicAuth, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index 8c755cd501..20d014a783 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -15,4 +15,3 @@ */ export { createOidcProvider } from './provider'; -export type { OidcProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index ac5035e9a5..da96ae1a25 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -24,7 +24,8 @@ import { setupServer } from 'msw/node'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OAuthAdapter } from '../../lib/oauth'; import { AuthProviderFactoryOptions } from '../types'; -import { createOidcProvider, OidcAuthProvider } from './provider'; +import { createOidcProvider, OidcAuthProvider, Options } from './provider'; +import { getVoidLogger } from '@backstage/backend-common'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -42,7 +43,23 @@ const issuerMetadata = { request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; -const clientMetadata = { +const catalogIdentityClient = { + findUser: jest.fn(), +}; +const tokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), +}; + +const clientMetadata: Options = { + authHandler: async input => ({ + profile: { + displayName: input.userinfo.email, + }, + }), + catalogIdentityClient: catalogIdentityClient as unknown as any, + logger: getVoidLogger(), + tokenIssuer: tokenIssuer as unknown as any, callbackUrl: 'https://oidc.test/callback', clientId: 'testclientid', clientSecret: 'testclientsecret', @@ -160,7 +177,7 @@ describe('OidcAuthProvider', () => { ...clientMetadata, metadataUrl: 'https://oidc.test/.well-known/openid-configuration', }, - }); + } as any); const options = { globalConfig: { appUrl: 'https://oidc.test', diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 092ffa7099..158af6f830 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -16,28 +16,36 @@ import express from 'express'; import { - Issuer, Client, + Issuer, Strategy as OidcStrategy, TokenSet, UserinfoResponse, } from 'openid-client'; import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, OAuthRefreshRequest, + OAuthResponse, + OAuthStartRequest, } from '../../lib/oauth'; import { executeFrameHandlerStrategy, executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + AuthHandler, + AuthProviderFactory, + RedirectInfo, + SignInResolver, +} from '../types'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { TokenIssuer } from '../../identity'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken?: string; @@ -58,6 +66,11 @@ export type Options = OAuthProviderOptions & { scope?: string; prompt?: string; tokenSignedResponseAlg?: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class OidcAuthProvider implements OAuthHandlers { @@ -65,16 +78,26 @@ export class OidcAuthProvider implements OAuthHandlers { private readonly scope?: string; private readonly prompt?: string; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; + constructor(options: Options) { this.implementation = this.setupStrategy(options); this.scope = options.scope; this.prompt = options.prompt; + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; } async start(req: OAuthStartRequest): Promise { const { strategy } = await this.implementation; const options: Record = { - accessType: 'offline', scope: req.scope || this.scope || 'openid profile email', state: encodeState(req.state), }; @@ -97,19 +120,8 @@ export class OidcAuthProvider implements OAuthHandlers { result: { userinfo, tokenset }, privateInfo, } = strategyResponse; - const identityResponse = await this.populateIdentity({ - profile: { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }, - providerInfo: { - idToken: tokenset.id_token, - accessToken: tokenset.access_token || '', - scope: tokenset.scope || '', - expiresInSeconds: tokenset.expires_in, - }, - }); + + const identityResponse = await this.handleResult({ tokenset, userinfo }); return { response: identityResponse, refreshToken: privateInfo.refreshToken, @@ -123,22 +135,13 @@ export class OidcAuthProvider implements OAuthHandlers { throw new Error('Refresh failed'); } const profile = await client.userinfo(tokenset.access_token); - - return this.populateIdentity({ - providerInfo: { - accessToken: tokenset.access_token, - refreshToken: tokenset.refresh_token, - expiresInSeconds: tokenset.expires_in, - idToken: tokenset.id_token, - scope: tokenset.scope || '', - }, - profile, - }); + return this.handleResult({ tokenset, userinfo: profile }); } private async setupStrategy(options: Options): Promise { const issuer = await Issuer.discover(options.metadataUrl); const client = new issuer.Client({ + access_type: 'offline', // this option must be passed to provider to receive a refresh token client_id: options.clientId, client_secret: options.clientSecret, redirect_uris: [options.callbackUrl], @@ -177,26 +180,71 @@ export class OidcAuthProvider implements OAuthHandlers { // Use this function to grab the user profile info from the token // Then populate the profile with it - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; - - if (!profile.email) { - throw new Error('Profile does not contain an email'); + private async handleResult(result: AuthResult): Promise { + const { profile } = await this.authHandler(result); + const response: OAuthResponse = { + providerInfo: { + idToken: result.tokenset.id_token, + accessToken: result.tokenset.access_token!, + refreshToken: result.tokenset.refresh_token, + scope: result.tokenset.scope!, + expiresInSeconds: result.tokenset.expires_in, + }, + profile, + }; + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); } - const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return response; } } -export type OidcProviderOptions = {}; +export const oAuth2DefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Profile contained no email'); + } + const userId = profile.email.split('@')[0]; + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + return { id: userId, token }; +}; + +export type OidcProviderOptions = { + authHandler?: AuthHandler; + + signIn?: { + resolver?: SignInResolver; + }; +}; export const createOidcProvider = ( - _options?: OidcProviderOptions, + options?: OidcProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); @@ -207,6 +255,28 @@ export const createOidcProvider = ( ); const scope = envConfig.getOptionalString('scope'); const prompt = envConfig.getOptionalString('prompt'); + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ userinfo }) => ({ + profile: { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }, + }); + const signInResolverFn = + options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); const provider = new OidcAuthProvider({ clientId, @@ -216,6 +286,11 @@ export const createOidcProvider = ( metadataUrl, scope, prompt, + signInResolver, + authHandler, + logger, + tokenIssuer, + catalogIdentityClient, }); return OAuthAdapter.fromConfig(globalConfig, provider, { diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 092d22189d..66e8b0bfc5 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -148,7 +148,7 @@ export class OneLoginProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts new file mode 100644 index 0000000000..31df7a4cef --- /dev/null +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -0,0 +1,45 @@ +/* + * 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 { BackstageIdentityResponse, BackstageSignInResult } from './types'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(Buffer.from(payload, 'base64').toString()); +} + +/** + * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token + * + * @public + */ +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse { + const { sub, ent } = parseJwtPayload(result.token); + return { + ...{ + // TODO: idToken is for backwards compatibility and can be removed in the future + idToken: result.token, + ...result, + }, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [], + }, + }; +} diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index fa5d9d9a82..0aea660941 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -15,4 +15,4 @@ */ export { createSamlProvider } from './provider'; -export type { SamlProviderOptions } from './provider'; +export type { SamlProviderOptions, SamlAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 8929a1dbf0..8a4afd1fa6 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -26,31 +26,53 @@ import { executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { AuthProviderRouteHandlers, AuthProviderFactory } from '../types'; +import { + AuthProviderRouteHandlers, + AuthProviderFactory, + AuthHandler, + SignInResolver, + AuthResponse, +} from '../types'; import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; +import { CatalogIdentityClient } from '../../lib/catalog'; +import { Logger } from 'winston'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; -type SamlInfo = { +/** @public */ +export type SamlAuthResult = { fullProfile: any; }; type Options = SamlConfig & { + signInResolver?: SignInResolver; + authHandler: AuthHandler; tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; appUrl: string; }; export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; private readonly tokenIssuer: TokenIssuer; + private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly logger: Logger; private readonly appUrl: string; constructor(options: Options) { this.appUrl = options.appUrl; + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; this.tokenIssuer = options.tokenIssuer; + this.catalogIdentityClient = options.catalogIdentityClient; + this.logger = options.logger; this.strategy = new SamlStrategy({ ...options }, (( fullProfile: SamlProfile, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { // TODO: There's plenty more validation and profile handling to do here, // this provider is currently only intended to validate the provider pattern @@ -71,27 +93,38 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res: express.Response, ): Promise { try { - const { result } = await executeFrameHandlerStrategy( + const { result } = await executeFrameHandlerStrategy( req, this.strategy, ); - const id = result.fullProfile.nameID; + const { profile } = await this.authHandler(result); - const idToken = await this.tokenIssuer.issueToken({ - claims: { sub: id }, - }); + const response: AuthResponse<{}> = { + profile, + providerInfo: {}, + }; + + if (this.signInResolver) { + const signInResponse = await this.signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this.tokenIssuer, + catalogIdentityClient: this.catalogIdentityClient, + logger: this.logger, + }, + ); + + response.backstageIdentity = + prepareBackstageIdentityResponse(signInResponse); + } return postMessageResponse(res, this.appUrl, { type: 'authorization_response', - response: { - profile: { - email: result.fullProfile.email, - displayName: result.fullProfile.displayName, - }, - providerInfo: {}, - backstageIdentity: { id, idToken }, - }, + response, }); } catch (error) { const { name, message } = isError(error) @@ -105,30 +138,88 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } async logout(_req: express.Request, res: express.Response): Promise { - res.send('noop'); - } - - identifyEnv(): string | undefined { - return undefined; + res.end(); } } +const samlDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const id = info.result.fullProfile.nameID; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id }, + }); + + return { id, token }; +}; + type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -export type SamlProviderOptions = {}; +/** @public */ +export type SamlProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver?: SignInResolver; + }; +}; + +/** @public */ export const createSamlProvider = ( - _options?: SamlProviderOptions, + options?: SamlProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => { - const opts = { + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => { + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: { + email: fullProfile.email, + displayName: fullProfile.displayName, + }, + }); + + const signInResolverFn = + options?.signIn?.resolver ?? samlDefaultSignInResolver; + + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); + + return new SamlAuthProvider({ callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, entryPoint: config.getString('entryPoint'), logoutUrl: config.getOptionalString('logoutUrl'), audience: config.getOptionalString('audience'), issuer: config.getString('issuer'), cert: config.getString('cert'), - privateCert: config.getOptionalString('privateKey'), + privateKey: config.getOptionalString('privateKey'), authnContext: config.getOptionalStringArray('authnContext'), identifierFormat: config.getOptionalString('identifierFormat'), decryptionPvk: config.getOptionalString('decryptionPvk'), @@ -140,8 +231,10 @@ export const createSamlProvider = ( tokenIssuer, appUrl: globalConfig.appUrl, - }; - - return new SamlAuthProvider(opts); + authHandler, + signInResolver, + logger, + catalogIdentityClient, + }); }; }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 089c82b293..dafa52163c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -137,42 +137,92 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; -export type BackstageIdentity = { +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + +/** + * A representation of a successful Backstage sign-in. + * + * Compared to the {@link BackstageIdentityResponse} this type omits + * the decoded identity information embedded in the token. + * + * @public + */ +export interface BackstageSignInResult { /** * An opaque ID that uniquely identifies the user within Backstage. * * This is typically the same as the user entity `metadata.name`. + * + * @deprecated Use the `identity` field instead */ id: string; - /** - * This is deprecated, use `token` instead. - * @deprecated - */ - idToken?: string; - - /** - * The token used to authenticate the user within Backstage. - */ - token?: string; - /** * The entity that the user is represented by within Backstage. * * This entity may or may not exist within the Catalog, and it can be used * to read and store additional metadata about the user. + * + * @deprecated Use the `identity` field instead. */ entity?: Entity; -}; + + /** + * The token used to authenticate the user within Backstage. + */ + token: string; +} + +/** + * The old exported symbol for {@link BackstageSignInResult}. + * @public + * @deprecated Use the `BackstageSignInResult` type instead. + */ +export type BackstageIdentity = BackstageSignInResult; + +/** + * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. + * @public + */ +export interface BackstageIdentityResponse extends BackstageSignInResult { + /** + * A plaintext description of the identity that is encapsulated within the token. + */ + identity: BackstageUserIdentity; +} /** * Used to display login information to user, i.e. sidebar popup. * * It is also temporarily used as the profile of the signed-in user's Backstage * identity, but we want to replace that with data from identity and/org catalog service + * + * @public */ export type ProfileInfo = { /** @@ -209,7 +259,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise; +) => Promise; export type AuthHandlerResult = { profile: ProfileInfo }; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 4aa9ed8778..6a96d421da 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -44,13 +44,10 @@ export interface RouterOptions { providerFactories?: ProviderFactories; } -export async function createRouter({ - logger, - config, - discovery, - database, - providerFactories, -}: RouterOptions): Promise { +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, config, discovery, database, providerFactories } = options; const router = Router(); const appUrl = config.getString('app.baseUrl'); diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 26c64b3f9d..e0e74a9bc6 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-azure-devops-backend +## 0.2.4 + +### Patch Changes + +- a77526afcd: Added getting builds by definition name +- Updated dependencies + - @backstage/backend-common@0.9.13 + - @backstage/plugin-azure-devops-common@0.1.2 + +## 0.2.3 + +### Patch Changes + +- 82cd709fdb: **Backend** + + - Created new `/dashboard-pull-requests/:projectName` endpoint + - Created new `/all-teams` endpoint + - Implemented pull request policy evaluation conversion + + **Frontend** + + - Refactored `PullRequestsPage` and added new properties for `projectName` and `pollingInterval` + - Fixed spacing issue between repo link and creation date in `PullRequestCard` + - Added missing condition to `PullRequestCardPolicy` for `RequiredReviewers` + - Updated `useDashboardPullRequests` hook to implement long polling for pull requests + +- Updated dependencies + - @backstage/plugin-azure-devops-common@0.1.1 + - @backstage/backend-common@0.9.12 + ## 0.2.2 ### Patch Changes diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 19d96dc728..9574215e3e 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -4,6 +4,8 @@ ```ts import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildRun } from '@backstage/plugin-azure-devops-common'; import { Config } from '@backstage/config'; import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import express from 'express'; @@ -23,12 +25,31 @@ export class AzureDevOpsApi { // (undocumented) getAllTeams(): Promise; // (undocumented) + getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise; + // (undocumented) getBuildList( projectName: string, repoId: string, top: number, ): Promise; // (undocumented) + getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ): Promise; + // (undocumented) + getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise; + // (undocumented) getDashboardPullRequests( projectName: string, options: PullRequestOptions, diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 12f62355dd..9741380fb1 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.2.2", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/config": "^0.1.11", - "@backstage/plugin-azure-devops-common": "^0.1.0", + "@backstage/plugin-azure-devops-common": "^0.1.2", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index 2cb7d93c0b..76b5089bbc 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -29,277 +29,510 @@ import { GitPullRequest, GitRepository, } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; +import { + mappedBuildRun, + mappedPullRequest, + mappedRepoBuild, +} from './AzureDevOpsApi'; import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; describe('AzureDevOpsApi', () => { describe('mappedRepoBuild', () => { - it('should return RepoBuild from Build', () => { - const inputBuildDefinition: DefinitionReference = { - name: 'My Build Definition', - }; + describe('mappedRepoBuild happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: inputBuildDefinition, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'My Build Definition - Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with no Build definition name', () => { - it('should return RepoBuild with only Build Number for title', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined status', () => { - it('should return BuildStatus of None for status', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: undefined, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.None, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined result', () => { - it('should return BuildResult of None for result', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined link', () => { - it('should return empty string for link', () => { - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + describe('mappedRepoBuild with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: undefined, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: '', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); }); describe('mappedPullRequest', () => { - it('should return PullRequest from GitPullRequest', () => { - const inputGitRepository: GitRepository = { - name: 'super-feature-repo', - }; + describe('mappedPullRequest happy path', () => { + it('should return PullRequest from GitPullRequest', () => { + const inputGitRepository: GitRepository = { + name: 'super-feature-repo', + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputPullRequest: GitPullRequest = { - pullRequestId: 7181, - repository: inputGitRepository, - title: 'My Awesome New Feature', - createdBy: inputIdentityRef, - creationDate: new Date('2020-09-12T06:10:23.932Z'), - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - }; + const inputPullRequest: GitPullRequest = { + pullRequestId: 7181, + repository: inputGitRepository, + title: 'My Awesome New Feature', + createdBy: inputIdentityRef, + creationDate: new Date('2020-09-12T06:10:23.932Z'), + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + }; - const inputBaseUrl = - 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; + const inputBaseUrl = + 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; - const outputPullRequest: PullRequest = { - pullRequestId: 7181, - repoName: 'super-feature-repo', - title: 'My Awesome New Feature', - uniqueName: 'DOMAIN\\jdoe', - createdBy: 'Jane Doe', - creationDate: '2020-09-12T06:10:23.932Z', - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', - }; + const outputPullRequest: PullRequest = { + pullRequestId: 7181, + repoName: 'super-feature-repo', + title: 'My Awesome New Feature', + uniqueName: 'DOMAIN\\jdoe', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', + }; - expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( - outputPullRequest, - ); + expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( + outputPullRequest, + ); + }); + }); + }); + + describe('mappedBuildRun', () => { + describe('mappedBuildRun happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; + + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); }); }); }); diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 53dc19c68d..6e45a5f4e5 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, DashboardPullRequest, Policy, @@ -35,7 +40,6 @@ import { getArtifactId, } from '../utils'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; @@ -258,6 +262,85 @@ export class AzureDevOpsApi { .map(teamMember => teamMember.identity?.id) .filter((id): id is string => Boolean(id)); } + + public async getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getDefinitions(projectName, definitionName); + } + + public async getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getBuilds( + projectName, + definitions, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + top, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + repoId, + repoId ? 'TfsGit' : undefined, + ); + } + + public async getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ) { + let repoId: string | undefined; + let definitions: number[] | undefined; + + if (repoName) { + const gitRepository = await this.getGitRepository(projectName, repoName); + repoId = gitRepository.id; + } + + if (definitionName) { + const buildDefinitions = await this.getBuildDefinitions( + projectName, + definitionName, + ); + definitions = buildDefinitions + .map(bd => bd.id) + .filter((bd): bd is number => Boolean(bd)); + } + + const builds = await this.getBuilds(projectName, top, repoId, definitions); + + const buildRuns: BuildRun[] = builds.map(mappedBuildRun); + + return buildRuns; + } } export function mappedRepoBuild(build: Build): RepoBuild { @@ -295,3 +378,20 @@ export function mappedPullRequest( link: `${linkBaseUrl}/${pullRequest.pullRequestId}`, }; } + +export function mappedBuildRun(build: Build): BuildRun { + return { + id: build.id, + title: [build.definition?.name, build.buildNumber] + .filter(Boolean) + .join(' - '), + link: build._links?.web.href ?? '', + status: build.status ?? BuildStatus.None, + result: build.result ?? BuildResult.None, + queueTime: build.queueTime?.toISOString(), + startTime: build.startTime?.toISOString(), + finishTime: build.finishTime?.toISOString(), + source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + uniqueName: build.requestedFor?.uniqueName ?? 'N/A', + }; +} diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index ce5f712392..87135781e6 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, PullRequest, PullRequestStatus, @@ -23,7 +28,6 @@ import { } from '@backstage/plugin-azure-devops-common'; import { AzureDevOpsApi } from '../api'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { ConfigReader } from '@backstage/config'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { createRouter } from './router'; @@ -39,8 +43,12 @@ describe('createRouter', () => { azureDevOpsApi = { getGitRepository: jest.fn(), getBuildList: jest.fn(), + getBuildDefinitions: jest.fn(), getRepoBuilds: jest.fn(), + getDefinitionBuilds: jest.fn(), getPullRequests: jest.fn(), + getBuilds: jest.fn(), + getBuildRuns: jest.fn(), } as any; const router = await createRouter({ azureDevOpsApi, @@ -260,4 +268,142 @@ describe('createRouter', () => { expect(response.body).toEqual(pullRequests); }); }); + + describe('GET /build-definitions/:projectName/:definitionName', () => { + it('fetches a list of build definitions', async () => { + const inputDefinition: BuildDefinitionReference = { + id: 1, + name: 'myBuildDefinition', + }; + + const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; + + azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( + inputDefinitions, + ); + + const response = await request(app).get( + '/build-definitions/myProject/myBuildDefinition', + ); + + expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( + 'myProject', + 'myBuildDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(inputDefinitions); + }); + }); + + describe('GET /builds/:projectName', () => { + describe('GET /builds/:projectName with repoName', () => { + it('fetches a list of build runs using repoName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', repoName: 'myRepo' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + 'myRepo', + undefined, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + + describe('GET /builds/:projectName with definitionName', () => { + it('fetches a list of build runs using definitionName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', definitionName: 'myDefinition' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + undefined, + 'myDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + }); }); diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 57d54df18f..99c140a424 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -144,6 +144,32 @@ export async function createRouter( res.status(200).json(allTeams); }); + router.get( + '/build-definitions/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( + projectName, + definitionName, + ); + res.status(200).json(buildDefinitionList); + }, + ); + + router.get('/builds/:projectName', async (req, res) => { + const { projectName } = req.params; + const repoName = req.query.repoName?.toString(); + const definitionName = req.query.definitionName?.toString(); + const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const builds = await azureDevOpsApi.getBuildRuns( + projectName, + top, + repoName, + definitionName, + ); + res.status(200).json(builds); + }); + router.use(errorHandler()); return router; } diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md index 4a834fb068..b2c8645e94 100644 --- a/plugins/azure-devops-common/CHANGELOG.md +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-azure-devops-common +## 0.1.2 + +### Patch Changes + +- a77526afcd: Added getting builds by definition name + +## 0.1.1 + +### Patch Changes + +- 0749dd0307: feat: Created pull request card component and initial pull request dashboard page. + ## 0.1.0 ### Minor Changes diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 17cccb05cb..bb458f169a 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -14,6 +14,29 @@ export enum BuildResult { Succeeded = 2, } +// Warning: (ae-missing-release-tag) "BuildRun" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +// Warning: (ae-missing-release-tag) "BuildRunOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRunOptions = { + top?: number; +}; + // Warning: (ae-missing-release-tag) "BuildStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index fe9df644b1..7b82296c40 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-common", - "version": "0.1.0", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.1" }, "files": [ "dist" diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index c87d8641ad..0befcf0b60 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -254,3 +254,19 @@ export enum PullRequestVoteStatus { WaitingForAuthor = -5, Rejected = -10, } +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +export type BuildRunOptions = { + top?: number; +}; diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 0bcd9bc686..57f80f508b 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-azure-devops +## 0.1.6 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/plugin-azure-devops-common@0.1.2 + +## 0.1.5 + +### Patch Changes + +- 0749dd0307: feat: Created pull request card component and initial pull request dashboard page. +- 82cd709fdb: **Backend** + + - Created new `/dashboard-pull-requests/:projectName` endpoint + - Created new `/all-teams` endpoint + - Implemented pull request policy evaluation conversion + + **Frontend** + + - Refactored `PullRequestsPage` and added new properties for `projectName` and `pollingInterval` + - Fixed spacing issue between repo link and creation date in `PullRequestCard` + - Added missing condition to `PullRequestCardPolicy` for `RequiredReviewers` + - Updated `useDashboardPullRequests` hook to implement long polling for pull requests + +- Updated dependencies + - @backstage/plugin-azure-devops-common@0.1.1 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.1.4 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 2badcf63b5..a37259739f 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,27 +28,28 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.4", - "@backstage/plugin-azure-devops-common": "^0.1.0", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-azure-devops-common": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "humanize-duration": "^3.27.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 736d079969..e1cdfa1490 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-badges-backend +## 0.1.13 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/backend-common@0.9.12 + ## 0.1.12 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 6f44e96b37..c481866a0a 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 449352f87b..049709e8be 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-badges +## 0.2.16 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.15 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index be38144ada..441f72c550 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.15", + "version": "0.2.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,24 +28,25 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 812ae8f5e3..2646731535 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-bazaar-backend +## 0.1.4 + +### Patch Changes + +- 210fcf63ee: Handle migration error when old data is present in the database +- Updated dependencies + - @backstage/backend-common@0.9.13 + ## 0.1.3 ### Patch Changes diff --git a/plugins/bazaar-backend/migrations/20211020073922_add_columns.js b/plugins/bazaar-backend/migrations/20211020073922_add_columns.js index d4cb87bd82..ea3a89ea72 100644 --- a/plugins/bazaar-backend/migrations/20211020073922_add_columns.js +++ b/plugins/bazaar-backend/migrations/20211020073922_add_columns.js @@ -29,6 +29,7 @@ exports.up = async function up(knex) { .comment('Optional end date of the project (ISO 8601 format)'); table .text('responsible') + .defaultTo('') .notNullable() .comment('Contact person of the project'); }); diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 9c6426fcba..aefe3bfdc6 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", - "@backstage/backend-test-utils": "^0.1.9", + "@backstage/backend-common": "^0.9.13", + "@backstage/backend-test-utils": "^0.1.10", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1" + "@backstage/cli": "^0.10.1" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 8988bf5d62..664b69a4a4 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-bazaar +## 0.1.6 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog@0.7.4 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/cli@0.10.1 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/cli@0.10.0 + - @backstage/core-plugin-api@0.2.2 + ## 0.1.4 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index bb45a7b974..3db79b9cdf 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/cli": "^0.9.0", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.7.3", - "@backstage/plugin-catalog-react": "^0.6.4", + "@backstage/cli": "^0.10.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/plugin-catalog-react": "^0.6.5", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,15 +34,16 @@ "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.13.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/dev-utils": "^0.2.13", + "@backstage/cli": "^0.10.1", + "@backstage/dev-utils": "^0.2.14", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" }, diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 874cbe2c6c..43c2ba1ac6 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-bitrise +## 0.1.19 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.1.18 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.1.17 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 3af16837de..35a0a9e079 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.17", + "version": "0.1.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -33,16 +33,17 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 9e769af873..b8e48924dc 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.19.0 + ## 0.3.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 36cacb6660..2817a0e209 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.7", + "version": "0.3.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", - "@backstage/plugin-catalog-backend": "^0.18.0", + "@backstage/plugin-catalog-backend": "^0.19.0", "@backstage/types": "^0.1.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -40,7 +40,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index c2225b8945..3db08d02fc 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.11 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/plugin-catalog-backend@0.19.0 + ## 0.2.10 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 391a7125de..8e9b99931c 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.10", + "version": "0.2.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "@azure/msal-node": "^1.1.0", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/plugin-catalog-backend": "^0.18.0", + "@backstage/plugin-catalog-backend": "^0.19.0", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", @@ -42,8 +42,8 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.11", - "@backstage/cli": "^0.9.1", + "@backstage/backend-common": "^0.9.12", + "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.23", "@types/lodash": "^4.14.151", "msw": "^0.35.0" diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index ce19314a50..dc7fc50f76 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-catalog-backend +## 0.19.1 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- 6bccc7d794: The `pagedRequest` method in the GitLab ingestion client is now public for re-use and may be used to make other calls to the GitLab API. Developers can now pass in a type into the GitLab `paginated` and `pagedRequest` functions as generics instead of forcing `any` (defaults to `any` to maintain compatibility). The `GitLabClient` now provides a `isSelfManaged` convenience method. +- 0e4daaa753: Reject catalog entities that have duplicate fields that vary only in casing. +- 98a9c35f0c: Honor database migration configuration +- Updated dependencies + - @backstage/backend-common@0.9.13 + +## 0.19.0 + +### Minor Changes + +- 905dd952ac: **BREAKING** `DefaultCatalogCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: + + ```diff + // packages/backend/src/plugins/search.ts + + ... + export default async function createPlugin({ + ... + + tokenManager, + }: PluginEnvironment) { + ... + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: DefaultCatalogCollator.fromConfig(config, { + discovery, + + tokenManager, + }), + }); + + ... + } + ``` + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/backend-common@0.9.12 + ## 0.18.0 ### Minor Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 43eb8d61b7..f773905ca0 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -752,13 +752,7 @@ export type DbPageInfo = // // @public (undocumented) export class DefaultCatalogCollator implements DocumentCollator { - constructor({ - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - }: { + constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 0628969ea3..6cfb73a3d1 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.18.0", + "version": "0.19.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@backstage/search-common": "^0.2.1", "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", @@ -62,9 +62,9 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/backend-test-utils": "^0.1.10", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts new file mode 100644 index 0000000000..b249137740 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts @@ -0,0 +1,268 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { readGitLabIntegrationConfig } from '@backstage/integration'; +import { getVoidLogger } from '@backstage/backend-common'; +import { rest } from 'msw'; +import { setupServer, SetupServerApi } from 'msw/node'; + +import { GitLabClient, paginated } from './client'; + +const server = setupServer(); +setupRequestMockHandlers(server); + +const MOCK_CONFIG = readGitLabIntegrationConfig( + new ConfigReader({ + host: 'example.com', + token: 'test-token', + apiBaseUrl: 'https://example.com/api/v4', + }), +); +const FAKE_PAGED_ENDPOINT = `/some-endpoint`; +const FAKE_PAGED_URL = `${MOCK_CONFIG.apiBaseUrl}${FAKE_PAGED_ENDPOINT}`; + +function setupFakeFourPageURL(srv: SetupServerApi, url: string) { + srv.use( + rest.get(url, (req, res, ctx) => { + const page = req.url.searchParams.get('page'); + const currentPage = page ? Number(page) : 1; + const fakePageCount = 4; + + return res( + // set next page number header if page requested is less than count + ctx.set( + 'x-next-page', + currentPage < fakePageCount ? String(currentPage + 1) : '', + ), + ctx.json([{ someContentOfPage: currentPage }]), + ); + }), + ); +} + +function setupFakeGroupProjectsEndpoint( + srv: SetupServerApi, + apiBaseUrl: string, + groupID: string, +) { + srv.use( + rest.get(`${apiBaseUrl}/groups/${groupID}/projects`, (_, res, ctx) => { + return res( + ctx.set('x-next-page', ''), + ctx.json([ + { + id: 1, + description: 'Project One Description', + name: 'Project One', + path: 'project-one', + }, + ]), + ); + }), + ); +} + +function setupFakeInstanceProjectsEndpoint( + srv: SetupServerApi, + apiBaseUrl: string, +) { + srv.use( + rest.get(`${apiBaseUrl}/projects`, (_, res, ctx) => { + return res( + ctx.set('x-next-page', ''), + ctx.json([ + { + id: 1, + description: 'Project One Description', + name: 'Project One', + path: 'project-one', + }, + { + id: 2, + description: 'Project Two Description', + name: 'Project Two', + path: 'project-two', + }, + ]), + ); + }), + ); +} + +describe('GitLabClient', () => { + describe('isSelfManaged', () => { + it('returns true if self managed instance', () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader({ + host: 'example.com', + token: 'test-token', + apiBaseUrl: 'https://example.com/api/v4', + }), + ), + logger: getVoidLogger(), + }); + expect(client.isSelfManaged()).toBeTruthy(); + }); + it('returns false if gitlab.com', () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.com', + token: 'test-token', + }), + ), + logger: getVoidLogger(), + }); + expect(client.isSelfManaged()).toBeFalsy(); + }); + }); + + describe('pagedRequest', () => { + beforeEach(() => { + // setup fake paginated endpoint with 4 pages each returning one item + setupFakeFourPageURL(server, FAKE_PAGED_URL); + }); + + it('should provide immediate items within the page', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const { items } = await client.pagedRequest(FAKE_PAGED_ENDPOINT); + // fake page contains exactly one item + expect(items).toHaveLength(1); + }); + + it('should request items for a given page number', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const requestedPage = 2; + const { items, nextPage } = await client.pagedRequest( + FAKE_PAGED_ENDPOINT, + { + page: requestedPage, + }, + ); + // should contain an item from a given page + expect(items[0].someContentOfPage).toEqual(requestedPage); + // should set the nextPage property to the next page + expect(nextPage).toEqual(3); + }); + + it('should not have a next page if at the end', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const { items, nextPage } = await client.pagedRequest( + FAKE_PAGED_ENDPOINT, + { + page: 4, + }, + ); + // should contain item of last page + expect(items).toHaveLength(1); + expect(nextPage).toBeNull(); + }); + + it('should throw if response is not okay', async () => { + const endpoint = '/unhealthy-endpoint'; + const url = `${MOCK_CONFIG.apiBaseUrl}${endpoint}`; + server.use( + rest.get(url, (_, res, ctx) => { + return res(ctx.status(400), ctx.json({ error: 'some error' })); + }), + ); + + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + // non-200 status code should throw + await expect(() => client.pagedRequest(endpoint)).rejects.toThrowError(); + }); + }); + + describe('listProjects', () => { + it('should get projects for a given group', async () => { + setupFakeGroupProjectsEndpoint( + server, + MOCK_CONFIG.apiBaseUrl, + 'test-group', + ); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const groupProjectsGen = paginated( + options => client.listProjects(options), + { group: 'test-group' }, + ); + const allItems = []; + for await (const item of groupProjectsGen) { + allItems.push(item); + } + expect(allItems).toHaveLength(1); + }); + + it('should get all projects for an instance', async () => { + setupFakeInstanceProjectsEndpoint(server, MOCK_CONFIG.apiBaseUrl); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const instanceProjects = paginated( + options => client.listProjects(options), + {}, + ); + const allProjects = []; + for await (const project of instanceProjects) { + allProjects.push(project); + } + expect(allProjects).toHaveLength(2); + }); + }); +}); + +describe('paginated', () => { + it('should iterate through the pages until exhausted', async () => { + setupFakeFourPageURL(server, FAKE_PAGED_URL); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const paginatedItems = paginated( + options => client.pagedRequest(FAKE_PAGED_ENDPOINT, options), + {}, + ); + const allItems = []; + for await (const item of paginatedItems) { + allItems.push(item); + } + + expect(allItems).toHaveLength(4); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index d1765521a3..4d42f5e068 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -21,6 +21,18 @@ import { } from '@backstage/integration'; import { Logger } from 'winston'; +export type ListOptions = { + [key: string]: string | number | boolean | undefined; + group?: string; + per_page?: number | undefined; + page?: number | undefined; +}; + +export type PagedResponse = { + items: T[]; + nextPage?: number; +}; + export class GitLabClient { private readonly config: GitLabIntegrationConfig; private readonly logger: Logger; @@ -30,12 +42,17 @@ export class GitLabClient { this.logger = options.logger; } + /** + * Indicates whether the client is for a SaaS or self managed GitLab instance. + */ + isSelfManaged(): boolean { + return this.config.host !== 'gitlab.com'; + } + async listProjects(options?: ListOptions): Promise> { if (options?.group) { return this.pagedRequest( - `${this.config.apiBaseUrl}/groups/${encodeURIComponent( - options?.group, - )}/projects`, + `/groups/${encodeURIComponent(options?.group)}/projects`, { ...options, include_subgroups: true, @@ -43,14 +60,26 @@ export class GitLabClient { ); } - return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); + return this.pagedRequest(`/projects`, options); } - private async pagedRequest( + /** + * Performs a request against a given paginated GitLab endpoint. + * + * This method may be used to perform authenticated REST calls against any + * paginated GitLab endpoint which uses X-NEXT-PAGE headers. The return value + * can be be used with the {@link paginated} async-generator function to yield + * each item from the paged request. + * + * @see {@link paginated} + * @param endpoint - The request endpoint, e.g. /projects. + * @param options - Request queryString options which may also include page variables. + */ + async pagedRequest( endpoint: string, options?: ListOptions, - ): Promise> { - const request = new URL(endpoint); + ): Promise> { + const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); for (const key in options) { if (options[key]) { request.searchParams.append(key, options[key]!.toString()); @@ -80,20 +109,20 @@ export class GitLabClient { } } -export type ListOptions = { - [key: string]: string | number | boolean | undefined; - group?: string; - per_page?: number | undefined; - page?: number | undefined; -}; - -export type PagedResponse = { - items: T[]; - nextPage?: number; -}; - -export async function* paginated( - request: (options: ListOptions) => Promise>, +/** + * Advances through each page and provides each item from a paginated request. + * + * The async generator function yields each item from repeated calls to the + * provided request function. The generator walks through each available page by + * setting the page key in the options passed into the request function and + * making repeated calls until there are no more pages. + * + * @see {@link pagedRequest} + * @param request - Function which returns a PagedResponse to walk through. + * @param options - Initial ListOptions for the request function. + */ +export async function* paginated( + request: (options: ListOptions) => Promise>, options: ListOptions, ) { let res; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 4c6342c6c6..ebad95e72f 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -56,19 +56,16 @@ export class DefaultCatalogCollator implements DocumentCollator { }); } - constructor({ - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - }: { + constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; filter?: CatalogEntitiesRequest['filter']; catalogClient?: CatalogApi; }) { + const { discovery, locationTemplate, filter, catalogClient, tokenManager } = + options; + this.discovery = discovery; this.locationTemplate = locationTemplate || '/catalog/:namespace/:kind/:name'; diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index c9ebc8f723..1618ef8d7b 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -336,7 +336,10 @@ export class NextCatalogBuilder { const parser = this.parser || defaultEntityDataParser; const dbClient = await database.getClient(); - await applyDatabaseMigrations(dbClient); + if (!database.migrations?.skip) { + logger.info('Performing database migration'); + await applyDatabaseMigrations(dbClient); + } const db = new CommonDatabase(dbClient, logger); diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index 507f3aa53e..b4a693a4fa 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -209,6 +209,11 @@ export class Stitcher { entity.metadata.etag = hash; } + // This may throw if the entity is invalid, so we call it before + // the final_entites write, even though we may end up not needing + // to write the search index. + const searchEntries = buildEntitySearch(entityId, entity); + const rowsChanged = await this.database( 'final_entities', ) @@ -235,7 +240,6 @@ export class Stitcher { // B writes the entity -> // B writes search -> // A writes search - const searchEntries = buildEntitySearch(entityId, entity); await this.database('search') .where({ entity_id: entityId }) .delete(); diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts index 9043dab9ba..58551a461f 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts @@ -163,5 +163,51 @@ describe('buildEntitySearch', () => { { entity_id: 'eid', key: 'relations.t2', value: 'k:ns/b' }, ]); }); + + it('rejects duplicate keys', () => { + expect(() => + buildEntitySearch('eid', { + relations: [], + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'n', + Namespace: 'ns', + }, + }), + ).toThrow( + `Entity has duplicate keys that vary only in casing, 'metadata.namespace'`, + ); + + expect(() => + buildEntitySearch('eid', { + relations: [ + { type: 'dup', target: { kind: 'k', namespace: 'ns', name: 'a' } }, + { type: 'DUP', target: { kind: 'k', namespace: 'ns', name: 'b' } }, + ], + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + }), + ).toThrow( + `Entity has duplicate keys that vary only in casing, 'relations.dup'`, + ); + + expect(() => + buildEntitySearch('eid', { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + spec: { + owner: 'o', + OWNER: 'o', + lifecycle: 'production', + lifeCycle: 'production', + }, + }), + ).toThrow( + `Entity has duplicate keys that vary only in casing, 'spec.owner', 'spec.lifecycle'`, + ); + }); }); }); diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts index ed84dfa018..b6c3eead2a 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts @@ -19,6 +19,7 @@ import { ENTITY_DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; +import { InputError } from '@backstage/errors'; import { DbSearchRow } from '../database/tables'; // These are excluded in the generic loop, either because they do not make sense @@ -184,5 +185,23 @@ export function buildEntitySearch( }); } + // This validates that there are no keys that vary only in casing, such + // as `spec.foo` and `spec.Foo`. + const keys = new Set(raw.map(r => r.key)); + const lowerKeys = new Set(raw.map(r => r.key.toLocaleLowerCase('en-US'))); + if (keys.size !== lowerKeys.size) { + const difference = []; + for (const key of keys) { + const lower = key.toLocaleLowerCase('en-US'); + if (!lowerKeys.delete(lower)) { + difference.push(lower); + } + } + const badKeys = `'${difference.join("', '")}'`; + throw new InputError( + `Entity has duplicate keys that vary only in casing, ${badKeys}`, + ); + } + return mapToRows(raw, entityId); } diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index c107a4e758..50842898df 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-graph +## 0.2.3 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 16c942de8b..b421a15265 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,28 +23,29 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.3.1", "lodash": "^4.17.15", "p-limit": "^3.1.0", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index e123f3b17f..fe6cc4cc00 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-graphql +## 0.2.14 + +### Patch Changes + +- 6b500622d5: Move to using node-fetch internally instead of cross-fetch + ## 0.2.13 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 44c820858d..9ed6918110 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.22", "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 6397718fd7..0fae63ff9d 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.7.5 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.7.4 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index d46495a58b..d417a337df 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.7.4", + "version": "0.7.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,32 +33,33 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog-react": "^0.6.4", + "@backstage/integration": "^0.6.10", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog-react": "^0.6.5", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@types/react": "*", "git-url-parse": "^11.6.0", "js-base64": "^3.6.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.12.2", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "yaml": "^1.10.0" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 276b6d351c..6154eec757 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -79,6 +79,9 @@ describe('CatalogImportClient', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const scmIntegrationsApi = ScmIntegrations.fromConfig( diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 784cbfbe9e..d6132b249e 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -37,6 +37,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 1778af73dc..498e786eb3 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -43,6 +43,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index e6d00b2f8f..c628735ef1 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-react +## 0.6.5 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- 69034b4419: Fix typo in catalog-react package.json +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/core-app-api@0.2.0 + - @backstage/version-bridge@0.1.1 + ## 0.6.4 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 2ae853650a..b5a6884fa5 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.4", + "version": "0.6.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -15,7 +15,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugins/plugin-catalog-common-react" + "directory": "plugins/catalog-react" }, "keywords": [ "backstage" @@ -31,28 +31,30 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-app-api": "^0.2.0", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.4", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.0", + "@backstage/version-bridge": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "jwt-decode": "^3.1.0", "lodash": "^4.17.21", "qs": "^6.9.4", - "react": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 7fda738d32..e0a6dd857a 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog +## 0.7.4 + +### Patch Changes + +- 9603827bb5: Addressed some peer dependency warnings +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.7.3 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e0218090bd..57e14b5475 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.7.3", + "version": "0.7.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,28 +33,30 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", + "history": "^5.0.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 0e9da8d8c8..6917b5abed 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -40,6 +40,9 @@ const identityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const guestIdentityApi: IdentityApi = { getUserId() { @@ -54,6 +57,9 @@ const guestIdentityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; describe('CatalogClientWrapper', () => { diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 3f3fc804e1..a45afe6e02 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -31,9 +31,8 @@ import { renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { EntityLayout } from './EntityLayout'; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx index 9681420e1c..ad111380f8 100644 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx @@ -16,8 +16,7 @@ import React from 'react'; import { Tabbed } from './Tabbed'; import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act, fireEvent } from '@testing-library/react'; import { Routes, Route } from 'react-router'; describe('Tabbed layout', () => { diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 18e227627b..8137294300 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-circleci +## 0.2.31 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- 56d04330c4: Switch to using `LogViewer` component from `@backstage/core-components` to display action output. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.2.30 + +### Patch Changes + +- 5a46712a7d: Fixed a bug in the `CircleCI` plugin where restarting builds was hard-coded to GitHub rather than introspecting the entity source location. +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.2.29 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 6f3eaa39e3..e2aa415043 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.2.29", + "version": "0.2.31", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,25 +44,24 @@ "humanize-duration": "^3.27.0", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/humanize-duration": "^3.25.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react-lazylog": "^4.5.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx index a4c2af241f..4a4a8a45f2 100644 --- a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { Accordion, AccordionDetails, @@ -24,10 +24,9 @@ import { import { makeStyles } from '@material-ui/core/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { BuildStepAction } from 'circleci-api'; -import React, { Suspense, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { durationHumanized } from '../../../../util'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); const useStyles = makeStyles({ accordionDetails: { padding: 0, @@ -85,11 +84,9 @@ export const ActionOutput = ({ {messages.length === 0 ? ( 'Nothing here...' ) : ( - }> -
- -
-
+
+ +
)} diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 735f5d2167..04963a6fbb 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cloudbuild +## 0.2.29 + +### Patch Changes + +- c77def982f: Remove unnecessary dependency. +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.28 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index f66dcbed00..6e91b22fcb 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.2.28", + "version": "0.2.29", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,27 +32,27 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index f1175172f5..387e86bc46 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-code-coverage-backend +## 0.1.16 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/backend-common@0.9.12 + ## 0.1.15 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 1bc637146b..83a0acf9c2 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.8", + "@backstage/integration": "^0.6.10", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -36,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index b5b413b9d2..777dc6d5df 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-code-coverage +## 0.1.19 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.18 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 3a45960684..951a93110a 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.18", + "version": "0.1.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,29 +23,30 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.11.0", "highlight.js": "^10.6.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index fc4f8697b8..d4999dc573 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-config-schema +## 0.1.15 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md index efeb1646eb..f3b620738f 100644 --- a/plugins/config-schema/api-report.md +++ b/plugins/config-schema/api-report.md @@ -41,11 +41,9 @@ export const configSchemaPlugin: BackstagePlugin< {} >; -// Warning: (ae-missing-release-tag) "StaticSchemaLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class StaticSchemaLoader implements ConfigSchemaApi { - constructor({ url }?: { url?: string }); + constructor(options?: { url?: string }); // (undocumented) schema$(): Observable; } diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 0d0357cd7c..59d34cc00d 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.14", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,25 +22,26 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.5", - "@backstage/theme": "^0.2.13", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "jsonschema": "^1.2.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/src/api/StaticSchemaLoader.ts b/plugins/config-schema/src/api/StaticSchemaLoader.ts index 71eae63376..5fc979b76d 100644 --- a/plugins/config-schema/src/api/StaticSchemaLoader.ts +++ b/plugins/config-schema/src/api/StaticSchemaLoader.ts @@ -24,12 +24,14 @@ const DEFAULT_URL = 'config-schema.json'; /** * A ConfigSchemaApi implementation that loads the configuration from a URL. + * + * @public */ export class StaticSchemaLoader implements ConfigSchemaApi { private readonly url: string; - constructor({ url = DEFAULT_URL }: { url?: string } = {}) { - this.url = url; + constructor(options: { url?: string } = {}) { + this.url = options?.url ?? DEFAULT_URL; } schema$(): Observable { diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index c43d7c0612..183cc27dcf 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.11.13 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.11.12 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 12133fcc64..45975163e4 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.11.12", + "version": "0.11.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,33 +32,34 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.9.6", - "@types/react": "*", "@types/recharts": "^1.8.14", "classnames": "^2.2.6", "history": "^5.0.0", "luxon": "^2.0.2", "pluralize": "^8.0.0", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5", "regression": "^2.0.1", "yup": "^0.32.9" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 09d2feb71b..fb18c1a68c 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -187,6 +187,9 @@ export const MockCostInsightsApiProvider = ({ getIdToken: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const defaultCostInsightsApi: CostInsightsApi = { diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 0560527f80..0123dcd77a 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-explore-react +## 0.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.3.0 + ## 0.0.7 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index a67f8b6892..ad7f92751e 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.7", + "version": "0.0.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core-plugin-api": "^0.2.0" + "@backstage/core-plugin-api": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.10.1", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 73117d7f32..fce849b17d 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-explore +## 0.3.22 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/plugin-explore-react@0.0.8 + ## 0.3.21 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 8988a7f215..9975992ea5 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.21", + "version": "0.3.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,27 +32,28 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/plugin-explore-react": "^0.0.7", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-explore-react": "^0.0.8", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.2.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index b137ed1f73..6cb869284e 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-firehydrant +## 0.1.9 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.8 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 421ec10c1c..68ee004605 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,23 +22,24 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^1.27.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index af99e13c4a..b63911f5db 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-fossa +## 0.2.24 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.2.23 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.2.22 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 3731d01661..b28c8bd89f 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.22", + "version": "0.2.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,26 +33,27 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "cross-fetch": "^3.0.6", "luxon": "^2.0.2", "p-limit": "^3.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index 4debb37ad6..e723a4b00a 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -24,20 +24,11 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); -const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, +const identityApi = { async getIdToken() { return Promise.resolve('fake-id-token'); }, - async signOut() { - return Promise.resolve(); - }, -}; +} as IdentityApi; describe('FossaClient', () => { setupRequestMockHandlers(server); diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index ac8dd570a6..7d832c5a4b 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gcp-projects +## 0.3.10 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.3.9 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 3068119de5..e73dc7736a 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,22 +31,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "^6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 1c627ab086..c4e50a250b 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.3.4 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.3.3 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index fd1ef75ccc..ab11255e08 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.3", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,28 +21,29 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/integration": "^0.6.10", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@types/react": "*", "luxon": "^2.0.2", "qs": "^6.10.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 7992f201cf..4c4a339415 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.4.26 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- cbd20c46f1: Switch to using `LogViewer` component from `@backstage/core-components` to display build logs. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.4.25 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index ea5f104c96..434a0b6224 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.25", + "version": "0.4.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,28 +34,28 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/integration": "^0.6.10", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index 399fed9557..0f1670bbbc 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { @@ -32,14 +32,11 @@ import { } from '@material-ui/core'; import DescriptionIcon from '@material-ui/icons/Description'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import React, { Suspense } from 'react'; +import React from 'react'; import { useProjectName } from '../useProjectName'; import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); -const LinePart = React.lazy(() => import('react-lazylog/build/LinePart')); - -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ button: { order: -1, marginRight: 0, @@ -53,49 +50,19 @@ const useStyles = makeStyles(() => ({ justifyContent: 'center', margin: 'auto', }, - normalLog: { + normalLogContainer: { height: '75vh', width: '100%', }, - modalLog: { + modalLogContainer: { height: '100%', width: '100%', }, + log: { + background: theme.palette.background.default, + }, })); -const DisplayLog = ({ - jobLogs, - className, -}: { - jobLogs: any; - className: string; -}) => { - return ( - }> -
- { - if ( - line.toLocaleLowerCase().includes('error') || - line.toLocaleLowerCase().includes('failed') || - line.toLocaleLowerCase().includes('failure') - ) { - return ( - - ); - } - return line; - }} - /> -
-
- ); -}; - /** * A component for Run Logs visualization. */ @@ -123,6 +90,7 @@ export const WorkflowRunLogs = ({ repo, id: runId, }); + const logText = jobLogs.value ? String(jobLogs.value) : undefined; const [open, setOpen] = React.useState(false); const handleOpen = () => { @@ -162,18 +130,19 @@ export const WorkflowRunLogs = ({ onClose={handleClose} > - +
+ +
- {jobLogs.value && ( - + {logText && ( +
+ +
)} ); diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 701b167135..767fd90780 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-deployments +## 0.1.22 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.21 ### Patch Changes diff --git a/plugins/github-deployments/api-report.md b/plugins/github-deployments/api-report.md index a8fca185e5..36680cb020 100644 --- a/plugins/github-deployments/api-report.md +++ b/plugins/github-deployments/api-report.md @@ -38,11 +38,7 @@ function createStatusColumn(): TableColumn; // Warning: (ae-missing-release-tag) "EntityGithubDeploymentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityGithubDeploymentsCard: ({ - last, - lastStatuses, - columns, -}: { +export const EntityGithubDeploymentsCard: (props: { last?: number | undefined; lastStatuses?: number | undefined; columns?: TableColumn[] | undefined; @@ -58,12 +54,9 @@ export const githubDeploymentsPlugin: BackstagePlugin<{}, {}>; // Warning: (ae-missing-release-tag) "GithubDeploymentsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function GithubDeploymentsTable({ - deployments, - isLoading, - reload, - columns, -}: GithubDeploymentsTableProps): JSX.Element; +export function GithubDeploymentsTable( + props: GithubDeploymentsTableProps, +): JSX.Element; // @public (undocumented) export namespace GithubDeploymentsTable { @@ -78,7 +71,7 @@ export namespace GithubDeploymentsTable { // Warning: (ae-missing-release-tag) "GithubStateIndicator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const GithubStateIndicator: ({ state }: { state: string }) => JSX.Element; +const GithubStateIndicator: (props: { state: string }) => JSX.Element; // Warning: (ae-missing-release-tag) "isGithubDeploymentsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 18c3cc3d05..1965f98ef1 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.21", + "version": "0.1.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,27 +22,28 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/integration": "^0.6.10", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/graphql": "^4.5.8", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index c01cd98a45..c1c33607a1 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -80,15 +80,12 @@ const GithubDeploymentsComponent = ({ ); }; -export const GithubDeploymentsCard = ({ - last, - lastStatuses, - columns, -}: { +export const GithubDeploymentsCard = (props: { last?: number; lastStatuses?: number; columns?: TableColumn[]; }) => { + const { last, lastStatuses, columns } = props; const { entity } = useEntity(); const [host] = [ entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 2de7cdf419..2c48f3eef1 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -36,12 +36,8 @@ type GithubDeploymentsTableProps = { columns: TableColumn[]; }; -export function GithubDeploymentsTable({ - deployments, - isLoading, - reload, - columns, -}: GithubDeploymentsTableProps) { +export function GithubDeploymentsTable(props: GithubDeploymentsTableProps) { + const { deployments, isLoading, reload, columns } = props; const classes = useStyles(); return ( diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index e892b12427..9a720c0e75 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -27,8 +27,8 @@ import { Link, } from '@backstage/core-components'; -export const GithubStateIndicator = ({ state }: { state: string }) => { - switch (state) { +export const GithubStateIndicator = (props: { state: string }) => { + switch (props.state) { case 'PENDING': return ; case 'IN_PROGRESS': diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 42f3533d31..69ece49c63 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gitops-profiles +## 0.3.10 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.3.9 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 8456eb4919..834fd2f7cf 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,22 +32,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 4fbf6b0dab..2a013aa6b3 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-graphiql +## 0.2.24 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + +## 0.2.23 + +### Patch Changes + +- dde1681f33: chore(dependencies): bump `graphiql` package to latest + ## 0.2.22 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index a04f4d6992..ec5a37bf36 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.22", + "version": "0.2.24", "private": false, "publishConfig": { "access": "public", @@ -31,23 +31,24 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "graphiql": "^1.0.0-alpha.10", - "graphql": "15.5.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "graphiql": "^1.5.12", + "graphql": "^16.0.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx index ae84b549ec..90f8edd4ad 100644 --- a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { GraphiQLPage } from './GraphiQLPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { GraphQLBrowseApi, graphQlBrowseApiRef } from '../../lib/api'; import { configApiRef } from '@backstage/core-plugin-api'; diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index 335545021e..5b505ee828 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.8", - "@backstage/plugin-catalog-graphql": "^0.2.12", + "@backstage/plugin-catalog-graphql": "^0.2.14", "@graphql-modules/core": "^0.7.17", "@types/express": "^4.17.6", "apollo-server": "^2.16.1", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.35.0", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 09e379a625..644d319b70 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-home +## 0.4.7 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.4.6 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 65f6fe2ee3..b04e4a787d 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.6", + "version": "0.4.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,24 +21,25 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 1bff4b22ef..a0698bf4a3 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-ilert +## 0.1.18 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.17 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 4dddf17cd9..5c07bc9d21 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.17", + "version": "0.1.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,15 +34,16 @@ "@material-ui/pickers": "^3.3.10", "humanize-duration": "^3.26.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 5cc91e0d5d..19d743b250 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-jenkins-backend +## 0.1.9 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/backend-common@0.9.12 + ## 0.1.8 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 4db26d94c4..db4298e6c2 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 1daba68a1d..c59c7581af 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins +## 0.5.14 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.5.13 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 18341f6eec..2211bf54de 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.5.13", + "version": "0.5.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,25 +33,26 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index a8f64b21e1..7fb5578628 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -32,7 +32,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.5", @@ -44,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 33ec4bd95a..be4f2d2a29 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kafka +## 0.2.22 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.21 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 8da053ab50..309e239a68 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.2.21", + "version": "0.2.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,23 +22,24 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 102c9c13e4..ccd7ce7194 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-kubernetes-backend +## 0.4.0 + +### Minor Changes + +- c010632f88: Add pod metrics lookup and display in pod table. + + ## Backwards incompatible changes + + If your Kubernetes distribution does not have the [metrics server](https://github.com/kubernetes-sigs/metrics-server) installed, + you will need to set the `skipMetricsLookup` config flag to `false`. + + See the [configuration docs](https://backstage.io/docs/features/kubernetes/configuration) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.2.0 + - @backstage/backend-common@0.9.13 + +## 0.3.20 + +### Patch Changes + +- 65ddccb5e8: Added apiVersionOverrides config to allow for specifying api versions to use for kubernetes objects +- f6087fc8f8: Query CronJobs from Kubernetes with apiGroup BatchV1beta1 +- Updated dependencies + - @backstage/backend-common@0.9.12 + - @backstage/plugin-kubernetes-common@0.1.7 + ## 0.3.19 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index e1236e61c5..7d3adac1c4 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -10,6 +10,7 @@ import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { Logger as Logger_2 } from 'winston'; import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { PodStatus } from '@kubernetes/client-node/dist/top'; // Warning: (ae-missing-release-tag) "AWSClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -34,6 +35,7 @@ export interface ClusterDetails { name: string; // (undocumented) serviceAccountToken?: string | undefined; + skipMetricsLookup?: boolean; // (undocumented) skipTLSVerify?: boolean; // (undocumented) @@ -164,6 +166,11 @@ export interface KubernetesFetcher { fetchObjectsForService( params: ObjectFetchParams, ): Promise; + // (undocumented) + fetchPodMetricsByNamespace( + clusterDetails: ClusterDetails, + namespace: string, + ): Promise; } // Warning: (ae-missing-release-tag) "KubernetesObjectsProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml b/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml new file mode 100644 index 0000000000..673064aaf2 --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml @@ -0,0 +1,137 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: system:aggregated-metrics-reader + labels: + rbac.authorization.k8s.io/aggregate-to-view: 'true' + rbac.authorization.k8s.io/aggregate-to-edit: 'true' + rbac.authorization.k8s.io/aggregate-to-admin: 'true' +rules: + - apiGroups: ['metrics.k8s.io'] + resources: ['pods'] + verbs: ['get', 'list', 'watch'] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: metrics-server:system:auth-delegator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: metrics-server-auth-reader + namespace: kube-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: kube-system +--- +apiVersion: apiregistration.k8s.io/v1beta1 +kind: APIService +metadata: + name: v1beta1.metrics.k8s.io +spec: + service: + name: metrics-server + namespace: kube-system + group: metrics.k8s.io + version: v1beta1 + insecureSkipTLSVerify: true + groupPriorityMinimum: 100 + versionPriority: 100 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: metrics-server + namespace: kube-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-server + namespace: kube-system + labels: + k8s-app: metrics-server +spec: + selector: + matchLabels: + k8s-app: metrics-server + template: + metadata: + name: metrics-server + labels: + k8s-app: metrics-server + spec: + serviceAccountName: metrics-server + volumes: + # mount in tmp so we can safely use from-scratch images and/or read-only containers + - name: tmp-dir + emptyDir: {} + containers: + - name: metrics-server + image: k8s.gcr.io/metrics-server-amd64:v0.3.1 + args: + - --kubelet-insecure-tls + - --kubelet-preferred-address-types=InternalIP + imagePullPolicy: Always + volumeMounts: + - name: tmp-dir + mountPath: /tmp + +--- +apiVersion: v1 +kind: Service +metadata: + name: metrics-server + namespace: kube-system + labels: + kubernetes.io/name: 'Metrics-server' +spec: + selector: + k8s-app: metrics-server + ports: + - port: 443 + protocol: TCP + targetPort: 443 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:metrics-server +rules: + - apiGroups: + - '' + resources: + - pods + - nodes + - nodes/stats + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:metrics-server +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:metrics-server +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: kube-system diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 0b3f7cb3cb..253cdfde18 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.3.19", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,13 +32,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.5", - "@backstage/plugin-kubernetes-common": "^0.1.6", + "@backstage/plugin-kubernetes-common": "^0.2.0", "@google-cloud/container": "^2.2.0", - "@kubernetes/client-node": "^0.15.0", + "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", "aws4": "^1.11.0", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 62346ac6c3..f5d6b56893 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -52,6 +52,7 @@ describe('ConfigClusterLocator', () => { serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, }, @@ -67,6 +68,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8080', authProvider: 'serviceAccount', skipTLSVerify: false, + skipMetricsLookup: true, dashboardUrl: 'https://k8s.foo.com', }, { @@ -74,6 +76,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8081', authProvider: 'google', skipTLSVerify: true, + skipMetricsLookup: false, }, ], }); @@ -90,6 +93,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8080', authProvider: 'serviceAccount', skipTLSVerify: false, + skipMetricsLookup: true, caData: undefined, }, { @@ -98,6 +102,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8081', authProvider: 'google', skipTLSVerify: true, + skipMetricsLookup: false, caData: undefined, }, ]); @@ -144,6 +149,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8080', authProvider: 'aws', skipTLSVerify: false, + skipMetricsLookup: false, caData: undefined, }, { @@ -154,6 +160,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8081', authProvider: 'aws', skipTLSVerify: true, + skipMetricsLookup: false, caData: undefined, }, { @@ -164,6 +171,7 @@ describe('ConfigClusterLocator', () => { serviceAccountToken: undefined, authProvider: 'aws', skipTLSVerify: true, + skipMetricsLookup: false, caData: undefined, }, ]); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index bb9dbfe13f..36b5f58f2c 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -35,6 +35,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { url: c.getString('url'), serviceAccountToken: c.getOptionalString('serviceAccountToken'), skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false, + skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false, caData: c.getOptionalString('caData'), authProvider: authProvider, }; diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 545f839907..bd342f566b 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -93,6 +93,7 @@ describe('GkeClusterLocator', () => { type: 'gke', projectId: 'some-project', region: 'some-region', + skipMetricsLookup: true, }); const sut = GkeClusterLocator.fromConfigWithClient(config, { @@ -107,6 +108,7 @@ describe('GkeClusterLocator', () => { name: 'some-cluster', url: 'https://1.2.3.4', skipTLSVerify: false, + skipMetricsLookup: true, }, ]); expect(mockedListClusters).toBeCalledTimes(1); @@ -143,6 +145,7 @@ describe('GkeClusterLocator', () => { name: 'some-cluster', url: 'https://1.2.3.4', skipTLSVerify: false, + skipMetricsLookup: false, }, ]); expect(mockedListClusters).toBeCalledTimes(1); @@ -184,12 +187,14 @@ describe('GkeClusterLocator', () => { name: 'some-cluster', url: 'https://1.2.3.4', skipTLSVerify: false, + skipMetricsLookup: false, }, { authProvider: 'google', name: 'some-other-cluster', url: 'https://6.7.8.9', skipTLSVerify: false, + skipMetricsLookup: false, }, ]); expect(mockedListClusters).toBeCalledTimes(1); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 3aa4a3e941..d7f5050399 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -23,6 +23,7 @@ type GkeClusterLocatorOptions = { projectId: string; region?: string; skipTLSVerify?: boolean; + skipMetricsLookup?: boolean; }; export class GkeClusterLocator implements KubernetesClustersSupplier { @@ -39,6 +40,8 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { projectId: config.getString('projectId'), region: config.getOptionalString('region') ?? '-', skipTLSVerify: config.getOptionalBoolean('skipTLSVerify') ?? false, + skipMetricsLookup: + config.getOptionalBoolean('skipMetricsLookup') ?? false, }; return new GkeClusterLocator(options, client); } @@ -52,7 +55,8 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { // TODO pass caData into the object async getClusters(): Promise { - const { projectId, region, skipTLSVerify } = this.options; + const { projectId, region, skipTLSVerify, skipMetricsLookup } = + this.options; const request = { parent: `projects/${projectId}/locations/${region}`, }; @@ -65,6 +69,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { url: `https://${r.endpoint ?? ''}`, authProvider: 'google', skipTLSVerify, + skipMetricsLookup, })); } catch (e) { throw new ForwardedError( diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index dde4dc1d93..2bc2c719a0 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -53,6 +53,7 @@ describe('getCombinedClusterDetails', () => { serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', + skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, }, @@ -61,6 +62,7 @@ describe('getCombinedClusterDetails', () => { serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'google', + skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, }, diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index de0dcdf373..37dad8c3e4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -29,6 +29,7 @@ import { } from '../types/types'; import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; +import { PodStatus } from '@kubernetes/client-node'; describe('KubernetesBuilder', () => { let app: express.Express; @@ -194,6 +195,7 @@ describe('KubernetesBuilder', () => { name: someCluster.name, }, errors: [], + podMetrics: [], resources: [ { type: 'pods', @@ -211,6 +213,12 @@ describe('KubernetesBuilder', () => { }; const fetcher: KubernetesFetcher = { + fetchPodMetricsByNamespace( + _clusterDetails: ClusterDetails, + _namespace: string, + ): Promise { + return Promise.resolve([]); + }, fetchObjectsForService( _params: ObjectFetchParams, ): Promise { diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts index 96bd1fe7c1..e813a63f0d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts @@ -47,14 +47,14 @@ describe('KubernetesClientProvider', () => { expect(mockGetKubeConfig.mock.calls.length).toBe(1); }); - it('can get apps client by cluster details', async () => { + it('can get custom objects client by cluster details', async () => { const sut = new KubernetesClientProvider(); const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); sut.getKubeConfig = mockGetKubeConfig; - const result = sut.getAppsClientByClusterDetails({ + const result = sut.getCustomObjectsClient({ name: 'cluster-name', url: 'http://localhost:9999', serviceAccountToken: 'TOKEN', diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts index 4a91118c7d..031e40547c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -15,12 +15,9 @@ */ import { - AppsV1Api, - BatchV1beta1Api, - AutoscalingV1Api, CoreV1Api, KubeConfig, - NetworkingV1beta1Api, + Metrics, CustomObjectsApi, } from '@kubernetes/client-node'; import { ClusterDetails } from '../types/types'; @@ -63,28 +60,10 @@ export class KubernetesClientProvider { return kc.makeApiClient(CoreV1Api); } - getAppsClientByClusterDetails(clusterDetails: ClusterDetails) { + getMetricsClient(clusterDetails: ClusterDetails) { const kc = this.getKubeConfig(clusterDetails); - return kc.makeApiClient(AppsV1Api); - } - - getAutoscalingClientByClusterDetails(clusterDetails: ClusterDetails) { - const kc = this.getKubeConfig(clusterDetails); - - return kc.makeApiClient(AutoscalingV1Api); - } - - getBatchClientByClusterDetails(clusterDetails: ClusterDetails) { - const kc = this.getKubeConfig(clusterDetails); - - return kc.makeApiClient(BatchV1beta1Api); - } - - getNetworkingBeta1Client(clusterDetails: ClusterDetails) { - const kc = this.getKubeConfig(clusterDetails); - - return kc.makeApiClient(NetworkingV1beta1Api); + return new Metrics(kc); } getCustomObjectsClient(clusterDetails: ClusterDetails) { diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 7973448467..da8f703ba0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -15,13 +15,30 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { ObjectFetchParams } from '../types/types'; +import { ClusterDetails, ObjectFetchParams } from '../types/types'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; +import { PodStatus } from '@kubernetes/client-node/dist/top'; const fetchObjectsForService = jest.fn(); +const fetchPodMetricsByNamespace = jest.fn(); const getClustersByServiceId = jest.fn(); +const POD_METRICS_FIXTURE = { + containers: [], + cpu: { + currentUsage: 100, + limitTotal: 102, + requestTotal: 101, + }, + memory: { + currentUsage: '1000', + limitTotal: '1002', + requestTotal: '1001', + }, + pod: {}, +}; + const mockFetch = (mock: jest.Mock) => { mock.mockImplementation((params: ObjectFetchParams) => Promise.resolve( @@ -33,6 +50,34 @@ const mockFetch = (mock: jest.Mock) => { ); }; +const mockMetrics = (mock: jest.Mock) => { + mock.mockImplementation((clusterDetails: ClusterDetails, namespace: string) => + Promise.resolve(generatePodStatus(clusterDetails.name, namespace)), + ); +}; + +function generatePodStatus( + _clusterName: string, + _namespace: string, +): PodStatus[] { + return [ + { + Pod: {}, + CPU: { + CurrentUsage: 100, + RequestTotal: 101, + LimitTotal: 102, + }, + Memory: { + CurrentUsage: BigInt('1000'), + RequestTotal: BigInt('1001'), + LimitTotal: BigInt('1002'), + }, + Containers: [], + }, + ] as any; +} + function generateMockResourcesAndErrors( serviceId: string, clusterName: string, @@ -84,6 +129,7 @@ function generateMockResourcesAndErrors( { metadata: { name: `my-pods-${serviceId}-${clusterName}`, + namespace: `ns-${serviceId}-${clusterName}`, }, }, ], @@ -94,6 +140,7 @@ function generateMockResourcesAndErrors( { metadata: { name: `my-configmaps-${serviceId}-${clusterName}`, + namespace: `ns-${serviceId}-${clusterName}`, }, }, ], @@ -104,6 +151,7 @@ function generateMockResourcesAndErrors( { metadata: { name: `my-services-${serviceId}-${clusterName}`, + namespace: `ns-${serviceId}-${clusterName}`, }, }, ], @@ -128,11 +176,13 @@ describe('handleGetKubernetesObjectsForService', () => { ); mockFetch(fetchObjectsForService); + mockMetrics(fetchPodMetricsByNamespace); const sut = new KubernetesFanOutHandler({ logger: getVoidLogger(), fetcher: { fetchObjectsForService, + fetchPodMetricsByNamespace, }, serviceLocator: { getClustersByServiceId, @@ -161,6 +211,11 @@ describe('handleGetKubernetesObjectsForService', () => { expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsForService.mock.calls.length).toBe(1); + expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(1); + expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe( + 'ns-test-component-test-cluster', + ); + expect(result).toStrictEqual({ items: [ { @@ -168,12 +223,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'test-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -184,6 +241,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -194,6 +252,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -205,6 +264,124 @@ describe('handleGetKubernetesObjectsForService', () => { }); }); + it('dont call top for the same namespace twice', async () => { + getClustersByServiceId.mockImplementation(() => + Promise.resolve([ + { + name: 'test-cluster', + authProvider: 'serviceAccount', + }, + ]), + ); + + fetchObjectsForService.mockImplementation((_: ObjectFetchParams) => + Promise.resolve({ + errors: [], + responses: [ + { + type: 'pods', + resources: [ + { + metadata: { + name: `pod1`, + namespace: `ns-a`, + }, + }, + { + metadata: { + name: `pod2`, + namespace: `ns-a`, + }, + }, + { + metadata: { + name: `pod3`, + namespace: `ns-b`, + }, + }, + ], + }, + ], + }), + ); + + mockMetrics(fetchPodMetricsByNamespace); + + const sut = new KubernetesFanOutHandler({ + logger: getVoidLogger(), + fetcher: { + fetchObjectsForService, + fetchPodMetricsByNamespace, + }, + serviceLocator: { + getClustersByServiceId, + }, + customResources: [], + }); + + const result = await sut.getKubernetesObjectsByEntity({ + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', + }, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + }, + }, + }); + + expect(getClustersByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsForService.mock.calls.length).toBe(1); + expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(2); + expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe('ns-a'); + expect(fetchPodMetricsByNamespace.mock.calls[1][1]).toBe('ns-b'); + + expect(result).toStrictEqual({ + items: [ + { + cluster: { + name: 'test-cluster', + }, + errors: [], + podMetrics: [POD_METRICS_FIXTURE, POD_METRICS_FIXTURE], + resources: [ + { + resources: [ + { + metadata: { + name: 'pod1', + namespace: 'ns-a', + }, + }, + { + metadata: { + name: 'pod2', + namespace: 'ns-a', + }, + }, + { + metadata: { + name: 'pod3', + namespace: 'ns-b', + }, + }, + ], + type: 'pods', + }, + ], + }, + ], + }); + }); + it('retrieve objects for two clusters', async () => { getClustersByServiceId.mockImplementation(() => Promise.resolve([ @@ -221,11 +398,13 @@ describe('handleGetKubernetesObjectsForService', () => { ); mockFetch(fetchObjectsForService); + mockMetrics(fetchPodMetricsByNamespace); const sut = new KubernetesFanOutHandler({ logger: getVoidLogger(), fetcher: { fetchObjectsForService, + fetchPodMetricsByNamespace, }, serviceLocator: { getClustersByServiceId, @@ -265,12 +444,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'test-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -281,6 +462,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -291,6 +473,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -303,12 +486,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'other-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -319,6 +504,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -329,6 +515,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -358,11 +545,13 @@ describe('handleGetKubernetesObjectsForService', () => { ); mockFetch(fetchObjectsForService); + mockMetrics(fetchPodMetricsByNamespace); const sut = new KubernetesFanOutHandler({ logger: getVoidLogger(), fetcher: { fetchObjectsForService, + fetchPodMetricsByNamespace, }, serviceLocator: { getClustersByServiceId, @@ -401,12 +590,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'test-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -417,6 +608,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -427,6 +619,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -439,12 +632,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'other-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -455,6 +650,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -465,6 +661,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -498,11 +695,13 @@ describe('handleGetKubernetesObjectsForService', () => { ); mockFetch(fetchObjectsForService); + mockMetrics(fetchPodMetricsByNamespace); const sut = new KubernetesFanOutHandler({ logger: getVoidLogger(), fetcher: { fetchObjectsForService, + fetchPodMetricsByNamespace, }, serviceLocator: { getClustersByServiceId, @@ -541,12 +740,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'test-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -557,6 +758,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -567,6 +769,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-test-cluster', + namespace: 'ns-test-component-test-cluster', }, }, ], @@ -579,12 +782,14 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'other-cluster', }, errors: [], + podMetrics: [POD_METRICS_FIXTURE], resources: [ { resources: [ { metadata: { name: 'my-pods-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -595,6 +800,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-configmaps-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -605,6 +811,7 @@ describe('handleGetKubernetesObjectsForService', () => { { metadata: { name: 'my-services-test-component-other-cluster', + namespace: 'ns-test-component-other-cluster', }, }, ], @@ -617,6 +824,7 @@ describe('handleGetKubernetesObjectsForService', () => { name: 'error-cluster', }, errors: ['some random cluster error'], + podMetrics: [], resources: [ { type: 'pods', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 41d75a9835..a25c220b80 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -27,9 +27,19 @@ import { import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; import { + ClientContainerStatus, + ClientCurrentResourceUsage, + ClientPodStatus, ClusterObjects, + FetchResponse, ObjectsByEntityResponse, + PodFetchResponse, } from '@backstage/plugin-kubernetes-common'; +import { + ContainerStatus, + CurrentResourceUsage, + PodStatus, +} from '@kubernetes/client-node'; export const DEFAULT_OBJECTS: ObjectToFetch[] = [ { @@ -93,6 +103,50 @@ export interface KubernetesFanOutHandlerOptions export interface KubernetesRequestBody extends ObjectsByEntityRequest {} +const isPodFetchResponse = (fr: FetchResponse): fr is PodFetchResponse => + fr.type === 'pods'; +const isString = (str: string | undefined): str is string => str !== undefined; + +const numberOrBigIntToNumberOrString = ( + value: number | BigInt, +): number | string => { + // @ts-ignore + return typeof value === 'bigint' ? value.toString() : value; +}; + +const toClientSafeResource = ( + current: CurrentResourceUsage, +): ClientCurrentResourceUsage => { + return { + currentUsage: numberOrBigIntToNumberOrString(current.CurrentUsage), + requestTotal: numberOrBigIntToNumberOrString(current.RequestTotal), + limitTotal: numberOrBigIntToNumberOrString(current.LimitTotal), + }; +}; + +const toClientSafeContainer = ( + container: ContainerStatus, +): ClientContainerStatus => { + return { + container: container.Container, + cpuUsage: toClientSafeResource(container.CPUUsage), + memoryUsage: toClientSafeResource(container.MemoryUsage), + }; +}; + +const toClientSafePodMetrics = ( + podMetrics: PodStatus[][], +): ClientPodStatus[] => { + return podMetrics.flat().map((pd: PodStatus): ClientPodStatus => { + return { + pod: pd.Pod, + memory: toClientSafeResource(pd.Memory), + cpu: toClientSafeResource(pd.CPU), + containers: pd.Containers.map(toClientSafeContainer), + }; + }); +}; + export class KubernetesFanOutHandler { private readonly logger: Logger; private readonly fetcher: KubernetesFetcher; @@ -162,10 +216,36 @@ export class KubernetesFanOutHandler { customResources: this.customResources, }) .then(result => { + if (clusterDetailsItem.skipMetricsLookup) { + return Promise.all([ + Promise.resolve(result), + Promise.resolve([]), + ]); + } + // TODO refactor, extract as method + const namespaces: Set = new Set( + result.responses + .filter(isPodFetchResponse) + .flatMap(r => r.resources) + .map(p => p.metadata?.namespace) + .filter(isString), + ); + + const podMetrics = Array.from(namespaces).map(ns => + this.fetcher.fetchPodMetricsByNamespace(clusterDetailsItem, ns), + ); + + return Promise.all([ + Promise.resolve(result), + Promise.all(podMetrics), + ]); + }) + .then(([result, metrics]) => { const objects: ClusterObjects = { cluster: { name: clusterDetailsItem.name, }, + podMetrics: toClientSafePodMetrics(metrics), resources: result.responses, errors: result.errors, }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index e86901ef92..e1f701b75c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -106,6 +106,7 @@ describe('KubernetesFetcher', () => { 'v1', 'pods', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -116,6 +117,7 @@ describe('KubernetesFetcher', () => { 'v1', 'services', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -197,6 +199,7 @@ describe('KubernetesFetcher', () => { 'v1', 'pods', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -207,6 +210,7 @@ describe('KubernetesFetcher', () => { 'v1', 'services', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -316,6 +320,7 @@ describe('KubernetesFetcher', () => { 'v1', 'pods', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -326,6 +331,7 @@ describe('KubernetesFetcher', () => { 'v1', 'services', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', @@ -336,6 +342,7 @@ describe('KubernetesFetcher', () => { 'v2', 'things', '', + false, '', '', 'backstage.io/kubernetes-id=some-service', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 685df85c33..a2c3b9a6a8 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import { - AppsV1Api, - AutoscalingV1Api, - BatchV1beta1Api, - CoreV1Api, - NetworkingV1beta1Api, -} from '@kubernetes/client-node'; +import { CoreV1Api, topPods } from '@kubernetes/client-node'; import lodash, { Dictionary } from 'lodash'; import { Logger } from 'winston'; import { @@ -37,13 +31,10 @@ import { KubernetesErrorTypes, } from '@backstage/plugin-kubernetes-common'; import { KubernetesClientProvider } from './KubernetesClientProvider'; +import { PodStatus } from '@kubernetes/client-node/dist/top'; export interface Clients { core: CoreV1Api; - apps: AppsV1Api; - autoscaling: AutoscalingV1Api; - batch: BatchV1beta1Api; - networkingBeta1: NetworkingV1beta1Api; } export interface KubernetesClientBasedFetcherOptions { @@ -112,10 +103,26 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { return Promise.all(fetchResults).then(fetchResultsToResponseWrapper); } + fetchPodMetricsByNamespace( + clusterDetails: ClusterDetails, + namespace: string, + ): Promise { + const metricsClient = + this.kubernetesClientProvider.getMetricsClient(clusterDetails); + const coreApi = + this.kubernetesClientProvider.getCoreClientByClusterDetails( + clusterDetails, + ); + + return topPods(coreApi, metricsClient, namespace); + } + private captureKubernetesErrorsRethrowOthers(e: any): KubernetesFetchError { if (e.response && e.response.statusCode) { - this.logger.info( - `statusCode=${e.response.statusCode} for resource ${e.response.request.uri.pathname}`, + this.logger.warn( + `statusCode=${e.response.statusCode} for resource ${ + e.response.request.uri.pathname + } body=[${JSON.stringify(e.response.body)}]`, ); return { errorType: statusCodeToErrorType(e.response.statusCode), @@ -145,6 +152,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { resource.apiVersion, resource.plural, '', + false, '', '', labelSelector, diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index b8aa482e47..a0ec0832c1 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -21,6 +21,7 @@ import type { KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-common'; +import { PodStatus } from '@kubernetes/client-node/dist/top'; export interface ObjectFetchParams { serviceId: string; @@ -40,6 +41,10 @@ export interface KubernetesFetcher { fetchObjectsForService( params: ObjectFetchParams, ): Promise; + fetchPodMetricsByNamespace( + clusterDetails: ClusterDetails, + namespace: string, + ): Promise; } export interface FetchResponseWrapper { @@ -93,6 +98,11 @@ export interface ClusterDetails { authProvider: string; serviceAccountToken?: string | undefined; skipTLSVerify?: boolean; + /** + * Whether to skip the lookup to the metrics server to retrieve pod resource usage. + * It is not guaranteed that the Kubernetes distro has the metrics server installed. + */ + skipMetricsLookup?: boolean; caData?: string | undefined; /** * Specifies the link to the Kubernetes dashboard managing this cluster. diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index b0e7aca129..8580e73fa7 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-kubernetes-common +## 0.2.0 + +### Minor Changes + +- c010632f88: Add pod metrics lookup and display in pod table. + + ## Backwards incompatible changes + + If your Kubernetes distribution does not have the [metrics server](https://github.com/kubernetes-sigs/metrics-server) installed, + you will need to set the `skipMetricsLookup` config flag to `false`. + + See the [configuration docs](https://backstage.io/docs/features/kubernetes/configuration) for more details. + +## 0.1.7 + +### Patch Changes + +- 59677fadb1: Improvements to API Reference documentation + ## 0.1.6 ### Patch Changes diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 7db05a7513..f3500aa271 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -4,11 +4,11 @@ ```ts import { Entity } from '@backstage/catalog-model'; -import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; import { V1ConfigMap } from '@kubernetes/client-node'; import { V1CronJob } from '@kubernetes/client-node'; import { V1Deployment } from '@kubernetes/client-node'; import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { V1Ingress } from '@kubernetes/client-node'; import { V1Job } from '@kubernetes/client-node'; import { V1Pod } from '@kubernetes/client-node'; import { V1ReplicaSet } from '@kubernetes/client-node'; @@ -19,6 +19,44 @@ import { V1Service } from '@kubernetes/client-node'; // @public (undocumented) export type AuthProviderType = 'google' | 'serviceAccount' | 'aws'; +// Warning: (ae-missing-release-tag) "ClientContainerStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ClientContainerStatus { + // (undocumented) + container: string; + // (undocumented) + cpuUsage: ClientCurrentResourceUsage; + // (undocumented) + memoryUsage: ClientCurrentResourceUsage; +} + +// Warning: (ae-missing-release-tag) "ClientCurrentResourceUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ClientCurrentResourceUsage { + // (undocumented) + currentUsage: number | string; + // (undocumented) + limitTotal: number | string; + // (undocumented) + requestTotal: number | string; +} + +// Warning: (ae-missing-release-tag) "ClientPodStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ClientPodStatus { + // (undocumented) + containers: ClientContainerStatus[]; + // (undocumented) + cpu: ClientCurrentResourceUsage; + // (undocumented) + memory: ClientCurrentResourceUsage; + // (undocumented) + pod: V1Pod; +} + // Warning: (ae-missing-release-tag) "ClusterAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -37,6 +75,8 @@ export interface ClusterObjects { // (undocumented) errors: KubernetesFetchError[]; // (undocumented) + podMetrics: ClientPodStatus[]; + // (undocumented) resources: FetchResponse[]; } @@ -110,7 +150,7 @@ export interface HorizontalPodAutoscalersFetchResponse { // @public (undocumented) export interface IngressesFetchResponse { // (undocumented) - resources: Array; + resources: Array; // (undocumented) type: 'ingresses'; } diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index a3ab7710c0..124e3415a7 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.1.6", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@kubernetes/client-node": "^0.15.0" + "@kubernetes/client-node": "^0.16.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1" + "@backstage/cli": "^0.10.1" }, "jest": { "roots": [ diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts index af146ae9fa..6b97c0eb7d 100644 --- a/plugins/kubernetes-common/src/index.ts +++ b/plugins/kubernetes-common/src/index.ts @@ -15,7 +15,7 @@ */ /** - * Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin + * Common functionalities for Kubernetes, to be shared between the `kubernetes` and `kubernetes-backend` plugins * * @packageDocumentation */ diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 8f4daa111b..0c5af00a97 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -15,11 +15,11 @@ */ import { - ExtensionsV1beta1Ingress, V1ConfigMap, V1CronJob, V1Deployment, V1HorizontalPodAutoscaler, + V1Ingress, V1Job, V1Pod, V1ReplicaSet, @@ -69,6 +69,7 @@ export interface ClusterAttributes { export interface ClusterObjects { cluster: ClusterAttributes; resources: FetchResponse[]; + podMetrics: ClientPodStatus[]; errors: KubernetesFetchError[]; } @@ -132,7 +133,7 @@ export interface CronJobsFetchResponse { export interface IngressesFetchResponse { type: 'ingresses'; - resources: Array; + resources: Array; } export interface CustomResourceFetchResponse { @@ -151,3 +152,22 @@ export type KubernetesErrorTypes = | 'UNAUTHORIZED_ERROR' | 'SYSTEM_ERROR' | 'UNKNOWN_ERROR'; + +export interface ClientCurrentResourceUsage { + currentUsage: number | string; + requestTotal: number | string; + limitTotal: number | string; +} + +export interface ClientContainerStatus { + container: string; + cpuUsage: ClientCurrentResourceUsage; + memoryUsage: ClientCurrentResourceUsage; +} + +export interface ClientPodStatus { + pod: V1Pod; + cpu: ClientCurrentResourceUsage; + memory: ClientCurrentResourceUsage; + containers: ClientContainerStatus[]; +} diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 437ea327e4..0329a9bc5b 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-kubernetes +## 0.5.0 + +### Minor Changes + +- c010632f88: Add pod metrics lookup and display in pod table. + + ## Backwards incompatible changes + + If your Kubernetes distribution does not have the [metrics server](https://github.com/kubernetes-sigs/metrics-server) installed, + you will need to set the `skipMetricsLookup` config flag to `false`. + + See the [configuration docs](https://backstage.io/docs/features/kubernetes/configuration) for more details. + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-kubernetes-common@0.2.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.4.22 + +### Patch Changes + +- 86ed770308: Added accordions to display information on Jobs and CronJobs in the kubernetes plugin. Updated the PodsTable with fewer default columns and the ability to pass in additional ones depending on the use case. +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + - @backstage/plugin-kubernetes-common@0.1.7 + ## 0.4.21 ### Patch Changes diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index c7a99e7334..068f26b7f5 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -66,6 +66,7 @@ class MockKubernetesClient implements KubernetesApi { { cluster: { name: 'mock-cluster' }, resources: this.resources, + podMetrics: [], errors: [], }, ], diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index a127c9edf6..3a2ea6a9a9 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.4.21", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/plugin-kubernetes-common": "^0.1.6", - "@backstage/theme": "^0.2.13", - "@kubernetes/client-node": "^0.15.0", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-kubernetes-common": "^0.2.0", + "@kubernetes/client-node": "^0.16.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -46,16 +46,17 @@ "js-yaml": "^4.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx index ddadd95272..620af05e69 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx @@ -46,6 +46,7 @@ describe('Cluster', () => { resources: oneDeployment.pods, }, ], + podMetrics: [], errors: [], }, podsWithErrors: new Set(), diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx index 416d6e23e3..b7a819cdbf 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -23,7 +23,10 @@ import { Grid, Typography, } from '@material-ui/core'; -import { ClusterObjects } from '@backstage/plugin-kubernetes-common'; +import { + ClientPodStatus, + ClusterObjects, +} from '@backstage/plugin-kubernetes-common'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { groupResponses } from '../../utils/response'; @@ -38,6 +41,7 @@ import { } from '../../hooks'; import { StatusError, StatusOK } from '@backstage/core-components'; +import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics'; type ClusterSummaryProps = { clusterName: string; @@ -108,39 +112,50 @@ type ClusterProps = { export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { const groupedResponses = groupResponses(clusterObjects.resources); + const podNameToMetrics = clusterObjects.podMetrics + .flat() + .reduce((accum, next) => { + const name = next.pod.metadata?.name; + if (name !== undefined) { + accum.set(name, next); + } + return accum; + }, new Map()); return ( - - - }> - - - - - - - - - - - - - - - + + + + }> + + + + + + + + + + + + + + + + - - - - + + + + ); diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx index 2434631fd0..9d2df328d2 100644 --- a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx @@ -23,7 +23,7 @@ import { kubernetesProviders } from '../../hooks/test-utils'; describe('CronJobsAccordions', () => { it('should render 1 active cronjobs', async () => { - const wrapper = kubernetesProviders(oneCronJobsFixture, []); + const wrapper = kubernetesProviders(oneCronJobsFixture, new Set()); const { getByText } = render( wrapper(wrapInTestApp()), @@ -36,7 +36,7 @@ describe('CronJobsAccordions', () => { }); it('should render 1 suspended cronjobs', async () => { - const wrapper = kubernetesProviders(twoCronJobsFixture, []); + const wrapper = kubernetesProviders(twoCronJobsFixture, new Set()); const { getByText } = render( wrapper(wrapInTestApp()), diff --git a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx index e14cd9093a..52734e39a8 100644 --- a/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx +++ b/plugins/kubernetes/src/components/CustomResources/ArgoRollouts/Rollout.tsx @@ -41,6 +41,7 @@ import { getOwnedPodsThroughReplicaSets, } from '../../../utils/owner'; import { StatusError, StatusOK } from '@backstage/core-components'; +import { READY_COLUMNS, RESOURCE_COLUMNS } from '../../Pods/PodsTable'; type RolloutAccordionsProps = { rollouts: any[]; @@ -237,7 +238,10 @@ const RolloutAccordion = ({ />
- +
diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx index 2fa140b5f7..58ab2fc2ba 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -36,12 +36,12 @@ import { getOwnedPodsThroughReplicaSets, getMatchingHpa, } from '../../utils/owner'; -import { containersReady, totalRestarts } from '../../utils/pod'; import { GroupedResponsesContext, PodNamesWithErrorsContext, } from '../../hooks'; -import { StatusError, StatusOK, TableColumn } from '@backstage/core-components'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable'; type DeploymentsAccordionsProps = { children?: React.ReactNode; @@ -62,20 +62,6 @@ type DeploymentSummaryProps = { children?: React.ReactNode; }; -const deploymentPodColumns: TableColumn[] = [ - { - title: 'containers ready', - align: 'center', - render: containersReady, - }, - { - title: 'total restarts', - align: 'center', - render: totalRestarts, - type: 'numeric', - }, -]; - const DeploymentSummary = ({ deployment, numberOfCurrentPods, @@ -176,7 +162,10 @@ const DeploymentAccordion = ({ /> - + ); diff --git a/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx b/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx index 02154c4825..832d6eed91 100644 --- a/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx +++ b/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx @@ -51,6 +51,7 @@ describe('ErrorPanel', () => { name: 'THIS_CLUSTER', }, resources: [], + podMetrics: [], errors: [ { errorType: 'SYSTEM_ERROR', diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx index 0018c4b070..dc88987d53 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; +import { V1Ingress } from '@kubernetes/client-node'; import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; import { Typography, Grid } from '@material-ui/core'; @@ -23,7 +23,7 @@ export const IngressDrawer = ({ ingress, expanded, }: { - ingress: ExtensionsV1beta1Ingress; + ingress: V1Ingress; expanded?: boolean; }) => { return ( @@ -31,7 +31,7 @@ export const IngressDrawer = ({ object={ingress} expanded={expanded} kind="Ingress" - renderObject={(ingressObject: ExtensionsV1beta1Ingress) => { + renderObject={(ingressObject: V1Ingress) => { return ingressObject.spec || {}; }} > diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx index c1d94fef7b..f6d6e4bf68 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressesAccordions.tsx @@ -23,7 +23,7 @@ import { Grid, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; +import { V1Ingress } from '@kubernetes/client-node'; import { IngressDrawer } from './IngressDrawer'; import { GroupedResponsesContext } from '../../hooks'; import { StructuredMetadataTable } from '@backstage/core-components'; @@ -31,11 +31,11 @@ import { StructuredMetadataTable } from '@backstage/core-components'; type IngressesAccordionsProps = {}; type IngressAccordionProps = { - ingress: ExtensionsV1beta1Ingress; + ingress: V1Ingress; }; type IngressSummaryProps = { - ingress: ExtensionsV1beta1Ingress; + ingress: V1Ingress; }; const IngressSummary = ({ ingress }: IngressSummaryProps) => { @@ -58,7 +58,7 @@ const IngressSummary = ({ ingress }: IngressSummaryProps) => { }; type IngressCardProps = { - ingress: ExtensionsV1beta1Ingress; + ingress: V1Ingress; }; const IngressCard = ({ ingress }: IngressCardProps) => { diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx index 093d616319..ea16e933c6 100644 --- a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx @@ -23,7 +23,7 @@ import { V1Job, ObjectSerializer } from '@kubernetes/client-node'; describe('JobsAccordions', () => { it('should render 2 jobs', async () => { - const wrapper = kubernetesProviders(oneCronJobsFixture, []); + const wrapper = kubernetesProviders(oneCronJobsFixture, new Set()); const jobs: V1Job[] = oneCronJobsFixture.jobs.map( job => ObjectSerializer.deserialize(job, 'V1Job') as V1Job, diff --git a/plugins/kubernetes/src/components/KubernetesContent.test.tsx b/plugins/kubernetes/src/components/KubernetesContent.test.tsx index db8cd06782..231eb3b37a 100644 --- a/plugins/kubernetes/src/components/KubernetesContent.test.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent.test.tsx @@ -73,6 +73,7 @@ describe('KubernetesContent', () => { resources: oneDeployment.pods, }, ], + podMetrics: [], errors: [], }, ], @@ -121,6 +122,7 @@ describe('KubernetesContent', () => { resources: twoDeployments.pods, }, ], + podMetrics: [], errors: [], }, { @@ -139,6 +141,7 @@ describe('KubernetesContent', () => { resources: oneDeployment.pods, }, ], + podMetrics: [], errors: [], }, ], diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx index 868b74bbc9..1201ad106d 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx @@ -18,25 +18,10 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as pod from './__fixtures__/pod.json'; import * as crashingPod from './__fixtures__/crashing-pod.json'; -import { TableColumn } from '@backstage/core-components'; import { wrapInTestApp } from '@backstage/test-utils'; -import { V1Pod } from '@kubernetes/client-node'; -import { PodsTable } from './PodsTable'; -import { containersReady, totalRestarts } from '../../utils/pod'; - -const extraColumns: TableColumn[] = [ - { - title: 'containers ready', - align: 'center', - render: containersReady, - }, - { - title: 'total restarts', - align: 'center', - render: totalRestarts, - type: 'numeric', - }, -]; +import { PodsTable, READY_COLUMNS, RESOURCE_COLUMNS } from './PodsTable'; +import { kubernetesProviders } from '../../hooks/test-utils'; +import { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; describe('PodsTable', () => { it('should render pod', async () => { @@ -58,7 +43,7 @@ describe('PodsTable', () => { it('should render pod with extra columns', async () => { const { getByText } = render( wrapInTestApp( - , + , ), ); @@ -76,11 +61,101 @@ describe('PodsTable', () => { expect(getByText('0')).toBeInTheDocument(); expect(getByText('OK')).toBeInTheDocument(); }); + it('should render pod, with metrics context', async () => { + const podNameToClientPodStatus = new Map(); + podNameToClientPodStatus.set('dice-roller-6c8646bfd-2m5hv', { + memory: { + currentUsage: '1069056', + requestTotal: '67108864', + limitTotal: '134217728', + }, + cpu: { + currentUsage: 0.4966115, + requestTotal: 0.05, + limitTotal: 0.05, + }, + } as any); + + const wrapper = kubernetesProviders( + undefined, + undefined, + podNameToClientPodStatus, + ); + const { getByText } = render( + wrapper( + wrapInTestApp( + , + ), + ), + ); + + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('containers ready')).toBeInTheDocument(); + expect(getByText('total restarts')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + expect(getByText('CPU usage %')).toBeInTheDocument(); + expect(getByText('Memory usage %')).toBeInTheDocument(); + + // values + expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('1/1')).toBeInTheDocument(); + expect(getByText('0')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + expect(getByText('requests: 99%')).toBeInTheDocument(); + expect(getByText('limits: 99%')).toBeInTheDocument(); + expect(getByText('requests: 1%')).toBeInTheDocument(); + expect(getByText('limits: 0%')).toBeInTheDocument(); + }); + it('should render placehoplder when empty metrics context', async () => { + const podNameToClientPodStatus = new Map(); + + const wrapper = kubernetesProviders( + undefined, + undefined, + podNameToClientPodStatus, + ); + const { getByText, getAllByText } = render( + wrapper( + wrapInTestApp( + , + ), + ), + ); + + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('containers ready')).toBeInTheDocument(); + expect(getByText('total restarts')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + expect(getByText('CPU usage %')).toBeInTheDocument(); + expect(getByText('Memory usage %')).toBeInTheDocument(); + + // values + expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('1/1')).toBeInTheDocument(); + expect(getByText('0')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + expect(getAllByText('unknown')).toHaveLength(2); + }); it('should render crashing pod with extra columns', async () => { const { getByText, getAllByText } = render( wrapInTestApp( - , + , ), ); diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index 8ce5f813b3..1d2369e322 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -14,11 +14,28 @@ * limitations under the License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { V1Pod } from '@kubernetes/client-node'; import { PodDrawer } from './PodDrawer'; -import { containerStatuses } from '../../utils/pod'; +import { + containersReady, + containerStatuses, + podStatusToCpuUtil, + podStatusToMemoryUtil, + totalRestarts, +} from '../../utils/pod'; import { Table, TableColumn } from '@backstage/core-components'; +import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics'; + +export const READY_COLUMNS: PodColumns = 'READY'; +export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE'; +export type PodColumns = 'READY' | 'RESOURCE'; + +type PodsTablesProps = { + pods: V1Pod[]; + extraColumns?: PodColumns[]; + children?: React.ReactNode; +}; const DEFAULT_COLUMNS: TableColumn[] = [ { @@ -36,13 +53,57 @@ const DEFAULT_COLUMNS: TableColumn[] = [ }, ]; -type PodsTablesProps = { - pods: V1Pod[]; - extraColumns?: TableColumn[]; - children?: React.ReactNode; -}; +const READY: TableColumn[] = [ + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, +]; export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { + const podNamesWithMetrics = useContext(PodNamesWithMetricsContext); + const columns: TableColumn[] = [...DEFAULT_COLUMNS]; + + if (extraColumns.includes(READY_COLUMNS)) { + columns.push(...READY); + } + if (extraColumns.includes(RESOURCE_COLUMNS)) { + const resourceColumns: TableColumn[] = [ + { + title: 'CPU usage %', + render: (pod: V1Pod) => { + const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? ''); + + if (!metrics) { + return 'unknown'; + } + + return podStatusToCpuUtil(metrics); + }, + }, + { + title: 'Memory usage %', + render: (pod: V1Pod) => { + const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? ''); + + if (!metrics) { + return 'unknown'; + } + + return podStatusToMemoryUtil(metrics); + }, + }, + ]; + columns.push(...resourceColumns); + } + const tableStyle = { minWidth: '0', width: '100%', @@ -53,7 +114,7 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
); diff --git a/plugins/kubernetes/src/error-detection/error-detection.test.ts b/plugins/kubernetes/src/error-detection/error-detection.test.ts index 2e982f25f7..dcac7db907 100644 --- a/plugins/kubernetes/src/error-detection/error-detection.test.ts +++ b/plugins/kubernetes/src/error-detection/error-detection.test.ts @@ -40,6 +40,7 @@ const oneItem = (value: FetchResponse): ObjectsByEntityResponse => { { cluster: { name: CLUSTER_NAME }, errors: [], + podMetrics: [], resources: [value], }, ], @@ -74,6 +75,7 @@ describe('detectErrors', () => { { cluster: { name: 'cluster-a' }, errors: [], + podMetrics: [], resources: [ { type: 'pods', @@ -84,6 +86,7 @@ describe('detectErrors', () => { { cluster: { name: 'cluster-b' }, errors: [], + podMetrics: [], resources: [ { type: 'horizontalpodautoscalers', @@ -94,6 +97,7 @@ describe('detectErrors', () => { { cluster: { name: 'cluster-c' }, errors: [], + podMetrics: [], resources: [ { type: 'deployments', diff --git a/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts b/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts new file mode 100644 index 0000000000..66a833ad88 --- /dev/null +++ b/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts @@ -0,0 +1,21 @@ +/* + * 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 { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; + +export const PodNamesWithMetricsContext = React.createContext< + Map +>(new Map()); diff --git a/plugins/kubernetes/src/hooks/test-utils.tsx b/plugins/kubernetes/src/hooks/test-utils.tsx index c5b7a8f5d9..a7a6f42512 100644 --- a/plugins/kubernetes/src/hooks/test-utils.tsx +++ b/plugins/kubernetes/src/hooks/test-utils.tsx @@ -17,17 +17,25 @@ import React from 'react'; import { GroupedResponsesContext } from './GroupedResponses'; import { PodNamesWithErrorsContext } from './PodNamesWithErrors'; +import { PodNamesWithMetricsContext } from './PodNamesWithMetrics'; +import { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; export const kubernetesProviders = ( - groupedResponses: any, - podsWithErrors: any, + groupedResponses: any = undefined, + podsWithErrors: Set = new Set(), + podNameToMetrics: Map = new Map< + string, + ClientPodStatus + >(), ) => { return (node: React.ReactNode) => ( <> - - {node} - + + + {node} + + ); diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index d6df99cb95..d6ef8fa784 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -21,7 +21,7 @@ import { V1HorizontalPodAutoscaler, V1Service, V1ConfigMap, - ExtensionsV1beta1Ingress, + V1Ingress, V1Job, V1CronJob, } from '@kubernetes/client-node'; @@ -36,7 +36,7 @@ export interface DeploymentResources { export interface GroupedResponses extends DeploymentResources { services: V1Service[]; configMaps: V1ConfigMap[]; - ingresses: ExtensionsV1beta1Ingress[]; + ingresses: V1Ingress[]; jobs: V1Job[]; cronJobs: V1CronJob[]; customResources: any[]; diff --git a/plugins/kubernetes/src/utils/pod.test.tsx b/plugins/kubernetes/src/utils/pod.test.tsx new file mode 100644 index 0000000000..5822a16b86 --- /dev/null +++ b/plugins/kubernetes/src/utils/pod.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { currentToDeclaredResourceToPerc, podStatusToCpuUtil } from './pod'; +import { SubvalueCell } from '@backstage/core-components'; + +describe('pod', () => { + describe('currentToDeclaredResourceToPerc', () => { + it('10%', () => { + const tests: (number | string)[][] = [ + [10, 100], + [10, '100'], + ['10', 100], + ['10', '100'], + ]; + tests.forEach(([a, b]) => { + const result = currentToDeclaredResourceToPerc(a, b); + expect(result).toBe('10%'); + }); + }); + }); + describe('podStatusToCpuUtil', () => { + it('does use correct units', () => { + const result = podStatusToCpuUtil({ + cpu: { + // ~50m + currentUsage: 0.4966115, + // 50m + requestTotal: 0.05, + // 100m + limitTotal: 0.1, + }, + } as any); + expect(result).toStrictEqual( + , + ); + }); + }); +}); diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx index 8be2d839c3..bfb40750cd 100644 --- a/plugins/kubernetes/src/utils/pod.tsx +++ b/plugins/kubernetes/src/utils/pod.tsx @@ -14,16 +14,20 @@ * limitations under the License. */ -import { V1Pod, V1PodCondition } from '@kubernetes/client-node'; +import { + V1Pod, + V1PodCondition, + V1DeploymentCondition, +} from '@kubernetes/client-node'; import React, { Fragment, ReactNode } from 'react'; import { Chip } from '@material-ui/core'; -import { V1DeploymentCondition } from '@kubernetes/client-node/dist/gen/model/v1DeploymentCondition'; import { StatusAborted, StatusError, StatusOK, SubvalueCell, } from '@backstage/core-components'; +import { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; export const imageChips = (pod: V1Pod): ReactNode => { const containerStatuses = pod.status?.containerStatuses ?? []; @@ -102,3 +106,62 @@ export const renderCondition = ( } return [condition.type, ]; }; + +// visible for testing +export const currentToDeclaredResourceToPerc = ( + current: number | string, + resource: number | string, +): string => { + if (typeof current === 'number' && typeof resource === 'number') { + return `${Math.round((current / resource) * 100)}%`; + } + + const numerator: bigint = BigInt(current); + const denominator: bigint = BigInt(resource); + + return `${(numerator * BigInt(100)) / denominator}%`; +}; + +export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => { + const cpuUtil = podStatus.cpu; + + let currentUsage: number | string = cpuUtil.currentUsage; + + // current usage number for CPU is a different unit than request/limit total + // this might be a bug in the k8s library + if (typeof cpuUtil.currentUsage === 'number') { + currentUsage = cpuUtil.currentUsage / 10; + } + + return ( + + ); +}; + +export const podStatusToMemoryUtil = ( + podStatus: ClientPodStatus, +): ReactNode => { + const memUtil = podStatus.memory; + + return ( + + ); +}; diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 48ef9a178d..f9b5f6a6ea 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.2.31 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.2.30 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 009252b774..43aabc3e3f 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.2.30", + "version": "0.2.31", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,30 +34,31 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 89b77e9866..3b8ab0dd8a 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-newrelic +## 0.3.10 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.3.9 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index bd5e57ae66..b7be235b9e 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,21 +32,22 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 37c3320810..0576d644da 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org +## 0.3.30 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.3.29 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index a9146c8f7a..4e7f556547 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.3.29", + "version": "0.3.30", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,25 +22,26 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "qs": "^6.10.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index bb74598f2a..e0c6ec0f29 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-pagerduty +## 0.3.19 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.3.18 ### Patch Changes diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 689edae1e0..bd26531da6 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -10,7 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { PropsWithChildren } from 'react'; +import { ReactNode } from 'react'; // Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -65,12 +65,7 @@ export class PagerDutyClient implements PagerDutyApi { // Warning: (ae-forgotten-export) The symbol "TriggerAlarmRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - triggerAlarm({ - integrationKey, - source, - description, - userName, - }: TriggerAlarmRequest): Promise; + triggerAlarm(request: TriggerAlarmRequest): Promise; } // Warning: (ae-missing-release-tag) "pagerDutyPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -84,9 +79,7 @@ export { pagerDutyPlugin as plugin }; // Warning: (ae-missing-release-tag) "TriggerButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TriggerButton({ - children, -}: PropsWithChildren): JSX.Element; +export function TriggerButton(props: TriggerButtonProps): JSX.Element; // Warning: (ae-missing-release-tag) "UnauthorizedError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index d02c2df57b..59e76878b5 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,26 +32,27 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.2.6", "luxon": "2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index c006403fe7..65ff83e7ba 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -91,12 +91,9 @@ export class PagerDutyClient implements PagerDutyApi { return oncalls; } - triggerAlarm({ - integrationKey, - source, - description, - userName, - }: TriggerAlarmRequest): Promise { + triggerAlarm(request: TriggerAlarmRequest): Promise { + const { integrationKey, source, description, userName } = request; + const body = JSON.stringify({ event_action: 'trigger', routing_key: integrationKey, diff --git a/plugins/pagerduty/src/components/TriggerButton/index.tsx b/plugins/pagerduty/src/components/TriggerButton/index.tsx index 93897c77be..6f83effcf4 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.tsx @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, PropsWithChildren, useState } from 'react'; +import React, { useCallback, ReactNode, useState } from 'react'; import { makeStyles, Button } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { usePagerdutyEntity } from '../../hooks'; import { TriggerDialog } from '../TriggerDialog'; -export type TriggerButtonProps = {}; +export type TriggerButtonProps = { + children?: ReactNode; +}; const useStyles = makeStyles(theme => ({ buttonStyle: { @@ -32,9 +34,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export function TriggerButton({ - children, -}: PropsWithChildren) { +export function TriggerButton(props: TriggerButtonProps) { const { buttonStyle } = useStyles(); const { integrationKey } = usePagerdutyEntity(); const [dialogShown, setDialogShown] = useState(false); @@ -56,7 +56,7 @@ export function TriggerButton({ disabled={disabled} > {integrationKey - ? children ?? 'Create Incident' + ? props.children ?? 'Create Incident' : 'Missing integration key'} {integrationKey && ( diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 5df0a5826a..cca070ae0c 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-permission-backend +## 0.2.1 + +### Patch Changes + +- a036b65c2f: Updated to use the new `BackstageIdentityResponse` type from `@backstage/plugin-auth-backend`. + + The `BackstageIdentityResponse` type is backwards compatible with the `BackstageIdentity`, and provides an additional `identity` field with the claims of the user. + +- Updated dependencies + - @backstage/plugin-auth-backend@0.5.0 + - @backstage/backend-common@0.9.13 + - @backstage/plugin-permission-node@0.2.1 + +## 0.2.0 + +### Minor Changes + +- 450ca92330: Change route used for integration between the authorization framework and other plugin backends to use the /.well-known prefix. + +### Patch Changes + +- e7851efa9e: Rename and adjust permission policy return type to reduce nesting +- Updated dependencies + - @backstage/plugin-auth-backend@0.4.10 + - @backstage/plugin-permission-node@0.2.0 + - @backstage/backend-common@0.9.12 + ## 0.1.0 ### Minor Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index f335079d0b..ed3bd86636 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.1.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,11 +19,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/config": "^0.1.11", - "@backstage/plugin-auth-backend": "^0.4.9", + "@backstage/plugin-auth-backend": "^0.5.0", "@backstage/plugin-permission-common": "^0.2.0", - "@backstage/plugin-permission-node": "^0.1.0", + "@backstage/plugin-permission-node": "^0.2.1", "@types/express": "*", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -33,7 +33,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index 6a35488117..7f46490ce2 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -64,7 +64,7 @@ describe('PermissionIntegrationClient', () => { server.listen({ onUnhandledRequest: 'error' }); server.use( rest.post( - `${mockBaseUrl}/permissions/apply-conditions`, + `${mockBaseUrl}/.well-known/backstage/permissions/apply-conditions`, mockApplyConditionsHandler, ), ); diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index 0d6017cfd3..de42a7f194 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -54,7 +54,7 @@ export class PermissionIntegrationClient { ): Promise { const endpoint = `${await this.discovery.getBaseUrl( pluginId, - )}/permissions/apply-conditions`; + )}/.well-known/backstage/permissions/apply-conditions`; const request: ApplyConditionsRequest = { resourceRef, diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b85e7feb85..41791b8fc8 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -23,7 +23,7 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { - BackstageIdentity, + BackstageIdentityResponse, IdentityClient, } from '@backstage/plugin-auth-backend'; import { @@ -71,7 +71,7 @@ export interface RouterOptions { const handleRequest = async ( { id, resourceRef, ...request }: Identified, - user: BackstageIdentity | undefined, + user: BackstageIdentityResponse | undefined, policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, authHeader?: string, diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 725aef0dec..495c262b03 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -45,7 +45,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 37c03b7050..3fdf0ccfff 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-permission-node +## 0.2.1 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- a036b65c2f: Updated to use the new `BackstageIdentityResponse` type from `@backstage/plugin-auth-backend`. + + The `BackstageIdentityResponse` type is backwards compatible with the `BackstageIdentity`, and provides an additional `identity` field with the claims of the user. + +- Updated dependencies + - @backstage/plugin-auth-backend@0.5.0 + +## 0.2.0 + +### Minor Changes + +- e7851efa9e: Rename and adjust permission policy return type to reduce nesting +- 450ca92330: Change route used for integration between the authorization framework and other plugin backends to use the /.well-known prefix. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.4.10 + ## 0.1.0 ### Minor Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 75c45a9250..3df1386d4d 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -5,7 +5,7 @@ ```ts import { AuthorizeRequest } from '@backstage/plugin-permission-common'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { Router } from 'express'; @@ -83,11 +83,7 @@ export const createConditionTransformer: < ) => ConditionTransformer; // @public -export const createPermissionIntegrationRouter: ({ - resourceType, - rules, - getResource, -}: { +export const createPermissionIntegrationRouter: (options: { resourceType: string; rules: PermissionRule[]; getResource: (resourceRef: string) => Promise; @@ -98,7 +94,7 @@ export interface PermissionPolicy { // (undocumented) handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 2bddc21aca..5e0a300873 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.1.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,14 +29,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/plugin-auth-backend": "^0.4.9", + "@backstage/plugin-auth-backend": "^0.5.0", "@backstage/plugin-permission-common": "^0.2.0", "@types/express": "^4.17.6", "express": "^4.17.1", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index af216dfcb8..15c9cf4003 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -61,7 +61,7 @@ describe('createPermissionIntegrationRouter', () => { expect(router).toBeDefined(); }); - describe('POST /permissions/apply-conditions', () => { + describe('POST /.well-known/backstage/permissions/apply-conditions', () => { it.each([ { rule: 'test-rule-1', params: ['abc', 123] }, { @@ -94,7 +94,7 @@ describe('createPermissionIntegrationRouter', () => { }, ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { const response = await request(app) - .post('/permissions/apply-conditions') + .post('/.well-known/backstage/permissions/apply-conditions') .send({ resourceRef: 'default:test/resource', resourceType: 'test-resource', @@ -135,7 +135,7 @@ describe('createPermissionIntegrationRouter', () => { 'returns 200/DENY when criteria do not match (case %#)', async conditions => { const response = await request(app) - .post('/permissions/apply-conditions') + .post('/.well-known/backstage/permissions/apply-conditions') .send({ resourceRef: 'default:test/resource', resourceType: 'test-resource', @@ -149,7 +149,7 @@ describe('createPermissionIntegrationRouter', () => { it('returns 400 when called with incorrect resource type', async () => { const response = await request(app) - .post('/permissions/apply-conditions') + .post('/.well-known/backstage/permissions/apply-conditions') .send({ resourceRef: 'default:test/resource', resourceType: 'test-incorrect-resource', @@ -168,7 +168,7 @@ describe('createPermissionIntegrationRouter', () => { mockGetResource.mockReturnValueOnce(Promise.resolve(undefined)); const response = await request(app) - .post('/permissions/apply-conditions') + .post('/.well-known/backstage/permissions/apply-conditions') .send({ resourceRef: 'default:test/resource', resourceType: 'test-resource', @@ -198,7 +198,7 @@ describe('createPermissionIntegrationRouter', () => { { conditions: { anyOf: [] } }, ])(`returns 400 for invalid input %#`, async input => { const response = await request(app) - .post('/permissions/apply-conditions') + .post('/.well-known/backstage/permissions/apply-conditions') .send(input); expect(response.status).toEqual(400); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 99e282b144..b8d3e02a81 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -116,23 +116,21 @@ const applyConditions = ( * This is used to construct the `createPermissionIntegrationRouter`, a function to add an * authorization route to your backend plugin. This route will be called by the `permission-backend` * when authorization conditions relating to this plugin need to be evaluated. + * * @public */ -export const createPermissionIntegrationRouter = ({ - resourceType, - rules, - getResource, -}: { +export const createPermissionIntegrationRouter = (options: { resourceType: string; rules: PermissionRule[]; getResource: (resourceRef: string) => Promise; }): Router => { + const { resourceType, rules, getResource } = options; const router = Router(); const getRule = createGetRule(rules); router.post( - '/permissions/apply-conditions', + '/.well-known/backstage/permissions/apply-conditions', express.json(), async ( req, diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 3548d051f6..d122ad8d8d 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; /** * An authorization request to be evaluated by the {@link PermissionPolicy}. @@ -83,6 +83,6 @@ export type PolicyDecision = export interface PermissionPolicy { handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } diff --git a/plugins/permission-react/.eslintrc.js b/plugins/permission-react/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/permission-react/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md new file mode 100644 index 0000000000..44c5a326d6 --- /dev/null +++ b/plugins/permission-react/CHANGELOG.md @@ -0,0 +1,23 @@ +# @backstage/plugin-permission-react + +## 0.1.1 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-plugin-api@0.3.0 + +## 0.1.0 + +### Minor Changes + +- 6ed24445a9: Add @backstage/plugin-permission-react + + @backstage/plugin-permission-react is a library containing utils for implementing permissions in your frontend Backstage plugins. See [the authorization PRFC](https://github.com/backstage/backstage/pull/7761) for more details. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.2.2 diff --git a/plugins/permission-react/README.md b/plugins/permission-react/README.md new file mode 100644 index 0000000000..72383f024d --- /dev/null +++ b/plugins/permission-react/README.md @@ -0,0 +1,5 @@ +# permission + +**NOTE: THIS PACKAGE IS EXPERIMENTAL!** + +Components and hooks to help implement permissions in Backstage frontend plugins. For more information, see the [authorization PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md new file mode 100644 index 0000000000..c10317495a --- /dev/null +++ b/plugins/permission-react/api-report.md @@ -0,0 +1,60 @@ +## API Report File for "@backstage/plugin-permission-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ApiRef } from '@backstage/core-plugin-api'; +import { AuthorizeRequest } from '@backstage/plugin-permission-common'; +import { AuthorizeResponse } from '@backstage/plugin-permission-common'; +import { ComponentProps } from 'react'; +import { Config } from '@backstage/config'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { Permission } from '@backstage/plugin-permission-common'; +import { ReactElement } from 'react'; +import { Route } from 'react-router'; + +// @public (undocumented) +export type AsyncPermissionResult = { + loading: boolean; + allowed: boolean; + error?: Error; +}; + +// @public +export class IdentityPermissionApi implements PermissionApi { + // (undocumented) + authorize(request: AuthorizeRequest): Promise; + // (undocumented) + static create(options: { + configApi: Config; + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }): IdentityPermissionApi; +} + +// @public +export type PermissionApi = { + authorize(request: AuthorizeRequest): Promise; +}; + +// @public +export const permissionApiRef: ApiRef; + +// @public +export const PermissionedRoute: ( + props: ComponentProps & { + permission: Permission; + resourceRef?: string; + errorComponent?: ReactElement | null; + }, +) => JSX.Element; + +// @public +export const usePermission: ( + permission: Permission, + resourceRef?: string | undefined, +) => AsyncPermissionResult; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json new file mode 100644 index 0000000000..7f49c5eb73 --- /dev/null +++ b/plugins/permission-react/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-permission-react", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/permission-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/config": "^0.1.11", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-permission-common": "^0.2.0", + "cross-fetch": "^3.0.6", + "react-router": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@types/jest": "^26.0.7" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-react/src/apis/IdentityPermissionApi.ts b/plugins/permission-react/src/apis/IdentityPermissionApi.ts new file mode 100644 index 0000000000..19de564f84 --- /dev/null +++ b/plugins/permission-react/src/apis/IdentityPermissionApi.ts @@ -0,0 +1,53 @@ +/* + * 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 { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { PermissionApi } from './PermissionApi'; +import { + AuthorizeRequest, + AuthorizeResponse, + PermissionClient, +} from '@backstage/plugin-permission-common'; +import { Config } from '@backstage/config'; + +/** + * The default implementation of the PermissionApi, which simply calls the authorize method of the given + * {@link @backstage/plugin-permission-common#PermissionClient}. + * @public + */ +export class IdentityPermissionApi implements PermissionApi { + private constructor( + private readonly permissionClient: PermissionClient, + private readonly identityApi: IdentityApi, + ) {} + + static create(options: { + configApi: Config; + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + const { configApi, discoveryApi, identityApi } = options; + const permissionClient = new PermissionClient({ discoveryApi, configApi }); + return new IdentityPermissionApi(permissionClient, identityApi); + } + + async authorize(request: AuthorizeRequest): Promise { + const response = await this.permissionClient.authorize([request], { + token: await this.identityApi.getIdToken(), + }); + return response[0]; + } +} diff --git a/plugins/permission-react/src/apis/PermissionApi.ts b/plugins/permission-react/src/apis/PermissionApi.ts new file mode 100644 index 0000000000..5c224ff06d --- /dev/null +++ b/plugins/permission-react/src/apis/PermissionApi.ts @@ -0,0 +1,40 @@ +/* + * 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 { + AuthorizeRequest, + AuthorizeResponse, +} from '@backstage/plugin-permission-common'; +import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; + +/** + * This API is used by various frontend utilities that allow developers to implement authorization wihtin their frontend + * plugins. A plugin developer will likely not have to interact with this API or its implementations directly, but + * rather with the aforementioned utility components/hooks. + * @public + */ +export type PermissionApi = { + authorize(request: AuthorizeRequest): Promise; +}; + +/** + * A Backstage ApiRef for the Permission API. See https://backstage.io/docs/api/utility-apis for more information on + * Backstage ApiRefs. + * @public + */ +export const permissionApiRef: ApiRef = createApiRef({ + id: 'plugin.permission.api', +}); diff --git a/plugins/permission-react/src/apis/index.ts b/plugins/permission-react/src/apis/index.ts new file mode 100644 index 0000000000..223339f891 --- /dev/null +++ b/plugins/permission-react/src/apis/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { permissionApiRef } from './PermissionApi'; +export type { PermissionApi } from './PermissionApi'; +export { IdentityPermissionApi } from './IdentityPermissionApi'; diff --git a/plugins/permission-react/src/components/PermissionedRoute.test.tsx b/plugins/permission-react/src/components/PermissionedRoute.test.tsx new file mode 100644 index 0000000000..3d7d1b7af7 --- /dev/null +++ b/plugins/permission-react/src/components/PermissionedRoute.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 { PermissionedRoute } from '.'; +import { usePermission } from '../hooks'; +import { renderInTestApp } from '@backstage/test-utils'; + +jest.mock('../hooks', () => ({ + usePermission: jest.fn(), +})); +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + +const permission = { + name: 'access.something', + attributes: { action: 'read' as const }, +}; + +describe('PermissionedRoute', () => { + it('Does not render when loading', async () => { + mockUsePermission.mockReturnValue({ loading: true, allowed: false }); + + const { queryByText } = await renderInTestApp( + content} + />, + ); + + expect(queryByText('content')).not.toBeTruthy(); + }); + + it('Renders given element if authorized', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + + const { getByText } = await renderInTestApp( + content} + />, + ); + + expect(getByText('content')).toBeTruthy(); + }); + + it('Renders not found page if not authorized', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + + await expect( + renderInTestApp( + content} + />, + ), + ).rejects.toThrowError('Reached NotFound Page'); + }); + + it('Renders custom error page if not authorized', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + + const { getByText } = await renderInTestApp( + content} + errorComponent={

Custom Error

} + />, + ); + + expect(getByText('Custom Error')).toBeTruthy(); + }); +}); diff --git a/plugins/permission-react/src/components/PermissionedRoute.tsx b/plugins/permission-react/src/components/PermissionedRoute.tsx new file mode 100644 index 0000000000..76017308aa --- /dev/null +++ b/plugins/permission-react/src/components/PermissionedRoute.tsx @@ -0,0 +1,51 @@ +/* + * 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, { ComponentProps, ReactElement } from 'react'; +import { Route } from 'react-router'; +import { useApp } from '@backstage/core-plugin-api'; +import { usePermission } from '../hooks'; +import { Permission } from '@backstage/plugin-permission-common'; + +/** + * Returns a React Router Route which only renders the element when authorized. If unauthorized, the Route will render a + * NotFoundErrorPage (see {@link @backstage/core-app-api#AppComponents}). + * + * @public + */ +export const PermissionedRoute = ( + props: ComponentProps & { + permission: Permission; + resourceRef?: string; + errorComponent?: ReactElement | null; + }, +) => { + const { permission, resourceRef, errorComponent, ...otherProps } = props; + const permissionResult = usePermission(permission, resourceRef); + const app = useApp(); + const { NotFoundErrorPage } = app.getComponents(); + + let shownElement: ReactElement | null | undefined = + errorComponent === undefined ? : errorComponent; + + if (permissionResult.loading) { + shownElement = null; + } else if (permissionResult.allowed) { + shownElement = props.element; + } + + return ; +}; diff --git a/plugins/permission-react/src/components/index.ts b/plugins/permission-react/src/components/index.ts new file mode 100644 index 0000000000..05f13bca81 --- /dev/null +++ b/plugins/permission-react/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { PermissionedRoute } from './PermissionedRoute'; diff --git a/plugins/permission-react/src/hooks/index.ts b/plugins/permission-react/src/hooks/index.ts new file mode 100644 index 0000000000..5fca80b89e --- /dev/null +++ b/plugins/permission-react/src/hooks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { usePermission } from './usePermission'; +export type { AsyncPermissionResult } from './usePermission'; diff --git a/plugins/permission-react/src/hooks/usePermission.test.tsx b/plugins/permission-react/src/hooks/usePermission.test.tsx new file mode 100644 index 0000000000..4cf0bc6530 --- /dev/null +++ b/plugins/permission-react/src/hooks/usePermission.test.tsx @@ -0,0 +1,87 @@ +/* + * 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, { FC } from 'react'; +import { render } from '@testing-library/react'; +import { usePermission } from './usePermission'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { permissionApiRef } from '../apis'; + +const mockAuthorize = jest.fn(); + +const permission = { + name: 'access.something', + attributes: { action: 'read' as const }, +}; + +const TestComponent: FC = () => { + const { loading, allowed, error } = usePermission(permission); + return ( +
+ {loading && 'loading'} + {error && 'error'} + {allowed ? 'content' : null} +
+ ); +}; + +describe('usePermission', () => { + it('Returns loading when permissionApi has not yet responded.', () => { + mockAuthorize.mockReturnValueOnce(new Promise(() => {})); + + const { getByText } = render( + + + , + ); + + expect(mockAuthorize).toHaveBeenCalledWith({ permission }); + expect(getByText('loading')).toBeTruthy(); + }); + + it('Returns allowed when permissionApi allows authorization.', async () => { + mockAuthorize.mockResolvedValueOnce({ result: AuthorizeResult.ALLOW }); + + const { findByText } = render( + + + , + ); + + expect(mockAuthorize).toHaveBeenCalledWith({ permission }); + expect(await findByText('content')).toBeTruthy(); + }); + + it('Returns not allowed when permissionApi denies authorization.', async () => { + mockAuthorize.mockResolvedValueOnce({ result: AuthorizeResult.DENY }); + + const { findByText } = render( + + + , + ); + + expect(mockAuthorize).toHaveBeenCalledWith({ permission }); + await expect(findByText('content')).rejects.toThrowError(); + }); +}); diff --git a/plugins/permission-react/src/hooks/usePermission.ts b/plugins/permission-react/src/hooks/usePermission.ts new file mode 100644 index 0000000000..129904d7df --- /dev/null +++ b/plugins/permission-react/src/hooks/usePermission.ts @@ -0,0 +1,60 @@ +/* + * 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 { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; +import { permissionApiRef } from '../apis'; +import { + AuthorizeResult, + Permission, +} from '@backstage/plugin-permission-common'; + +/** @public */ +export type AsyncPermissionResult = { + loading: boolean; + allowed: boolean; + error?: Error; +}; + +/** + * React hook utlity for authorization. Given a {@link @backstage/plugin-permission-common#Permission} and an optional + * resourceRef, it will return whether or not access is allowed (for the given resource, if resourceRef is provided). See + * {@link @backstage/plugin-permission-common/PermissionClient#authorize} for more details. + * @public + */ +export const usePermission = ( + permission: Permission, + resourceRef?: string, +): AsyncPermissionResult => { + const permissionApi = useApi(permissionApiRef); + + const { loading, error, value } = useAsync(async () => { + const { result } = await permissionApi.authorize({ + permission, + resourceRef, + }); + + return result; + }, [permissionApi, permission, resourceRef]); + + if (loading) { + return { loading: true, allowed: false }; + } + if (error) { + return { error, loading: false, allowed: false }; + } + return { loading: false, allowed: value === AuthorizeResult.ALLOW }; +}; diff --git a/plugins/permission-react/src/index.ts b/plugins/permission-react/src/index.ts new file mode 100644 index 0000000000..57352c5cdd --- /dev/null +++ b/plugins/permission-react/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './components'; +export * from './hooks'; +export * from './apis'; diff --git a/plugins/permission-react/src/setupTests.ts b/plugins/permission-react/src/setupTests.ts new file mode 100644 index 0000000000..992b60d3a4 --- /dev/null +++ b/plugins/permission-react/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 78586449c7..68be26391e 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.8", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -43,7 +43,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index bb30692476..96c1002e5e 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index aee0a74525..8368deee5e 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-rollbar +## 0.3.20 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.3.19 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 0e4086bd4a..d6a14a5b2a 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.3.19", + "version": "0.3.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,33 +33,34 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 43a522482f..b177521d8b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.1.5 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/plugin-scaffolder-backend@0.15.15 + - @backstage/backend-common@0.9.12 + ## 0.1.4 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 95dab84e88..bff3f49c83 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.12", "@backstage/errors": "^0.1.4", - "@backstage/integration": "^0.6.9", - "@backstage/plugin-scaffolder-backend": "^0.15.13", + "@backstage/integration": "^0.6.10", + "@backstage/plugin-scaffolder-backend": "^0.15.15", "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", @@ -32,7 +32,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.0", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 9dcd6bcb88..5893faedbb 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.2.0 + +### Minor Changes + +- 64db0efffe: update publish format from ESM to CJS + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.16 + - @backstage/backend-common@0.9.13 + ## 0.1.7 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 37b077568e..28dc5f7ba6 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,19 +1,19 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.1.7", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, "publishConfig": { "access": "public", - "main": "dist/index.esm.js", + "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", + "build": "backstage-cli backend:build", + "start": "backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", @@ -21,17 +21,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/plugin-scaffolder-backend": "^0.15.13", + "@backstage/backend-common": "^0.9.13", + "@backstage/plugin-scaffolder-backend": "^0.15.16", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.4", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 092a678fce..3986961f16 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend +## 0.15.16 + +### Patch Changes + +- 2a3fb13718: Bump esbuild to ^0.14.1 +- Updated dependencies + - @backstage/backend-common@0.9.13 + - @backstage/plugin-catalog-backend@0.19.1 + +## 0.15.15 + +### Patch Changes + +- 0398ea25d3: Removed unused scaffolder visibility configuration; this has been moved to publish actions. Deprecated scaffolder provider configuration keys; these should use the integrations configuration instead. +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- c6b44d80ad: Add options to spawn in runCommand helper +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/plugin-catalog-backend@0.19.0 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.5 + - @backstage/backend-common@0.9.12 + ## 0.15.14 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 602028f9b3..2dae658d74 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.14", + "version": "0.15.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,15 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", - "@backstage/plugin-catalog-backend": "^0.18.0", + "@backstage/integration": "^0.6.10", + "@backstage/plugin-catalog-backend": "^0.19.1", "@backstage/plugin-scaffolder-common": "^0.1.1", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.4", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.5", "@backstage/types": "^0.1.1", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^34.6.0", @@ -73,15 +73,15 @@ "vm2": "^3.9.5" }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", - "esbuild": "^0.13.14", + "esbuild": "^0.14.1", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", "msw": "^0.35.0", diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 05d5ae3031..9099eda99d 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -40,6 +40,6 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.0" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index e809ae6c71..55ccd3e737 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-scaffolder +## 0.11.14 + +### Patch Changes + +- 6845cce533: Can specify allowedOwners to the RepoUrlPicker picker in a template definition +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- 2edcf7738f: Fix bug with setting owner in RepoUrlPicker causing validation failure +- b291c3176e: Switch to using `LogViewer` component from `@backstage/core-components` to display scaffolder logs. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.11.13 + +### Patch Changes + +- ed5bef529e: Add group filtering to the scaffolder page so that individuals can surface specific templates to end users ahead of others, or group templates together. This can be accomplished by passing in a `groups` prop to the `ScaffolderPage` + + ``` + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + ``` + +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.11.12 ### Patch Changes diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 3a749792f1..f7c6aa62fc 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -17,7 +17,11 @@ import { CatalogClient } from '@backstage/catalog-client'; import { createDevApp } from '@backstage/dev-utils'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + starredEntitiesApiRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; @@ -25,14 +29,25 @@ import { configApiRef, discoveryApiRef, identityApiRef, + storageApiRef, } from '@backstage/core-plugin-api'; +import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() + .addPage({ + path: '/catalog/:kind/:namespace/:name', + element: , + }) .registerApi({ api: catalogApiRef, deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), }) + .registerApi({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + }) .registerApi({ api: scaffolderApiRef, deps: { diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 2cd341d5b9..e3aaa722b0 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.11.12", + "version": "0.11.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,20 +34,19 @@ "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/integration": "^0.6.10", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", "@rjsf/material-ui": "^3.2.1", - "@types/react": "*", "classnames": "^2.2.6", "git-url-parse": "^11.6.0", "humanize-duration": "^3.25.1", @@ -56,20 +55,22 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "use-immer": "^0.6.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 11d12282d2..58ec3dfc02 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -20,7 +20,7 @@ import { Header, Lifecycle, Page, - Progress, + LogViewer, } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import { @@ -40,15 +40,13 @@ import Check from '@material-ui/icons/Check'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import classNames from 'classnames'; import { DateTime, Interval } from 'luxon'; -import React, { memo, Suspense, useEffect, useMemo, useState } from 'react'; +import React, { memo, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { useInterval } from 'react-use'; import { Status, TaskOutput } from '../../types'; import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskPageLinks } from './TaskPageLinks'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); - // typings are wrong for this library, so fallback to not parsing types. const humanizeDuration = require('humanize-duration'); @@ -213,22 +211,6 @@ export const TaskStatusStepper = memo( }, ); -const TaskLogger = memo(({ log }: { log: string }) => { - return ( - }> -
- -
-
- ); -}); - const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean => !!(entityRef || remoteUrl || links.length > 0); @@ -318,7 +300,9 @@ export const TaskPage = () => { - +
+ +
diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 60e9ee8840..7ca65fff25 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -20,9 +20,8 @@ import { } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { fireEvent, within } from '@testing-library/react'; +import { act, fireEvent, within } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { MemoryRouter, Route } from 'react-router'; import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index f862fa66fe..c4b1e79262 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -27,7 +27,7 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import { useApi } from '@backstage/core-plugin-api'; import { Progress } from '@backstage/core-components'; -function splitFormData(url: string | undefined) { +function splitFormData(url: string | undefined, allowedOwners?: string[]) { let host = undefined; let owner = undefined; let repo = undefined; @@ -39,7 +39,7 @@ function splitFormData(url: string | undefined) { if (url) { const parsed = new URL(`https://${url}`); host = parsed.host; - owner = parsed.searchParams.get('owner') || undefined; + owner = parsed.searchParams.get('owner') || allowedOwners?.[0]; repo = parsed.searchParams.get('repo') || undefined; // This is azure dev ops specific. not used for any other provider. organization = parsed.searchParams.get('organization') || undefined; @@ -95,13 +95,16 @@ export const RepoUrlPicker = ({ const scaffolderApi = useApi(scaffolderApiRef); const integrationApi = useApi(scmIntegrationsApiRef); const allowedHosts = uiSchema['ui:options']?.allowedHosts as string[]; + const allowedOwners = uiSchema['ui:options']?.allowedOwners as string[]; const { value: integrations, loading } = useAsync(async () => { return await scaffolderApi.getIntegrationsList({ allowedHosts }); }); - const { host, owner, repo, organization, workspace, project } = - splitFormData(formData); + const { host, owner, repo, organization, workspace, project } = splitFormData( + formData, + allowedOwners, + ); const updateHost = useCallback( (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { onChange( @@ -300,21 +303,57 @@ export const RepoUrlPicker = ({ )} {/* Show this for all hosts except bitbucket */} - {host && integrationApi.byHost(host)?.type !== 'bitbucket' && ( - <> - 0 && !owner} - > - Owner - - - The organization, user or project that this repo will belong to - - - - )} + {host && + integrationApi.byHost(host)?.type !== 'bitbucket' && + !allowedOwners && ( + <> + 0 && !owner} + > + Owner + + + The organization, user or project that this repo will belong to + + + + )} + {/* Show this for all hosts except bitbucket where allowed owner is set */} + {host && + integrationApi.byHost(host)?.type !== 'bitbucket' && + allowedOwners && ( + <> + 0 && !owner} + > + Owner Available + + + The organization, user or project that this repo will belong to + + + + )} {/* Show this for all hosts */} ; + static fromConfig( + options: ElasticSearchOptions, + ): Promise; // (undocumented) index(type: string, documents: IndexableDocument[]): Promise; // (undocumented) @@ -41,11 +36,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { // Warning: (ae-forgotten-export) The symbol "ConcreteElasticSearchQuery" needs to be exported by the entry point index.d.ts // // (undocumented) - protected translator({ - term, - filters, - types, - pageCursor, - }: SearchQuery): ConcreteElasticSearchQuery; + protected translator(query: SearchQuery): ConcreteElasticSearchQuery; } ``` diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 5ebc7850a9..633af83ca6 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.5", + "version": "0.0.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/cli": "^0.9.0", + "@backstage/backend-common": "^0.9.13", + "@backstage/cli": "^0.10.1", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 1a41099c9c..7912c691b3 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -64,6 +64,9 @@ function isBlank(str: string) { return (isEmpty(str) && !isNumber(str)) || nan(str); } +/** + * @public + */ export class ElasticSearchSearchEngine implements SearchEngine { constructor( private readonly elasticSearchClient: Client, @@ -72,12 +75,14 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly logger: Logger, ) {} - static async fromConfig({ - logger, - config, - aliasPostfix = `search`, - indexPrefix = ``, - }: ElasticSearchOptions) { + static async fromConfig(options: ElasticSearchOptions) { + const { + logger, + config, + aliasPostfix = `search`, + indexPrefix = ``, + } = options; + return new ElasticSearchSearchEngine( await ElasticSearchSearchEngine.constructElasticSearchClient( logger, @@ -164,12 +169,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { }); } - protected translator({ - term, - filters = {}, - types, - pageCursor, - }: SearchQuery): ConcreteElasticSearchQuery { + protected translator(query: SearchQuery): ConcreteElasticSearchQuery { + const { term, filters = {}, types, pageCursor } = query; + const filter = Object.entries(filters) .filter(([_, value]) => Boolean(value)) .map(([key, value]: [key: string, value: any]) => { @@ -190,7 +192,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { 'Failed to add filters to query. Unrecognized filter type', ); }); - const query = isBlank(term) + const esbQuery = isBlank(term) ? esb.matchAllQuery() : esb .multiMatchQuery(['*'], term) @@ -202,7 +204,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { return { elasticSearchQuery: esb .requestBodySearch() - .query(esb.boolQuery().filter(filter).must([query])) + .query(esb.boolQuery().filter(filter).must([esbQuery])) .from(page * pageSize) .size(pageSize) .toJSON(), diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index daba328467..84851ee352 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-pg +## 0.2.2 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + ## 0.2.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index 7a171127a3..e2edfc291a 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -77,9 +77,7 @@ export interface DatabaseStore { export class PgSearchEngine implements SearchEngine { constructor(databaseStore: DatabaseStore); // (undocumented) - static from({ - database, - }: { + static from(options: { database: PluginDatabaseManager; }): Promise; // (undocumented) diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 74e9698ea4..b0b910a5e7 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,15 +20,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.13", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "lodash": "^4.17.21", "knex": "^0.95.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.0" + "@backstage/backend-test-utils": "^0.1.10", + "@backstage/cli": "^0.10.1" }, "files": [ "dist", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index a032dd783e..6fd6c571a8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -35,13 +35,11 @@ export type ConcretePgSearchQuery = { export class PgSearchEngine implements SearchEngine { constructor(private readonly databaseStore: DatabaseStore) {} - static async from({ - database, - }: { + static async from(options: { database: PluginDatabaseManager; }): Promise { return new PgSearchEngine( - await DatabaseDocumentStore.create(await database.getClient()), + await DatabaseDocumentStore.create(await options.database.getClient()), ); } diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 369c52a4f6..9774a1081b 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -26,8 +26,8 @@ "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/cli": "^0.9.0" + "@backstage/backend-common": "^0.9.12", + "@backstage/cli": "^0.10.0" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index bf0c2df174..5b23d0c670 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend +## 0.2.8 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + ## 0.2.7 ### Patch Changes diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index f2479610fe..cac97bd725 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -7,12 +7,16 @@ import express from 'express'; import { Logger as Logger_2 } from 'winston'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -// Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createRouter({ - engine, - logger, -}: RouterOptions): Promise; +export function createRouter(options: RouterOptions): Promise; + +// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RouterOptions = { + engine: SearchEngine; + logger: Logger_2; +}; ``` diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 3767944a25..1c749cbef4 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "@types/express": "^4.17.6", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 6df39ca1eb..5bd99988a7 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -20,15 +20,15 @@ import { Logger } from 'winston'; import { SearchQuery, SearchResultSet } from '@backstage/search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -type RouterOptions = { +export type RouterOptions = { engine: SearchEngine; logger: Logger; }; -export async function createRouter({ - engine, - logger, -}: RouterOptions): Promise { +export async function createRouter( + options: RouterOptions, +): Promise { + const { engine, logger } = options; const router = Router(); router.get( '/query', diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index ef3736b1df..3cfb9093a8 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-search +## 0.5.1 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- 382e3a94b3: Export SearchApi interface from plugin +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.5.0 + +### Minor Changes + +- c5b6045f36: Search Modal now relies on the Search Context to access state and state setter. If you use the SidebarSearchModal as described in the [getting started documentation](https://backstage.io/docs/features/search/getting-started#using-the-search-modal), make sure to update your code with the SearchContextProvider. + + ```diff + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + - + + + + + + + + ... + ``` + +### Patch Changes + +- f06ecd09a7: Add optional icon and secondaryAction properties for DefaultResultListItem component +- c5941d5c30: Add a new optional clearButton property to the SearchBar component. The default value for this new property is true. +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.4.18 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 39b694d924..3db26579e3 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -74,7 +74,14 @@ export const HomePageSearchBar: ({ // @public (undocumented) export const Router: () => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "SearchApi" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SearchApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchApi { + // (undocumented) + query(query: SearchQuery): Promise; +} + // Warning: (ae-missing-release-tag) "searchApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -221,7 +228,7 @@ export const useSearch: () => SearchContextValue; // Warnings were encountered during analysis: // -// src/components/SearchContext/SearchContext.d.ts:21:5 - (ae-forgotten-export) The symbol "SettableSearchContext" needs to be exported by the entry point index.d.ts +// src/components/SearchContext/SearchContext.d.ts:23:5 - (ae-forgotten-export) The symbol "SettableSearchContext" needs to be exported by the entry point index.d.ts // src/components/SearchFilter/SearchFilter.d.ts:13:5 - (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // src/components/SearchFilter/SearchFilter.d.ts:14:5 - (ae-forgotten-export) The symbol "Component" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/search/package.json b/plugins/search/package.json index a032f5ef26..dbef8e5608 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.4.18", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,29 +32,30 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.6.4", + "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/search-common": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 91395d1f0b..5721812059 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -34,6 +34,9 @@ describe('apis', () => { getUserId: jest.fn(), getProfile: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }); const client = new SearchClient({ diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index 11d6a2786e..dd9310d782 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -37,6 +37,8 @@ type SearchContextValue = { setTypes: React.Dispatch>; filters: JsonObject; setFilters: React.Dispatch>; + open?: boolean; + toggleModal: () => void; pageCursor?: string; setPageCursor: React.Dispatch>; fetchNextPage?: React.DispatchWithoutAction; @@ -49,6 +51,7 @@ type SettableSearchContext = Omit< | 'setTerm' | 'setTypes' | 'setFilters' + | 'toggleModal' | 'setPageCursor' | 'fetchNextPage' | 'fetchPreviousPage' @@ -74,6 +77,12 @@ export const SearchContextProvider = ({ const [filters, setFilters] = useState(initialState.filters); const [term, setTerm] = useState(initialState.term); const [types, setTypes] = useState(initialState.types); + const [open, setOpen] = useState(false); + const toggleModal = useCallback( + (): void => setOpen(prevState => !prevState), + [], + ); + const prevTerm = usePrevious(term); const result = useAsync( @@ -109,6 +118,8 @@ export const SearchContextProvider = ({ result, filters, setFilters, + open, + toggleModal, term, setTerm, types, diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 5eefad8e0d..6fa93c6588 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -14,28 +14,15 @@ * limitations under the License. */ -import React, { useState, ComponentType } from 'react'; +import React, { ComponentType } from 'react'; import { Button } from '@material-ui/core'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { wrapInTestApp } from '@backstage/test-utils'; import { SearchModal } from '../index'; +import { useSearch, SearchContextProvider } from '../SearchContext'; import { searchApiRef } from '../../apis'; import { rootRouteRef } from '../../plugin'; -export default { - title: 'Plugins/Search/SearchModal', - component: SearchModal, - decorators: [ - (Story: ComponentType<{}>) => - wrapInTestApp( - <> - - , - { mountedRoutes: { '/search': rootRouteRef } }, - ), - ], -}; - const mockSearchApi = { query: () => Promise.resolve({ @@ -70,16 +57,33 @@ const mockSearchApi = { const apiRegistry = () => ApiRegistry.from([[searchApiRef, mockSearchApi]]); +export default { + title: 'Plugins/Search/SearchModal', + component: SearchModal, + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + <> + + + + + + , + { mountedRoutes: { '/search': rootRouteRef } }, + ), + ], +}; + export const Default = () => { - const [open, setOpen] = useState(false); - const toggleModal = (): void => setOpen(prevState => !prevState); + const { open, toggleModal } = useSearch(); return ( - + <> - + ); }; diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index 4d4fa1a9fc..ae38526c9d 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React from 'react'; import SearchIcon from '@material-ui/icons/Search'; -import { SearchModal } from '../SearchModal'; import { SidebarItem } from '@backstage/core-components'; +import { SearchModal } from '../SearchModal'; +import { useSearch } from '../SearchContext'; export const SidebarSearchModal = () => { - const [open, setOpen] = useState(false); - const toggleModal = (): void => setOpen(prevState => !prevState); + const { open, toggleModal } = useSearch(); return ( <> diff --git a/plugins/search/src/components/util.test.tsx b/plugins/search/src/components/util.test.tsx index a5b82370e3..1b452a205e 100644 --- a/plugins/search/src/components/util.test.tsx +++ b/plugins/search/src/components/util.test.tsx @@ -16,11 +16,10 @@ import React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { useNavigateToQuery } from './util'; import { Routes, Route } from 'react-router-dom'; import { rootRouteRef } from '../plugin'; -import { act } from 'react-dom/test-utils'; const navigate = jest.fn(); jest.mock('react-router-dom', () => ({ diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 09cf204017..f5c58450fc 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -21,6 +21,7 @@ */ export { searchApiRef } from './apis'; +export type { SearchApi } from './apis'; export { Filters, FiltersButton, diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index cad08289ed..37bb975828 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.3.30 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.3.29 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 2ea32c4f22..41dc522e77 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.29", + "version": "0.3.30", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,32 +33,33 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index fdd9f44231..7b9e21571f 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-shortcuts +## 0.1.15 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 0569585733..a706d43e0e 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.1.14", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,27 +21,28 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@types/zen-observable": "^0.8.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.12.2", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "uuid": "^8.3.2", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index 0b5c5fc919..e080026f00 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -37,7 +37,7 @@ describe('ShortcutItem', () => { it('displays the shortcut', async () => { await renderInTestApp( - + {} }}> , ); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index e0a70d6069..8cee9a6622 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -29,7 +29,7 @@ import { SidebarContext } from '@backstage/core-components'; describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( - + {} }}> { diff --git a/plugins/sonarqube/src/index.ts b/plugins/sonarqube/src/index.ts index dcf0417151..1e6f83aa49 100644 --- a/plugins/sonarqube/src/index.ts +++ b/plugins/sonarqube/src/index.ts @@ -14,6 +14,13 @@ * limitations under the License. */ +/** + * A Backstage plugin to display {@link https://www.sonarqube.org | SonarQube} + * code quality and security results. + * + * @packageDocumentation + */ + export * from './components'; export { sonarQubePlugin, diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index b6d77a50cc..bf7632ce5a 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.3.16 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.3.15 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index ac24c463c6..e5b55836a4 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.15", + "version": "0.3.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,25 +32,26 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "classnames": "^2.2.6", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 92bd7ebf33..c168df5472 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.2 + +### Patch Changes + +- c6c8b8e53e: Minor fixes in Readme to make the examples more directly usable. +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.0 + - @backstage/backend-common@0.9.12 + - @backstage/plugin-tech-insights-node@0.1.1 + ## 0.1.1 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index c0f992ffc7..108b1c8564 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -51,8 +51,10 @@ By default this implementation comes with an in-memory storage to store checks. Checks for this FactChecker are constructed as [`json-rules-engine` compatible JSON rules](https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#conditions). A check could look like the following for example: ```ts -import { TechInsightJsonRuleCheck } from '../types'; -import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; +import { + JSON_RULE_ENGINE_CHECK_TYPE, + TechInsightJsonRuleCheck, +} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; export const exampleCheck: TechInsightJsonRuleCheck = { id: 'demodatacheck', // Unique identifier of this check diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index c7086cf98c..fa65ba561a 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.5", - "@backstage/plugin-tech-insights-common": "^0.1.0", - "@backstage/plugin-tech-insights-node": "^0.1.0", + "@backstage/plugin-tech-insights-common": "^0.2.0", + "@backstage/plugin-tech-insights-node": "^0.1.1", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -43,7 +43,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", + "@backstage/cli": "^0.10.0", "@types/node-cron": "^2.0.4" }, "files": [ diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 0982a3cdc8..02267f1ee1 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-tech-insights-backend +## 0.1.3 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- b5bd60fddc: Removed unnecessary check for specific server error in `@backstage plugin-tech-insights-backend`. +- c6c8b8e53e: Minor fixes in Readme to make the examples more directly usable. +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.0 + - @backstage/backend-common@0.9.12 + - @backstage/plugin-tech-insights-node@0.1.1 + ## 0.1.2 ### Patch Changes diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 764ad860b8..31f5e402b0 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -42,7 +42,7 @@ export default async function createPlugin({ }); return await createRouter({ - ...(await builder.build()), + ...(await builder), logger, config, }); @@ -74,10 +74,10 @@ With the `techInsights.ts` router setup in place, add the router to At this point the Tech Insights backend is installed in your backend package, but you will not have any fact retrievers present in your application. To have the implemented FactRetrieverEngine within this package to be able to retrieve and store fact data into the database, you need to add these. -To create factRetrieverRegistration you need to implement `FactRetriever` interface defined in `@backstage/plugin-tech-insights-common` package. After you have implemented this interface you can wrap that into a registration object like follows: +To create factRetrieverRegistration you need to implement `FactRetriever` interface defined in `@backstage/plugin-tech-insights-node` package (see [Creating fact retrievers](#creating-fact-retrievers) for details). After you have implemented this interface you can wrap that into a registration object like follows: ```ts -import { createFactRetrieverRegistration } from './createFactRetriever'; +import { createFactRetrieverRegistration } from '@backstage/plugin-tech-insights-backend'; const myFactRetriever = { /** @@ -134,6 +134,8 @@ A Fact Retriever consist of four required and one optional parts: An example implementation of a FactRetriever could for example be as follows: ```ts +import { FactRetriever } from '@backstage/plugin-tech-insights-node'; + const myFactRetriever: FactRetriever = { id: 'documentation-number-factretriever', // unique identifier of the fact retriever version: '0.1.1', // SemVer version number of this fact retriever schema. This should be incremented if the implementation changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 5852702cd6..eb62d5b6a3 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.12", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.5", - "@backstage/plugin-tech-insights-common": "^0.1.0", - "@backstage/plugin-tech-insights-node": "^0.1.0", + "@backstage/plugin-tech-insights-common": "^0.2.0", + "@backstage/plugin-tech-insights-node": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -51,8 +51,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.1", + "@backstage/backend-test-utils": "^0.1.10", + "@backstage/cli": "^0.10.0", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index a8951318ea..80902c307c 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -92,11 +92,6 @@ export async function createRouter< router.post('/checks/run/:namespace/:kind/:name', async (req, res) => { const { namespace, kind, name } = req.params; try { - if (!('checks' in req.body)) { - return res.status(422).send({ - message: 'Failed to get checks from request.', - }); - } const { checks }: { checks: string[] } = req.body; const entityTriplet = stringifyEntityRef({ namespace, kind, name }); const checkResult = await factChecker.runChecks(entityTriplet, checks); diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md new file mode 100644 index 0000000000..9e72ce8ace --- /dev/null +++ b/plugins/tech-insights-common/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-tech-insights-common + +## 0.2.0 + +### Minor Changes + +- b5bd60fddc: Added new property 'result' in CheckResult in @backstage/plugin-tech-insights-common. This property is later used in `@backstage/plugin-tech-insights` package. diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md index ddb38cfa85..8618f6266b 100644 --- a/plugins/tech-insights-common/api-report.md +++ b/plugins/tech-insights-common/api-report.md @@ -4,6 +4,7 @@ ```ts import { DateTime } from 'luxon'; +import { JsonValue } from '@backstage/types'; // @public export interface BooleanCheckResult extends CheckResult { @@ -25,6 +26,7 @@ export interface CheckResponse { export type CheckResult = { facts: FactResponse; check: CheckResponse; + result: JsonValue; }; // @public diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 26d8922329..7bf668a24e 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-common", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,11 @@ }, "dependencies": { "@types/luxon": "^2.0.5", - "luxon": "^2.0.2" + "luxon": "^2.0.2", + "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts index 4bfc660748..ecdaa298c1 100644 --- a/plugins/tech-insights-common/src/index.ts +++ b/plugins/tech-insights-common/src/index.ts @@ -15,6 +15,7 @@ */ import { DateTime } from 'luxon'; +import { JsonValue } from '@backstage/types'; /** * @public @@ -112,6 +113,7 @@ export type FactResponse = { export type CheckResult = { facts: FactResponse; check: CheckResponse; + result: JsonValue; }; /** diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md new file mode 100644 index 0000000000..83a42bb41e --- /dev/null +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/plugin-tech-insights-node + +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.0 + - @backstage/backend-common@0.9.12 diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 3c2ba5da08..5ba94a2ff8 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,15 +30,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.8", - "@backstage/plugin-tech-insights-common": "^0.1.0 ", + "@backstage/plugin-tech-insights-common": "^0.2.0", "@types/luxon": "^2.0.5", "luxon": "^2.0.2", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.10.0" }, "files": [ "dist" diff --git a/plugins/tech-insights/.eslintrc.js b/plugins/tech-insights/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/tech-insights/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md new file mode 100644 index 0000000000..b05b3bf1a3 --- /dev/null +++ b/plugins/tech-insights/CHANGELOG.md @@ -0,0 +1,25 @@ +# @backstage/plugin-tech-insights + +## 0.1.1 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + +## 0.1.0 + +### Minor Changes + +- b5bd60fddc: New package containing UI components for the Tech Insights plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-tech-insights-common@0.2.0 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 diff --git a/plugins/tech-insights/README.md b/plugins/tech-insights/README.md new file mode 100644 index 0000000000..b61cea48ec --- /dev/null +++ b/plugins/tech-insights/README.md @@ -0,0 +1,47 @@ +# Tech Insights + +This plugin provides the UI for the `@backstage/tech-insights-backend` plugin, in order to display results of the checks running following the rules and the logic defined in the `@backstage/tech-insights-backend` plugin itself. + +Main areas covered by this plugin currently are: + +- Providing an overview for default boolean checks in a form of Scorecards. + +- Providing an option to render different custom components based on type of the checks running in the backend. + +## Installation + +### Install the plugin + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-tech-insights +``` + +### Add boolean checks overview (Scorecards) page to the EntityPage: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityTechInsightsScorecardContent } from '@backstage/plugin-tech-insights'; + +const serviceEntityPage = ( + + + {overviewContent} + + + {cicdContent} + + ... + + + + ... + +); +``` + +## Links + +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md new file mode 100644 index 0000000000..e7f30a2b30 --- /dev/null +++ b/plugins/tech-insights/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-tech-insights" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const EntityTechInsightsScorecardContent: () => JSX.Element; + +// @public (undocumented) +export const techInsightsPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights/dev/index.tsx b/plugins/tech-insights/dev/index.tsx new file mode 100644 index 0000000000..17b1fca00e --- /dev/null +++ b/plugins/tech-insights/dev/index.tsx @@ -0,0 +1,30 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { + techInsightsPlugin, + EntityTechInsightsScorecardContent, +} from '../src/plugin'; + +createDevApp() + .registerPlugin(techInsightsPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/tech-insight-scorecard', + }) + .render(); diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json new file mode 100644 index 0000000000..16e1ec6784 --- /dev/null +++ b/plugins/tech-insights/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-tech-insights", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4", + "react-router-dom": "6.0.0-beta.0", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-tech-insights-common": "^0.2.0", + "@backstage/catalog-model": "^0.9.7", + "@backstage/errors": "^0.1.4", + "@backstage/types": "^0.1.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "msw": "^0.35.0", + "cross-fetch": "^3.0.6" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts new file mode 100644 index 0000000000..2c55930f0a --- /dev/null +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -0,0 +1,35 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Check } from './types'; +import { CheckResultRenderer } from '../components/CheckResultRenderer'; +import { EntityName } from '@backstage/catalog-model'; + +export const techInsightsApiRef = createApiRef({ + id: 'plugin.techinsights.service', + description: 'Used by the tech insights plugin to make requests', +}); + +export interface TechInsightsApi { + getScorecardsDefinition: ( + type: string, + value: CheckResult[], + ) => CheckResultRenderer | undefined; + getAllChecks(): Promise; + runChecks(entityParams: EntityName, checks?: Check[]): Promise; +} diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts new file mode 100644 index 0000000000..25002e30a1 --- /dev/null +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -0,0 +1,82 @@ +/* + * 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 { TechInsightsApi } from './TechInsightsApi'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Check } from './types'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { EntityName } from '@backstage/catalog-model'; + +import { + CheckResultRenderer, + defaultCheckResultRenderers, +} from '../components/CheckResultRenderer'; + +export type Options = { + discoveryApi: DiscoveryApi; +}; + +export class TechInsightsClient implements TechInsightsApi { + private readonly discoveryApi: DiscoveryApi; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + } + + getScorecardsDefinition( + type: string, + value: CheckResult[], + ): CheckResultRenderer | undefined { + const resultRenderers = defaultCheckResultRenderers(value); + return resultRenderers.find(d => d.type === type); + } + + async getAllChecks(): Promise { + const url = await this.discoveryApi.getBaseUrl('tech-insights'); + const response = await fetch(`${url}/checks`); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + return await response.json(); + } + + async runChecks( + entityParams: EntityName, + checks: Check[], + ): Promise { + const url = await this.discoveryApi.getBaseUrl('tech-insights'); + const { namespace, kind, name } = entityParams; + const allChecks = checks ? checks : await this.getAllChecks(); + const checkIds = allChecks.map((check: Check) => check.id); + const response = await fetch( + `${url}/checks/run/${encodeURIComponent(namespace)}/${encodeURIComponent( + kind, + )}/${encodeURIComponent(name)}`, + { + method: 'POST', + body: JSON.stringify({ checks: checkIds }), + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + return await response.json(); + } +} diff --git a/plugins/tech-insights/src/api/index.ts b/plugins/tech-insights/src/api/index.ts new file mode 100644 index 0000000000..bcd2575a52 --- /dev/null +++ b/plugins/tech-insights/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './TechInsightsApi'; +export * from './TechInsightsClient'; diff --git a/plugins/tech-insights/src/api/types.ts b/plugins/tech-insights/src/api/types.ts new file mode 100644 index 0000000000..20071ba0c9 --- /dev/null +++ b/plugins/tech-insights/src/api/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Check = { + id: string; + type: string; + name: string; + description: string; + factIds: string[]; +}; diff --git a/plugins/tech-insights/src/components/BooleanCheck/BooleanCheck.tsx b/plugins/tech-insights/src/components/BooleanCheck/BooleanCheck.tsx new file mode 100644 index 0000000000..e38e0aa7b3 --- /dev/null +++ b/plugins/tech-insights/src/components/BooleanCheck/BooleanCheck.tsx @@ -0,0 +1,60 @@ +/* + * 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 { makeStyles, List, ListItem, ListItemText } from '@material-ui/core'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + listItemText: { + paddingRight: theme.spacing(0.5), + flex: '0 1 auto', + }, + icon: { + marginLeft: 'auto', + }, +})); + +type Prop = { + checkResult: CheckResult[]; +}; + +export const BooleanCheck = ({ checkResult }: Prop) => { + const classes = useStyles(); + + return ( + + {checkResult.map((check, index) => ( + + + {check.result ? ( + + ) : ( + + )} + + ))} + + ); +}; diff --git a/plugins/tech-insights/src/components/BooleanCheck/index.ts b/plugins/tech-insights/src/components/BooleanCheck/index.ts new file mode 100644 index 0000000000..729ab4d62e --- /dev/null +++ b/plugins/tech-insights/src/components/BooleanCheck/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BooleanCheck } from './BooleanCheck'; diff --git a/plugins/tech-insights/src/components/CheckResultRenderer.tsx b/plugins/tech-insights/src/components/CheckResultRenderer.tsx new file mode 100644 index 0000000000..60e0fad9c5 --- /dev/null +++ b/plugins/tech-insights/src/components/CheckResultRenderer.tsx @@ -0,0 +1,40 @@ +/* + * 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 { CheckResult } from '@backstage/plugin-tech-insights-common'; +import React from 'react'; +import { BooleanCheck } from './BooleanCheck'; + +export type CheckResultRenderer = { + type: string; + title: string; + description: string; + component: React.ReactElement; +}; + +export function defaultCheckResultRenderers( + value: CheckResult[], +): CheckResultRenderer[] { + return [ + { + type: 'json-rules-engine', + title: 'Boolean scorecard', + description: + 'This card represents an overview of default boolean Backstage checks:', + component: , + }, + ]; +} diff --git a/plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx b/plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx new file mode 100644 index 0000000000..10025d1ba6 --- /dev/null +++ b/plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx @@ -0,0 +1,68 @@ +/* + * 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 { makeStyles, Grid, Typography } from '@material-ui/core'; +import { useApi } from '@backstage/core-plugin-api'; +import { Content, Page, InfoCard } from '@backstage/core-components'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { techInsightsApiRef } from '../../api/TechInsightsApi'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + contentScorecards: { + paddingLeft: 0, + paddingRight: 0, + }, + subheader: { + fontWeight: 'bold', + paddingLeft: theme.spacing(0.5), + }, +})); + +type Checks = { + checks: CheckResult[]; +}; + +export const ChecksOverview = ({ checks }: Checks) => { + const classes = useStyles(); + const api = useApi(techInsightsApiRef); + const checkRenderType = api.getScorecardsDefinition( + checks[0].check.type, + checks, + ); + + if (checkRenderType) { + return ( + + + + + + {checkRenderType.description} + + + {checkRenderType.component} + + + + + + ); + } + + return <>; +}; diff --git a/plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx b/plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx new file mode 100644 index 0000000000..92a57bd643 --- /dev/null +++ b/plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx @@ -0,0 +1,41 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { Progress } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { ChecksOverview } from './ChecksOverview'; +import Alert from '@material-ui/lab/Alert'; +import { techInsightsApiRef } from '../../api/TechInsightsApi'; + +export const ScorecardsOverview = () => { + const api = useApi(techInsightsApiRef); + const { namespace, kind, name } = useParams(); + + const { value, loading, error } = useAsync( + async () => await api.runChecks({ namespace, kind, name }), + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; diff --git a/plugins/tech-insights/src/components/ScorecardsOverview/index.ts b/plugins/tech-insights/src/components/ScorecardsOverview/index.ts new file mode 100644 index 0000000000..64198790d9 --- /dev/null +++ b/plugins/tech-insights/src/components/ScorecardsOverview/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ScorecardsOverview } from './ScorecardsOverview'; diff --git a/plugins/tech-insights/src/index.ts b/plugins/tech-insights/src/index.ts new file mode 100644 index 0000000000..273c11fd71 --- /dev/null +++ b/plugins/tech-insights/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + techInsightsPlugin, + EntityTechInsightsScorecardContent, +} from './plugin'; diff --git a/plugins/tech-insights/src/plugin.test.ts b/plugins/tech-insights/src/plugin.test.ts new file mode 100644 index 0000000000..a4a9e6f308 --- /dev/null +++ b/plugins/tech-insights/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { techInsightsPlugin } from './plugin'; + +describe('tech insights', () => { + it('should export plugin', () => { + expect(techInsightsPlugin).toBeDefined(); + }); +}); diff --git a/plugins/tech-insights/src/plugin.ts b/plugins/tech-insights/src/plugin.ts new file mode 100644 index 0000000000..f229b654f3 --- /dev/null +++ b/plugins/tech-insights/src/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { + createPlugin, + createRoutableExtension, + createApiFactory, + discoveryApiRef, +} from '@backstage/core-plugin-api'; +import { rootRouteRef } from './routes'; +import { techInsightsApiRef } from './api/TechInsightsApi'; +import { TechInsightsClient } from './api/TechInsightsClient'; + +/** + * @public + */ +export const techInsightsPlugin = createPlugin({ + id: 'tech-insights', + apis: [ + createApiFactory({ + api: techInsightsApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new TechInsightsClient({ discoveryApi }), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +/** + * @public + */ +export const EntityTechInsightsScorecardContent = techInsightsPlugin.provide( + createRoutableExtension({ + name: 'EntityTechInsightsScorecardContent', + component: () => + import('./components/ScorecardsOverview').then(m => m.ScorecardsOverview), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/tech-insights/src/routes.ts b/plugins/tech-insights/src/routes.ts new file mode 100644 index 0000000000..b1b7900074 --- /dev/null +++ b/plugins/tech-insights/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'tech-insights', +}); diff --git a/plugins/tech-insights/src/setupTests.ts b/plugins/tech-insights/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/tech-insights/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index cef7885007..79f77679e3 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-tech-radar +## 0.4.13 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + ## 0.4.12 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 5b11fbbe7c..7d6fdb4afe 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.4.12", + "version": "0.4.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,24 +31,25 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "color": "^4.0.1", "d3-force": "^2.0.1", "prop-types": "^15.7.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -56,7 +57,7 @@ "@types/d3-force": "^2.1.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/tech-radar/src/components/RadarComponent.test.tsx b/plugins/tech-radar/src/components/RadarComponent.test.tsx index 2b237c8d83..aeced1a2da 100644 --- a/plugins/tech-radar/src/components/RadarComponent.test.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.test.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { render, waitForElement } from '@testing-library/react'; +import { act, render, waitForElement } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { act } from 'react-dom/test-utils'; import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index 7f4e69e388..e9bcb3d314 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -22,9 +22,8 @@ import { } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { render, waitForElement } from '@testing-library/react'; +import { act, render, waitForElement } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarPage } from './RadarPage'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 7c937e0bad..1c3c561bc9 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-techdocs-backend +## 0.12.0 + +### Minor Changes + +- 1bada775a9: Added the ability for the TechDocs Backend to (optionally) leverage a cache + store to improve performance when reading files from a cloud storage provider. + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + - @backstage/techdocs-common@0.11.0 + +## 0.11.0 + +### Minor Changes + +- 905dd952ac: **BREAKING** `DefaultTechDocsCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: + + ```diff + // packages/backend/src/plugins/search.ts + + ... + export default async function createPlugin({ + ... + + tokenManager, + }: PluginEnvironment) { + ... + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: DefaultTechDocsCollator.fromConfig(config, { + discovery, + logger, + + tokenManager, + }), + }); + + ... + } + ``` + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/backend-common@0.9.12 + ## 0.10.9 ### Patch Changes diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index f9be757e6a..b241917bc0 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -10,6 +10,7 @@ import express from 'express'; import { GeneratorBuilder } from '@backstage/techdocs-common'; import { Knex } from 'knex'; import { Logger as Logger_2 } from 'winston'; +import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PreparerBuilder } from '@backstage/techdocs-common'; import { PublisherBase } from '@backstage/techdocs-common'; @@ -27,15 +28,7 @@ export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export class DefaultTechDocsCollator implements DocumentCollator { // @deprecated - constructor({ - discovery, - locationTemplate, - logger, - catalogClient, - tokenManager, - parallelismLimit, - legacyPathCasing, - }: TechDocsCollatorOptions); + constructor(options: TechDocsCollatorOptions); // (undocumented) protected applyArgsToFormat( format: string, diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 9e563137bd..8d531e0536 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -226,6 +226,32 @@ export interface Config { }; }; + /** + * @example http://localhost:7007/api/techdocs + * Techdocs cache information + */ + cache?: { + /** + * The cache time-to-live for TechDocs sites (in milliseconds). Set this + * to a non-zero value to cache TechDocs sites and assets as they are + * read from storage. + * + * Note: you must also configure `backend.cache` appropriately as well, + * and to pass a PluginCacheManager instance to TechDocs Backend's + * createRouter method in your backend. + */ + ttl: number; + + /** + * The time (in milliseconds) that the TechDocs backend will wait for + * a cache service to respond before continuing on as though the cached + * object was not found (e.g. when the cache sercice is unavailable). + * + * Defaults to 1000 milliseconds. + */ + readTimeout?: number; + }; + /** * @example http://localhost:7007/api/techdocs * @visibility frontend diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index ffad4bc4c1..da1d53e86f 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.9", + "version": "0.12.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.11", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", + "@backstage/integration": "^0.6.10", "@backstage/search-common": "^0.2.1", - "@backstage/techdocs-common": "^0.10.8", + "@backstage/techdocs-common": "^0.11.0", "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -51,8 +52,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/test-utils": "^0.1.24", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 821b3478a0..66eb52c1aa 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -36,6 +36,7 @@ import path from 'path'; import { Writable } from 'stream'; import { Logger } from 'winston'; import { BuildMetadataStorage } from './BuildMetadataStorage'; +import { TechDocsCache } from '../cache'; type DocsBuilderArguments = { preparers: PreparerBuilder; @@ -46,6 +47,7 @@ type DocsBuilderArguments = { config: Config; scmIntegrations: ScmIntegrationRegistry; logStream?: Writable; + cache?: TechDocsCache; }; export class DocsBuilder { @@ -57,6 +59,7 @@ export class DocsBuilder { private config: Config; private scmIntegrations: ScmIntegrationRegistry; private logStream: Writable | undefined; + private cache?: TechDocsCache; constructor({ preparers, @@ -67,6 +70,7 @@ export class DocsBuilder { config, scmIntegrations, logStream, + cache, }: DocsBuilderArguments) { this.preparer = preparers.get(entity); this.generator = generators.get(entity); @@ -76,6 +80,7 @@ export class DocsBuilder { this.config = config; this.scmIntegrations = scmIntegrations; this.logStream = logStream; + this.cache = cache; } /** @@ -210,11 +215,19 @@ export class DocsBuilder { )}`, ); - await this.publisher.publish({ + const published = await this.publisher.publish({ entity: this.entity, directory: outputDir, }); + // Invalidate the cache for any published objects. + if (this.cache && published && published?.objects?.length) { + this.logger.debug( + `Invalidating ${published.objects.length} cache objects`, + ); + await this.cache.invalidateMultiple(published.objects); + } + try { // Not a blocker hence no need to await this. fs.remove(outputDir); diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts new file mode 100644 index 0000000000..17613e1b58 --- /dev/null +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { CacheClient, getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { CacheInvalidationError, TechDocsCache } from './TechDocsCache'; + +const cached = (str: string): string => { + return Buffer.from(str).toString('base64'); +}; + +describe('TechDocsCache', () => { + let CacheUnderTest: TechDocsCache; + let MockClient: jest.Mocked; + + beforeEach(() => { + MockClient = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + }; + CacheUnderTest = TechDocsCache.fromConfig(new ConfigReader({}), { + cache: MockClient, + logger: getVoidLogger(), + }); + }); + + describe('get', () => { + it('returns undefined if no response', async () => { + const expectedPath = 'some/index.html'; + MockClient.get.mockResolvedValueOnce(undefined); + + const actual = await CacheUnderTest.get(expectedPath); + expect(MockClient.get).toHaveBeenCalledWith(expectedPath); + expect(actual).toBe(undefined); + }); + + it('returns undefined if cache get throws', async () => { + const expectedPath = 'some/index.html'; + MockClient.get.mockRejectedValueOnce(new Error()); + + const actual = await CacheUnderTest.get(expectedPath); + expect(actual).toBe(undefined); + }); + + it('returns undefined if no response after 1s by default', async () => { + const expectedPath = 'some/index.html'; + MockClient.get.mockImplementationOnce(() => { + return new Promise(resolve => { + setTimeout(() => resolve(cached('value')), 1500); + }); + }); + const actual = await CacheUnderTest.get(expectedPath); + expect(actual).toBe(undefined); + }); + + it('returns undefined if no response after configured readTimeout', async () => { + const expectedPath = 'some/index.html'; + MockClient.get.mockImplementationOnce(() => { + return new Promise(resolve => { + setTimeout(() => resolve(cached('value')), 20); + }); + }); + + CacheUnderTest = TechDocsCache.fromConfig( + new ConfigReader({ + techdocs: { cache: { readTimeout: 10 } }, + }), + { + cache: MockClient, + logger: getVoidLogger(), + }, + ); + + const actual = await CacheUnderTest.get(expectedPath); + expect(actual).toBe(undefined); + }); + + it('returns data if cache get returns it', async () => { + const expectedPath = 'some/index.html'; + MockClient.get.mockResolvedValueOnce(cached('expected value')); + + const actual = await CacheUnderTest.get(expectedPath); + expect(actual?.toString()).toBe('expected value'); + }); + }); + + describe('set', () => { + it('sets a base64-encoded string', async () => { + const expectedPath = 'some/index.html'; + MockClient.set.mockResolvedValueOnce(undefined); + + await CacheUnderTest.set(expectedPath, Buffer.from('some data')); + expect(MockClient.set).toHaveBeenCalledWith( + expectedPath, + cached('some data'), + ); + }); + + it('does not throw if client throws', () => { + MockClient.set.mockRejectedValueOnce(new Error()); + expect(() => CacheUnderTest.set('i.html', Buffer.from(''))).not.toThrow(); + }); + }); + + describe('invalidate', () => { + it('calls delete on client', async () => { + const expectedPath = 'some/index.html'; + MockClient.delete.mockResolvedValueOnce(undefined); + + await CacheUnderTest.invalidate(expectedPath); + expect(MockClient.delete).toHaveBeenCalledWith(expectedPath); + }); + }); + + describe('invalidateMultiple', () => { + it('calls delete once per given path', async () => { + const expectedPaths = ['one/index.html', 'two/index.html']; + MockClient.delete.mockResolvedValue(undefined); + + await CacheUnderTest.invalidateMultiple(expectedPaths); + expect(MockClient.delete).toHaveBeenNthCalledWith(1, expectedPaths[0]); + expect(MockClient.delete).toHaveBeenNthCalledWith(2, expectedPaths[1]); + }); + + it('returns an array of as many paths provided', async () => { + const expectedPaths = ['one/index.html', 'two/index.html']; + MockClient.delete.mockResolvedValue(undefined); + + const actual = await CacheUnderTest.invalidateMultiple(expectedPaths); + expect(actual.length).toBe(2); + }); + + it('calls delete on all paths even if the first rejects', async () => { + const expectedPaths = ['one/index.html', 'two/index.html']; + MockClient.delete.mockRejectedValueOnce(new Error()); + MockClient.delete.mockResolvedValueOnce(undefined); + + await expect( + CacheUnderTest.invalidateMultiple(expectedPaths), + ).rejects.toThrowError(CacheInvalidationError); + expect(MockClient.delete).toHaveBeenCalledTimes(2); + }); + + it('rejects with invalidations error response', async () => { + const expectedPaths = ['one/index.html', 'two/index.html']; + MockClient.delete.mockResolvedValueOnce(undefined); + MockClient.delete.mockRejectedValueOnce(new Error()); + + await expect( + CacheUnderTest.invalidateMultiple.bind(CacheUnderTest, expectedPaths), + ).rejects.toThrow(CacheInvalidationError); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.ts new file mode 100644 index 0000000000..807d6ca87b --- /dev/null +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.ts @@ -0,0 +1,105 @@ +/* + * 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 { CacheClient } from '@backstage/backend-common'; +import { assertError, CustomErrorBase } from '@backstage/errors'; +import { Config } from '@backstage/config'; +import { Logger } from 'winston'; + +export class CacheInvalidationError extends CustomErrorBase {} + +export class TechDocsCache { + protected readonly cache: CacheClient; + protected readonly logger: Logger; + protected readonly readTimeout: number; + + private constructor({ + cache, + logger, + readTimeout, + }: { + cache: CacheClient; + logger: Logger; + readTimeout: number; + }) { + this.cache = cache; + this.logger = logger; + this.readTimeout = readTimeout; + } + + static fromConfig( + config: Config, + { cache, logger }: { cache: CacheClient; logger: Logger }, + ) { + const timeout = config.getOptionalNumber('techdocs.cache.readTimeout'); + const readTimeout = timeout === undefined ? 1000 : timeout; + return new TechDocsCache({ cache, logger, readTimeout }); + } + + async get(path: string): Promise { + try { + // Promise.race ensures we don't hang the client for long if the cache is + // temporarily unreachable. + const response = (await Promise.race([ + this.cache.get(path), + new Promise(cancelAfter => setTimeout(cancelAfter, this.readTimeout)), + ])) as string | undefined; + + if (response !== undefined) { + this.logger.debug(`Cache hit: ${path}`); + return Buffer.from(response, 'base64'); + } + + this.logger.debug(`Cache miss: ${path}`); + return response; + } catch (e) { + assertError(e); + this.logger.warn(`Error getting cache entry ${path}: ${e.message}`); + this.logger.debug(e.stack); + return undefined; + } + } + + async set(path: string, data: Buffer): Promise { + this.logger.debug(`Writing cache entry for ${path}`); + this.cache + .set(path, data.toString('base64')) + .catch(e => this.logger.error('write error', e)); + } + + async invalidate(path: string): Promise { + return this.cache.delete(path); + } + + async invalidateMultiple( + paths: string[], + ): Promise[]> { + const settled = await Promise.allSettled( + paths.map(path => this.cache.delete(path)), + ); + const rejected = settled.filter( + s => s.status === 'rejected', + ) as PromiseRejectedResult[]; + + if (rejected.length) { + throw new CacheInvalidationError( + 'TechDocs cache invalidation error', + rejected, + ); + } + + return settled; + } +} diff --git a/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts new file mode 100644 index 0000000000..312674a5bf --- /dev/null +++ b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { createCacheMiddleware, TechDocsCache } from '.'; + +/** + * Mocks cached HTTP response. + */ +const getMockHttpResponseFor = (content: string): Buffer => { + return Buffer.concat([ + Buffer.from(`HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Sat, 1 Jul 2021 12:00:00 GMT +Date: Sat, 1 Jul 2021 12:00:00 GMT +Connection: close +Content-Length: ${content.length}\n\n`), + Buffer.from(content), + ]); +}; + +/** + * Wait for the socket to close. Works because, above, we set connection: close + */ +const waitForSocketClose = () => new Promise(resolve => setTimeout(resolve, 0)); + +describe('createCacheMiddleware', () => { + let cache: jest.Mocked; + let app: express.Express; + + beforeEach(async () => { + cache = { + get: jest.fn().mockResolvedValue(undefined), + set: jest.fn().mockResolvedValue(undefined), + invalidate: jest.fn().mockResolvedValue(undefined), + invalidateMultiple: jest.fn().mockResolvedValue(undefined), + } as unknown as jest.Mocked; + const router = await createCacheMiddleware({ + logger: getVoidLogger(), + cache, + }); + app = express().use(router); + app.use((req, res, next) => { + // By default, send cacheable content. + if (req.path !== '/static/docs/error.png') { + res.send('default-response'); + } else { + next(new Error()); + } + }); + }); + + describe('middleware', () => { + it('does not apply to non-static/docs paths', async () => { + await request(app) + .get('/static/not-docs') + .expect(200, 'default-response'); + + expect(cache.set).not.toHaveBeenCalled(); + }); + + it('responds with cached response', async () => { + cache.get.mockResolvedValueOnce(getMockHttpResponseFor('xyz')); + + await request(app).get('/static/docs/foo.html').expect(200, 'xyz'); + + await waitForSocketClose(); + expect(cache.set).not.toHaveBeenCalled(); + }); + + it('sets cache when content is cacheable', async () => { + const expectedPath = 'default/api/xyz/index.html'; + await request(app) + .get(`/static/docs/${expectedPath}`) + .expect(200, 'default-response'); + + await waitForSocketClose(); + expect(cache.set).toHaveBeenCalled(); + + const [actualPath, actualBuffer] = (cache.set as jest.Mock).mock.calls[0]; + expect(actualPath).toBe(expectedPath); + expect(actualBuffer.toString()).toContain('default-response'); + }); + + it('does not set cache on error', async () => { + await request(app).get('/static/docs/error.png').expect(500); + + await waitForSocketClose(); + expect(cache.set).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/cache/cacheMiddleware.ts b/plugins/techdocs-backend/src/cache/cacheMiddleware.ts new file mode 100644 index 0000000000..cb59681304 --- /dev/null +++ b/plugins/techdocs-backend/src/cache/cacheMiddleware.ts @@ -0,0 +1,94 @@ +/* + * 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 { Router } from 'express'; +import router from 'express-promise-router'; +import { Logger } from 'winston'; +import { TechDocsCache } from './TechDocsCache'; + +type CacheMiddlewareOptions = { + cache: TechDocsCache; + logger: Logger; +}; + +type ErrorCallback = (err?: Error) => void; + +export const createCacheMiddleware = ({ + cache, +}: CacheMiddlewareOptions): Router => { + const cacheMiddleware = router(); + + // Middleware that, through socket monkey patching, captures responses as + // they're sent over /static/docs/* and caches them. Subsequent requests are + // loaded from cache. Cache key is the object's path (after `/static/docs/`). + cacheMiddleware.use(async (req, res, next) => { + const socket = res.socket; + const isCacheable = req.path.startsWith('/static/docs/'); + + // Continue early if this is non-cacheable, or there's no socket. + if (!isCacheable || !socket) { + next(); + return; + } + + // Make concrete references to these things. + const reqPath = decodeURI(req.path.match(/\/static\/docs\/(.*)$/)![1]); + const realEnd = socket.end.bind(socket); + const realWrite = socket.write.bind(socket); + let writeToCache = true; + const chunks: Buffer[] = []; + + // Monkey-patch the response's socket to keep track of chunks as they are + // written over the wire. + socket.write = ( + data: string | Uint8Array, + encoding?: BufferEncoding | ErrorCallback, + callback?: ErrorCallback, + ) => { + chunks.push(Buffer.from(data)); + if (typeof encoding === 'function') { + return realWrite(data, encoding); + } + return realWrite(data, encoding, callback); + }; + + // When a socket is closed, if there were no errors and the data written + // over the socket should be cached, cache it! + socket.on('close', async hadError => { + const content = Buffer.concat(chunks); + const head = content.toString('utf8', 0, 12); + if (writeToCache && !hadError && head.match(/HTTP\/\d\.\d 200/)) { + await cache.set(reqPath, content); + } + }); + + // Attempt to retrieve data from the cache. + const cached = await cache.get(reqPath); + + // If there is a cache hit, write it out on the socket, ensure we don't re- + // cache the data, and prevent going back to canonical storage by never + // calling next(). + if (cached) { + writeToCache = false; + realEnd(cached); + return; + } + + // No data retrieved from cache: allow retrieval from canonical storage. + next(); + }); + + return cacheMiddleware; +}; diff --git a/plugins/techdocs-backend/src/cache/index.ts b/plugins/techdocs-backend/src/cache/index.ts new file mode 100644 index 0000000000..751d8e8625 --- /dev/null +++ b/plugins/techdocs-backend/src/cache/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { createCacheMiddleware } from './cacheMiddleware'; +export { TechDocsCache } from './TechDocsCache'; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 283d0264e4..fa8a1156fe 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -63,24 +63,17 @@ export class DefaultTechDocsCollator implements DocumentCollator { /** * @deprecated use static fromConfig method instead. */ - constructor({ - discovery, - locationTemplate, - logger, - catalogClient, - tokenManager, - parallelismLimit = 10, - legacyPathCasing = false, - }: TechDocsCollatorOptions) { - this.discovery = discovery; + constructor(options: TechDocsCollatorOptions) { + this.discovery = options.discovery; this.locationTemplate = - locationTemplate || '/docs/:namespace/:kind/:name/:path'; - this.logger = logger; + options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; + this.logger = options.logger; this.catalogClient = - catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.parallelismLimit = parallelismLimit; - this.legacyPathCasing = legacyPathCasing; - this.tokenManager = tokenManager; + options.catalogClient || + new CatalogClient({ discoveryApi: options.discovery }); + this.parallelismLimit = options.parallelismLimit ?? 10; + this.legacyPathCasing = options.legacyPathCasing ?? false; + this.tokenManager = options.tokenManager; } static fromConfig(config: Config, options: TechDocsCollatorOptions) { diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index ac60f3da3a..1eb8c8f56f 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -25,11 +25,25 @@ import { PreparerBuilder, PublisherBase, } from '@backstage/techdocs-common'; +import { TechDocsCache } from '../cache'; import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; jest.mock('../DocsBuilder'); +jest.mock('cross-fetch', () => ({ + __esModule: true, + default: async () => { + return { + json: async () => { + return { + build_timestamp: 123, + }; + }, + }; + }, +})); + const MockedDocsBuilder = DocsBuilder as jest.MockedClass; describe('DocsSynchronizer', () => { @@ -52,6 +66,12 @@ describe('DocsSynchronizer', () => { getBaseUrl: jest.fn(), getExternalBaseUrl: jest.fn(), }; + const cache: jest.Mocked = { + get: jest.fn(), + set: jest.fn(), + invalidate: jest.fn(), + invalidateMultiple: jest.fn(), + } as unknown as jest.Mocked; let docsSynchronizer: DocsSynchronizer; const mockResponseHandler: jest.Mocked = { @@ -71,6 +91,7 @@ describe('DocsSynchronizer', () => { config: new ConfigReader({}), logger: getVoidLogger(), scmIntegrations: ScmIntegrations.fromConfig(new ConfigReader({})), + cache, }); }); @@ -193,4 +214,112 @@ describe('DocsSynchronizer', () => { expect(mockResponseHandler.error).toBeCalledWith(error); }); }); + + describe('doCacheSync', () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: '0', + name: 'test', + namespace: 'default', + }, + }; + + it('should not check metadata too often', async () => { + (shouldCheckForUpdate as jest.Mock).mockReturnValue(false); + + await docsSynchronizer.doCacheSync({ + responseHandler: mockResponseHandler, + discovery, + token: undefined, + entity, + }); + + expect(mockResponseHandler.finish).toBeCalledWith({ updated: false }); + expect(shouldCheckForUpdate).toBeCalledTimes(1); + }); + + it('should do nothing if source/cached metadata matches', async () => { + (shouldCheckForUpdate as jest.Mock).mockReturnValue(true); + (publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({ + build_timestamp: 123, + }); + + await docsSynchronizer.doCacheSync({ + responseHandler: mockResponseHandler, + discovery, + token: undefined, + entity, + }); + + expect(mockResponseHandler.finish).toBeCalledWith({ updated: false }); + }); + + it('should invalidate expected files when source/cached metadata differ', async () => { + (shouldCheckForUpdate as jest.Mock).mockReturnValue(true); + (publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({ + build_timestamp: 456, + files: ['index.html'], + }); + + await docsSynchronizer.doCacheSync({ + responseHandler: mockResponseHandler, + discovery, + token: undefined, + entity, + }); + + expect(mockResponseHandler.finish).toBeCalledWith({ updated: true }); + expect(cache.invalidateMultiple).toHaveBeenCalledWith([ + 'default/component/test/index.html', + ]); + }); + + it('should invalidate expected files when source/cached metadata differ with legacy casing', async () => { + (shouldCheckForUpdate as jest.Mock).mockReturnValue(true); + (publisher.fetchTechDocsMetadata as jest.Mock).mockResolvedValue({ + build_timestamp: 456, + files: ['index.html'], + }); + + const docsSynchronizerWithLegacy = new DocsSynchronizer({ + publisher, + config: new ConfigReader({ + techdocs: { legacyUseCaseSensitiveTripletPaths: true }, + }), + logger: getVoidLogger(), + scmIntegrations: ScmIntegrations.fromConfig(new ConfigReader({})), + cache, + }); + + await docsSynchronizerWithLegacy.doCacheSync({ + responseHandler: mockResponseHandler, + discovery, + token: undefined, + entity, + }); + + expect(mockResponseHandler.finish).toBeCalledWith({ updated: true }); + expect(cache.invalidateMultiple).toHaveBeenCalledWith([ + 'default/Component/test/index.html', + ]); + }); + + it('should gracefully handle errors', async () => { + (shouldCheckForUpdate as jest.Mock).mockReturnValue(true); + (publisher.fetchTechDocsMetadata as jest.Mock).mockRejectedValue( + new Error(), + ); + + await docsSynchronizer.doCacheSync({ + responseHandler: mockResponseHandler, + discovery, + token: undefined, + entity, + }); + + expect(mockResponseHandler.finish).toBeCalledWith({ updated: false }); + }); + }); }); diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 6f977dcb47..4d407ab54a 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, NotFoundError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -23,9 +24,15 @@ import { PreparerBuilder, PublisherBase, } from '@backstage/techdocs-common'; +import fetch from 'cross-fetch'; import { PassThrough } from 'stream'; import * as winston from 'winston'; -import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; +import { TechDocsCache } from '../cache'; +import { + BuildMetadataStorage, + DocsBuilder, + shouldCheckForUpdate, +} from '../DocsBuilder'; export type DocsSynchronizerSyncOpts = { log: (message: string) => void; @@ -38,22 +45,26 @@ export class DocsSynchronizer { private readonly logger: winston.Logger; private readonly config: Config; private readonly scmIntegrations: ScmIntegrationRegistry; + private readonly cache: TechDocsCache | undefined; constructor({ publisher, logger, config, scmIntegrations, + cache, }: { publisher: PublisherBase; logger: winston.Logger; config: Config; scmIntegrations: ScmIntegrationRegistry; + cache: TechDocsCache | undefined; }) { this.config = config; this.logger = logger; this.publisher = publisher; this.scmIntegrations = scmIntegrations; + this.cache = cache; } async doSync({ @@ -104,6 +115,7 @@ export class DocsSynchronizer { config: this.config, scmIntegrations: this.scmIntegrations, logStream, + cache: this.cache, }); const updated = await docsBuilder.build(); @@ -145,4 +157,76 @@ export class DocsSynchronizer { finish({ updated: true }); } + + async doCacheSync({ + responseHandler: { finish }, + discovery, + token, + entity, + }: { + responseHandler: DocsSynchronizerSyncOpts; + discovery: PluginEndpointDiscovery; + token: string | undefined; + entity: Entity; + }) { + // Check if the last update check was too recent. + if (!shouldCheckForUpdate(entity.metadata.uid!) || !this.cache) { + finish({ updated: false }); + return; + } + + // Fetch techdocs_metadata.json from the publisher and from cache. + const baseUrl = await discovery.getBaseUrl('techdocs'); + const namespace = entity.metadata?.namespace || ENTITY_DEFAULT_NAMESPACE; + const kind = entity.kind; + const name = entity.metadata.name; + const legacyPathCasing = + this.config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; + const tripletPath = `${namespace}/${kind}/${name}`; + const entityTripletPath = `${ + legacyPathCasing ? tripletPath : tripletPath.toLocaleLowerCase('en-US') + }`; + try { + const [sourceMetadata, cachedMetadata] = await Promise.all([ + this.publisher.fetchTechDocsMetadata({ namespace, kind, name }), + fetch( + `${baseUrl}/static/docs/${entityTripletPath}/techdocs_metadata.json`, + { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }, + ).then( + f => + f.json().catch(() => undefined) as ReturnType< + PublisherBase['fetchTechDocsMetadata'] + >, + ), + ]); + + // If build timestamps differ, merge their files[] lists and invalidate all objects. + if (sourceMetadata.build_timestamp !== cachedMetadata.build_timestamp) { + const files = [ + ...new Set([ + ...(sourceMetadata.files || []), + ...(cachedMetadata.files || []), + ]), + ].map(f => `${entityTripletPath}/${f}`); + await this.cache.invalidateMultiple(files); + finish({ updated: true }); + } else { + finish({ updated: false }); + } + } catch (e) { + assertError(e); + // In case of error, log and allow the user to go about their business. + this.logger.error( + `Error syncing cache for ${entityTripletPath}: ${e.message}`, + ); + finish({ updated: false }); + } finally { + // Update the last check time for the entity + new BuildMetadataStorage(entity.metadata.uid!).setLastUpdated(); + } + } } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index d53a2faed4..8f48342683 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + PluginCacheManager, +} from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -31,6 +34,7 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; +import { createCacheMiddleware, TechDocsCache } from '../cache'; /** * All of the required dependencies for running TechDocs in the "out-of-the-box" @@ -44,6 +48,7 @@ type OutOfTheBoxDeploymentOptions = { discovery: PluginEndpointDiscovery; database?: Knex; // TODO: Make database required when we're implementing database stuff. config: Config; + cache?: PluginCacheManager; }; /** @@ -80,12 +85,22 @@ export async function createRouter( const router = Router(); const { publisher, config, logger, discovery } = options; const catalogClient = new CatalogClient({ discoveryApi: discovery }); + + // Set up a cache client if configured. + let cache: TechDocsCache | undefined; + const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl'); + if (isOutOfTheBoxOption(options) && options.cache && defaultTtl) { + const cacheClient = options.cache.getClient({ defaultTtl }); + cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger }); + } + const scmIntegrations = ScmIntegrations.fromConfig(config); const docsSynchronizer = new DocsSynchronizer({ publisher, logger, config, scmIntegrations, + cache, }); router.get('/metadata/techdocs/:namespace/:kind/:name', async (req, res) => { @@ -175,6 +190,17 @@ export async function createRouter( // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline // of the repository) is responsible for building and publishing documentation to the storage provider if (config.getString('techdocs.builder') !== 'local') { + // However, if caching is enabled, take the opportunity to check and + // invalidate stale cache entries. + if (cache) { + await docsSynchronizer.doCacheSync({ + responseHandler, + discovery, + token, + entity, + }); + return; + } responseHandler.finish({ updated: false }); return; } @@ -199,6 +225,11 @@ export async function createRouter( ); }); + // If a cache manager was provided, attach the cache middleware. + if (cache) { + router.use(createCacheMiddleware({ logger, cache })); + } + // Route middleware which serves files from the storage set in the publisher. router.use('/static/docs', publisher.docsRouter()); diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 2755daea4a..5cab32a73f 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-techdocs +## 0.12.9 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- d90dad84b0: Switch to using `LogViewer` component from `@backstage/core-components` to display build logs. +- 3421826ca8: The problem of lowercase entity triplets which causes docs to not load on entity page is fixed. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog@0.7.4 + - @backstage/integration-react@0.1.15 + - @backstage/plugin-catalog-react@0.6.5 + - @backstage/plugin-search@0.5.1 + +## 0.12.8 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + - @backstage/plugin-search@0.5.0 + ## 0.12.7 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 1677d43095..4ac62a2b56 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.7", + "version": "0.12.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,37 +34,38 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.5", - "@backstage/integration": "^0.6.9", - "@backstage/integration-react": "^0.1.14", - "@backstage/plugin-catalog": "^0.7.3", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/plugin-search": "^0.4.18", - "@backstage/theme": "^0.2.13", + "@backstage/integration": "^0.6.10", + "@backstage/integration-react": "^0.1.15", + "@backstage/plugin-catalog": "^0.7.4", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-search": "^0.5.1", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.10.0", - "@types/react": "*", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", "git-url-parse": "^11.6.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-text-truncate": "^0.16.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/techdocs/src/EntityPageDocs.tsx b/plugins/techdocs/src/EntityPageDocs.tsx index c5356289f0..b10fba6d2e 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -17,15 +17,18 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; import { Reader } from './reader'; +import { toLowerMaybe } from './helpers'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; export const EntityPageDocs = ({ entity }: { entity: Entity }) => { + const config = useApi(configApiRef); return ( ); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 7624f250d9..bdb2f74772 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -44,6 +44,9 @@ describe('TechDocsStorageClient', () => { getProfile: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; beforeEach(() => { diff --git a/plugins/techdocs/src/helpers.ts b/plugins/techdocs/src/helpers.ts new file mode 100644 index 0000000000..7ff4dec6e7 --- /dev/null +++ b/plugins/techdocs/src/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { Config } from '@backstage/config'; + +// Lower-case entity triplets by default, but allow override. +export function toLowerMaybe(str: string, config: Config) { + return config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) + ? str + : str.toLocaleLowerCase('en-US'); +} diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.tsx index 77e69995ae..1aef793bd2 100644 --- a/plugins/techdocs/src/home/components/DocsCardGrid.tsx +++ b/plugins/techdocs/src/home/components/DocsCardGrid.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api'; import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; import { rootDocsRouteRef } from '../../routes'; @@ -26,6 +26,7 @@ import { ItemCardGrid, ItemCardHeader, } from '@backstage/core-components'; +import { toLowerMaybe } from '../../helpers'; export const DocsCardGrid = ({ entities, @@ -33,14 +34,7 @@ export const DocsCardGrid = ({ entities: Entity[] | undefined; }) => { const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); - - // Lower-case entity triplets by default, but allow override. - const toLowerMaybe = useApi(configApiRef).getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ) - ? (str: string) => str - : (str: string) => str.toLocaleLowerCase('en-US'); - + const config = useApi(configApiRef); if (!entities) return null; return ( @@ -59,9 +53,10 @@ export const DocsCardGrid = ({ to={getRouteToReaderPageFor({ namespace: toLowerMaybe( entity.metadata.namespace ?? 'default', + config, ), - kind: toLowerMaybe(entity.kind), - name: toLowerMaybe(entity.metadata.name), + kind: toLowerMaybe(entity.kind, config), + name: toLowerMaybe(entity.metadata.name, config), })} color="primary" data-testid="read_docs" diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx index 0e64c2e2e8..83d6f05788 100644 --- a/plugins/techdocs/src/home/components/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/DocsTable.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { useCopyToClipboard } from 'react-use'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useRouteRef, useApi, configApiRef } from '@backstage/core-plugin-api'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { formatEntityRefTitle, @@ -34,6 +34,7 @@ import { import * as actionFactories from './actions'; import * as columnFactories from './columns'; import { DocsTableRow } from './types'; +import { toLowerMaybe } from '../../helpers'; export const DocsTable = ({ entities, @@ -50,26 +51,21 @@ export const DocsTable = ({ }) => { const [, copyToClipboard] = useCopyToClipboard(); const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); - - // Lower-case entity triplets by default, but allow override. - const toLowerMaybe = useApi(configApiRef).getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ) - ? (str: string) => str - : (str: string) => str.toLocaleLowerCase('en-US'); - + const config = useApi(configApiRef); if (!entities) return null; const documents = entities.map(entity => { const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - return { entity, resolved: { docsUrl: getRouteToReaderPageFor({ - namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'), - kind: toLowerMaybe(entity.kind), - name: toLowerMaybe(entity.metadata.name), + namespace: toLowerMaybe( + entity.metadata.namespace ?? 'default', + config, + ), + kind: toLowerMaybe(entity.kind, config), + name: toLowerMaybe(entity.metadata.name, config), }), ownedByRelations, ownedByRelationsTitle: ownedByRelations diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx index fa82527282..ea24a3627f 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx @@ -14,32 +14,30 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; -import React from 'react'; +import React, { ReactNode } from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; import { TechDocsBuildLogs, TechDocsBuildLogsDrawerContent, } from './TechDocsBuildLogs'; -// react-lazylog is based on a react-virtualized component which doesn't -// write the content to the dom, so we mock it. -jest.mock('react-lazylog/build/LazyLog', () => { - return { - default: ({ text }: { text: string }) => { - return

{text}

; - }, - }; -}); +// The inside needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}, +})); describe('', () => { - it('should render with button', () => { - const rendered = render(); + it('should render with button', async () => { + const rendered = await renderInTestApp(); expect(rendered.getByText(/Show Build Logs/i)).toBeInTheDocument(); expect(rendered.queryByText(/Build Details/i)).not.toBeInTheDocument(); }); - it('should open drawer', () => { - const rendered = render(); + it('should open drawer', async () => { + const rendered = await renderInTestApp(); rendered.getByText(/Show Build Logs/i).click(); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); }); @@ -48,7 +46,7 @@ describe('', () => { describe('', () => { it('should render with empty log', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( , ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); @@ -61,7 +59,7 @@ describe('', () => { it('should render logs', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( ', () => { expect(onClose).toBeCalledTimes(0); }); - it('should call onClose', () => { + it('should call onClose', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( , ); rendered.getByTitle('Close the drawer').click(); diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx index 8c739a9bcf..dc446a55d8 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { Button, createStyles, @@ -26,9 +26,7 @@ import { Typography, } from '@material-ui/core'; import Close from '@material-ui/icons/Close'; -import React, { Suspense, useState } from 'react'; - -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); +import React, { useState } from 'react'; const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -46,6 +44,9 @@ const useDrawerStyles = makeStyles((theme: Theme) => height: '100%', overflow: 'hidden', }, + logs: { + background: theme.palette.background.default, + }, }), ); @@ -57,6 +58,8 @@ export const TechDocsBuildLogsDrawerContent = ({ onClose: () => void; }) => { const classes = useDrawerStyles(); + const logText = + buildLog.length === 0 ? 'Waiting for logs...' : buildLog.join('\n'); return ( - - }> - - + ); }; diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 60fd7a5490..9903ca2b64 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-todo-backend +## 0.1.15 + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + +## 0.1.14 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- Updated dependencies + - @backstage/integration@0.6.10 + - @backstage/backend-common@0.9.12 + ## 0.1.13 ### Patch Changes diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index f989755a62..00e243d1a2 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -112,7 +112,7 @@ export class TodoScmReader implements TodoReader { options: Omit, ): TodoScmReader; // (undocumented) - readTodos({ url }: ReadTodosOptions): Promise; + readTodos(options: ReadTodosOptions): Promise; } // Warning: (ae-missing-release-tag) "TodoService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index abda1076ae..ecb35920a6 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.13", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,12 +25,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.13", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.7", + "@backstage/integration": "^0.6.10", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.10.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 8442983637..804dd1146d 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -76,7 +76,8 @@ export class TodoScmReader implements TodoReader { this.integrations = options.integrations; } - async readTodos({ url }: ReadTodosOptions): Promise { + async readTodos(options: ReadTodosOptions): Promise { + const { url } = options; const inFlightRead = this.inFlightReads.get(url); if (inFlightRead) { return inFlightRead.then(read => read.result); @@ -101,9 +102,10 @@ export class TodoScmReader implements TodoReader { } private async doReadTodos( - { url }: ReadTodosOptions, + options: ReadTodosOptions, etag?: string, ): Promise { + const { url } = options; const tree = await this.reader.readTree(url, { etag, filter(filePath, info) { diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 1a6a352e73..a0f8798d93 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-todo +## 0.1.16 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + - @backstage/plugin-catalog-react@0.6.5 + ## 0.1.15 ### Patch Changes diff --git a/plugins/todo/api-report.md b/plugins/todo/api-report.md index e0fdb8ba58..0784f5d56e 100644 --- a/plugins/todo/api-report.md +++ b/plugins/todo/api-report.md @@ -26,13 +26,7 @@ export const todoApiRef: ApiRef; export class TodoClient implements TodoApi { constructor(options: TodoClientOptions); // (undocumented) - listTodos({ - entity, - offset, - limit, - orderBy, - filters, - }: TodoListOptions): Promise; + listTodos(options: TodoListOptions): Promise; } // @public diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 9a6098799f..10f2b311ca 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,23 +28,24 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/theme": "^0.2.13", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index f75e4b6954..7e4e4c8f79 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -43,13 +43,8 @@ export class TodoClient implements TodoApi { this.identityApi = options.identityApi; } - async listTodos({ - entity, - offset, - limit, - orderBy, - filters, - }: TodoListOptions): Promise { + async listTodos(options: TodoListOptions): Promise { + const { entity, offset, limit, orderBy, filters } = options; const baseUrl = await this.discoveryApi.getBaseUrl('todo'); const token = await this.identityApi.getIdToken(); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index a309afb497..57a6326022 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-user-settings +## 0.3.13 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + +## 0.3.12 + +### Patch Changes + +- 9a1c8e92eb: The theme switcher now renders the title of themes instead of their variant +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.3.11 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 15b1771bde..aaef5eb0a3 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.11", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,22 +31,23 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", - "@backstage/theme": "^0.2.13", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 4ae5c49c32..a76282fac8 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-xcmetrics +## 0.2.12 + +### Patch Changes + +- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. +- 343ef34599: Handle a case where XCode data from backend (before 0.0.8) could be missing +- Updated dependencies + - @backstage/core-components@0.8.0 + - @backstage/core-plugin-api@0.3.0 + +## 0.2.11 + +### Patch Changes + +- af09b1e059: Fix bug where invalid build statuses led to crashes +- Updated dependencies + - @backstage/core-components@0.7.6 + - @backstage/theme@0.2.14 + - @backstage/core-plugin-api@0.2.2 + ## 0.2.10 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index f14735a448..55488aa8f4 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.10", + "version": "0.2.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,25 +21,26 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.5", - "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.13", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { - "@backstage/cli": "^0.9.1", - "@backstage/core-app-api": "^0.1.23", - "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.23", + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 44f27872ca..6c7d133259 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -163,7 +163,7 @@ export type Xcode = { export type BuildResponse = { build: Build; targets: Target[]; - xcode: Xcode; + xcode?: Xcode; // Can be undefined if XCMetrics version < v0.0.8 }; export type BuildFilters = { diff --git a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx index 7083d890d6..c26c101143 100644 --- a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx +++ b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx @@ -51,6 +51,20 @@ describe('BuildDetails', () => { ).toBeInTheDocument(); expect(rendered.getByText(client.mockBuild.schema)).toBeInTheDocument(); }); + + it('should render if xcode data is not present', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect( + rendered.getByText('Xcode').parentNode?.childNodes[1].textContent, + ).toEqual('Unknown'); + }); }); describe('BuildDetails with request', () => { diff --git a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.tsx b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.tsx index 1d77bf7a7a..8732bd92d8 100644 --- a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.tsx +++ b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.tsx @@ -78,7 +78,7 @@ export const BuildDetails = ({ {formatStatus(build.buildStatus)} ), - xcode: `${xcode.version} (${xcode.buildNumber})`, + xcode: xcode ? `${xcode.version} (${xcode.buildNumber})` : 'Unknown', CI: build.isCi, }; diff --git a/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.test.tsx b/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.test.tsx index a208432f59..0801a34b48 100644 --- a/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.test.tsx +++ b/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.test.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { StatusIcon } from './StatusIcon'; +import { BuildStatus } from '../../api'; describe('StatusIcon', () => { it('should render', async () => { @@ -31,4 +32,11 @@ describe('StatusIcon', () => { rendered = await renderInTestApp(); expect(rendered.getByLabelText('Status warning')).toBeInTheDocument(); }); + + it('should render invalid statuses', async () => { + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status aborted')).toBeInTheDocument(); + }); }); diff --git a/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.tsx b/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.tsx index 610575b817..f28b5e4fe4 100644 --- a/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.tsx +++ b/plugins/xcmetrics/src/components/StatusIcon/StatusIcon.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { + StatusAborted, StatusError, StatusOK, StatusWarning, @@ -32,4 +33,4 @@ interface StatusIconProps { } export const StatusIcon = ({ buildStatus }: StatusIconProps) => - STATUS_ICONS[buildStatus]; + STATUS_ICONS[buildStatus] ?? ; diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index bb5f3b0967..4db1109145 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -143,7 +143,10 @@ function findTypesPackage(dep, pkg) { */ function findTypeDepErrors(typeDeps, pkg) { const devDeps = mkTypeDepSet(pkg.get('devDependencies')); - const deps = mkTypeDepSet(pkg.get('dependencies')); + const deps = mkTypeDepSet({ + ...pkg.get('dependencies'), + ...pkg.get('peerDependencies'), + }); const errors = []; for (const typeDep of typeDeps) { diff --git a/yarn.lock b/yarn.lock index 1e5d199349..7a599498d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,25 +69,25 @@ dependencies: tslib "~2.0.1" -"@asyncapi/avro-schema-parser@^0.2.1": - version "0.2.1" - resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.2.1.tgz#0b7d9953e12084e6f556db261ae08bd8f6690faa" - integrity sha512-RZJaHsdYM4dChYSrb/TWrVCn/r2qcus+9/8iLL8+SMINHb0ECgH8tFZFJpr3Tq+LV2SBFaRQ+9kuecjQ8BNDSA== +"@asyncapi/avro-schema-parser@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-0.3.0.tgz#6922acc559ef999c57e81297d78ffe680fc92b3c" + integrity sha512-gWAqS2CKxbChdX8hZY+5EYQl6atP8FTSBvoG5mGGQ89XUoNdlLX14lsvbgvBnDj5sSwqfs+b5Mh5PUZMR/8maA== -"@asyncapi/openapi-schema-parser@^2.0.1": +"@asyncapi/openapi-schema-parser@^2.0.0": version "2.0.1" resolved "https://registry.npmjs.org/@asyncapi/openapi-schema-parser/-/openapi-schema-parser-2.0.1.tgz#4d6e82cced907b14e0ad6f98261ff2562d968d96" integrity sha512-algbtdM1gcAOa8+V8kp7WeBhdaNac82jmZUXx8YjyNfRVo02N2juDrjeBAGJd+FNva9Mb4MM7qfkJoAFpTL5VQ== dependencies: "@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0" -"@asyncapi/parser@^1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.5.0.tgz#d70616a1e6081c7dd86957befd5dccc46b9a77df" - integrity sha512-HzrehCcT6R+iqtktNmrUM9wRUEMBqnCeXIrGJT0txBMS1QduNRmXGjvuDpxqwyaATPH/xu1gSp2l6pFP/hyVbA== +"@asyncapi/parser@^1.13.0": + version "1.13.0" + resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.13.0.tgz#bc20d53ba870ead9d26d5a4e3a838c5765ba8d4c" + integrity sha512-UnTYSjx2Sc+VKWIIeIV5I+ogFbN5iWzNgx1UgTXQ5oPhtNlsdIkF+w7qV8GyiraaKbBqAuwepv4xbBvlEi8hkw== dependencies: "@apidevtools/json-schema-ref-parser" "^9.0.6" - "@asyncapi/specs" "^2.7.7" + "@asyncapi/specs" "^2.11.0" "@fmvilas/pseudo-yaml-ast" "^0.3.1" ajv "^6.10.1" js-yaml "^3.13.1" @@ -96,25 +96,24 @@ node-fetch "^2.6.0" tiny-merge-patch "^0.1.2" -"@asyncapi/react-component@^0.23.0": - version "0.23.1" - resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.23.1.tgz#6ae574aac86d835e6845268d49803c4808f37b97" - integrity sha512-xBH8F//sF4Sb7M95eVDnQDG+czklQieFKHkCr8SJmmb2j4NylAOTJBoYSZJ3QTC9r0O+SxdQglwh+u1mhMAU+A== +"@asyncapi/react-component@^1.0.0-next.25": + version "1.0.0-next.25" + resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-1.0.0-next.25.tgz#76b27e0e239b03c32499ee5cd8977e5ceb74f754" + integrity sha512-S+HguQ6IltQWiy+jO7hlmANJYxJ2UOvH7iMAw+YTcDtYGOe/mrDr7vbrrB3HigZRYIyZSV34cNgNOhw/psVHlg== dependencies: - "@asyncapi/avro-schema-parser" "^0.2.1" - "@asyncapi/openapi-schema-parser" "^2.0.1" - "@asyncapi/parser" "^1.5.0" - constate "^1.2.0" - dompurify "^2.1.1" - markdown-it "^11.0.1" - merge "^2.1.0" - openapi-sampler "^1.0.0-beta.15" - react-use "^12.2.0" + "@asyncapi/avro-schema-parser" "^0.3.0" + "@asyncapi/openapi-schema-parser" "^2.0.0" + "@asyncapi/parser" "^1.13.0" + highlight.js "^10.7.2" + isomorphic-dompurify "^0.13.0" + marked "^2.1.1" + openapi-sampler "^1.1.0" + use-resize-observer "^7.0.0" -"@asyncapi/specs@^2.7.7": - version "2.7.7" - resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.7.tgz#10f72c95153a3cc10039f6ba9c3a6f7c3b7fecfc" - integrity sha512-z8kj4GDJ640DU4msRsWprvmuC9n7vIeJW+D7Tp1xdefoLX5ZJrK7+4Xruna513wV0fSLpFzCmGz7McEP6CtKDg== +"@asyncapi/specs@^2.11.0": + version "2.12.0" + resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.12.0.tgz#15534ca9e518b561d33edfdfdbb234f9b274f261" + integrity sha512-X4Xkrl+9WXSk5EJhsueIxNx6ymHI5wpkw4ofetV+VRnPLNob/XO4trPSJClrL5hlknxbGADLvlrkI5d3XJ996g== "@azure/abort-controller@^1.0.0": version "1.0.2" @@ -2182,6 +2181,28 @@ "@backstage/errors" "^0.1.2" cross-fetch "^3.0.6" +"@backstage/core-app-api@^0.1.13", "@backstage/core-app-api@^0.1.16", "@backstage/core-app-api@^0.1.3", "@backstage/core-app-api@^0.1.6": + version "0.1.24" + resolved "https://registry.npmjs.org/@backstage/core-app-api/-/core-app-api-0.1.24.tgz#4cfc366c56de3e698218770eacb59a0aa16c8f05" + integrity sha512-TJ2F+8UjwBQqlSd6y3RpPwdmJfLsvds2OaH3TuiFvjdpogqboIjWGq3nIgxzhe+vxYoWLhoHrMoJYP4IpF7VQQ== + dependencies: + "@backstage/app-defaults" "^0.1.1" + "@backstage/config" "^0.1.11" + "@backstage/core-components" "^0.7.6" + "@backstage/core-plugin-api" "^0.2.2" + "@backstage/theme" "^0.2.14" + "@backstage/types" "^0.1.1" + "@backstage/version-bridge" "^0.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@types/prop-types" "^15.7.3" + "@types/react" "*" + prop-types "^15.7.2" + react "^16.12.0" + react-router-dom "6.0.0-beta.0" + react-use "^17.2.4" + zen-observable "^0.8.15" + "@backstage/core-components@^0.3.0": version "0.3.3" resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.3.3.tgz#ec63eac6d789303f90219857849fa1cefe4e0dde" @@ -2360,6 +2381,48 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/core-components@^0.7.0", "@backstage/core-components@^0.7.6": + version "0.7.6" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.7.6.tgz#2d29480b13c607c8ea8a2821a06326bd05ca3db8" + integrity sha512-aB0ndQDxjNW8Tkzs0rhABd8qF2wLC9f0PWKT3RMh5Tovmg6Y/OOhj+YDzlsPrYJFLwfIwFDC4iA+H1WdkFe7tg== + dependencies: + "@backstage/config" "^0.1.11" + "@backstage/core-plugin-api" "^0.2.2" + "@backstage/errors" "^0.1.5" + "@backstage/theme" "^0.2.14" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@types/react" "*" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^3.0.0" + d3-shape "^3.0.0" + d3-zoom "^3.0.0" + dagre "^0.8.5" + immer "^9.0.1" + lodash "^4.17.21" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-markdown "^7.0.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.3" + react-text-truncate "^0.16.0" + react-use "^17.2.4" + remark-gfm "^2.0.0" + zen-observable "^0.8.15" + "@backstage/core-plugin-api@^0.1.10", "@backstage/core-plugin-api@^0.1.3", "@backstage/core-plugin-api@^0.1.4", "@backstage/core-plugin-api@^0.1.6", "@backstage/core-plugin-api@^0.1.8": version "0.1.13" resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.1.13.tgz#583c0fe3440450c304f13dcc85eac3bd80f357d7" @@ -2378,6 +2441,24 @@ react-use "^17.2.4" zen-observable "^0.8.15" +"@backstage/core-plugin-api@^0.2.2": + version "0.2.2" + resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.2.2.tgz#e746b8595959a9d4acdbe4d6bc5b71d591d0cd1b" + integrity sha512-04TMAX75cBTOMYdKTIgp8FAz4BRrCC9s7YDIBpUM0z+d1UlfCWe48EmP2igPsCt/8apcw38gdR81BPlqzoD6UA== + dependencies: + "@backstage/config" "^0.1.11" + "@backstage/theme" "^0.2.14" + "@backstage/types" "^0.1.1" + "@backstage/version-bridge" "^0.1.0" + "@material-ui/core" "^4.12.2" + "@types/react" "*" + history "^5.0.0" + prop-types "^15.7.2" + react "^16.12.0" + react-router-dom "6.0.0-beta.0" + react-use "^17.2.4" + zen-observable "^0.8.15" + "@backstage/plugin-catalog-react@^0.4.0": version "0.4.6" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.4.6.tgz#f33f3cd734f110d3c547f7eae4e25d14806ec796" @@ -2627,6 +2708,71 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@codemirror/highlight@^0.19.0": + version "0.19.6" + resolved "https://registry.npmjs.org/@codemirror/highlight/-/highlight-0.19.6.tgz#7f2e066f83f5649e8e0748a3abe0aaeaf64b8ac2" + integrity sha512-+eibu6on9quY8uN3xJ/n3rH+YIDLlpX7YulVmFvqAIz/ukRQ5tWaBmB7fMixHmnmRIRBRZgB8rNtonuMwZSAHQ== + dependencies: + "@codemirror/language" "^0.19.0" + "@codemirror/rangeset" "^0.19.0" + "@codemirror/state" "^0.19.0" + "@codemirror/view" "^0.19.0" + "@lezer/common" "^0.15.0" + style-mod "^4.0.0" + +"@codemirror/language@^0.19.0": + version "0.19.7" + resolved "https://registry.npmjs.org/@codemirror/language/-/language-0.19.7.tgz#9eef8e827692d93a701b18db9d46a42be34ecca6" + integrity sha512-pNNUtYWMIMG0lUSKyUXJr8U0rFiCKsKFXbA2Oj17PC+S1FY99hV0z1vcntW67ekAIZw9DMEUQnLsKBuIbAUX7Q== + dependencies: + "@codemirror/state" "^0.19.0" + "@codemirror/text" "^0.19.0" + "@codemirror/view" "^0.19.0" + "@lezer/common" "^0.15.5" + "@lezer/lr" "^0.15.0" + +"@codemirror/rangeset@^0.19.0": + version "0.19.2" + resolved "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.2.tgz#d7a999e4273c00fecef4aba8535a426073cdcddf" + integrity sha512-5d+X8LtmeZtfFtKrSx57bIHRUpKv2HD0b74clp4fGA7qJLLfYehF6FGkJJxJb8lKsqAga1gdjjWr0jiypmIxoQ== + dependencies: + "@codemirror/state" "^0.19.0" + +"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.3": + version "0.19.6" + resolved "https://registry.npmjs.org/@codemirror/state/-/state-0.19.6.tgz#d631f041d39ce41b7891b099fca26cb1fdb9763e" + integrity sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw== + dependencies: + "@codemirror/text" "^0.19.0" + +"@codemirror/stream-parser@^0.19.2": + version "0.19.2" + resolved "https://registry.npmjs.org/@codemirror/stream-parser/-/stream-parser-0.19.2.tgz#793428e55aa7b9daa64cb733973e5d5e3d9a2306" + integrity sha512-hBKRQlyu8GUOrY33xZ6/1kAfNZ8ZUm6cX9a7mPx8zAAqnpz/fpksC/qJRrkg1mPMBwxm+JG4fqAwDGJ3gLVniQ== + dependencies: + "@codemirror/highlight" "^0.19.0" + "@codemirror/language" "^0.19.0" + "@codemirror/state" "^0.19.0" + "@codemirror/text" "^0.19.0" + "@lezer/common" "^0.15.0" + "@lezer/lr" "^0.15.0" + +"@codemirror/text@^0.19.0": + version "0.19.5" + resolved "https://registry.npmjs.org/@codemirror/text/-/text-0.19.5.tgz#75033af2476214e79eae22b81ada618815441c18" + integrity sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow== + +"@codemirror/view@^0.19.0": + version "0.19.27" + resolved "https://registry.npmjs.org/@codemirror/view/-/view-0.19.27.tgz#76e5dc19ecb4ce53e9fef1d29245040d7ff64183" + integrity sha512-Uz/LecEf7CyvMWaQBlKtbJCYn0hRnEZ2yYvuZVy9YMhmvGmES6ec7FaKw7lDFFOMLwLbBThc9kfw4DCHreHN1w== + dependencies: + "@codemirror/rangeset" "^0.19.0" + "@codemirror/state" "^0.19.3" + "@codemirror/text" "^0.19.0" + style-mod "^4.0.0" + w3c-keyname "^2.2.4" + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -2650,9 +2796,9 @@ figures "^1.7.0" "@cypress/request@^2.88.5": - version "2.88.5" - resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" - integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + version "2.88.10" + resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" + integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -2661,19 +2807,17 @@ extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" + http-signature "~1.3.6" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" - oauth-sign "~0.9.0" performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" tough-cookie "~2.5.0" tunnel-agent "^0.6.0" - uuid "^3.3.2" + uuid "^8.3.2" "@cypress/xvfb@^1.2.4": version "1.2.4" @@ -2987,13 +3131,12 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" -"@graphiql/toolkit@^0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.3.2.tgz#551753436ada2bc27ea870b7668e5199a958ccfb" - integrity sha512-IweIT9VC8uDovg7kuCO9YqZcnIuWU8IGzrpUisXv6CUNK2Ed1ke8yERDTMmF/rjvLd2DeVZwM8iEOjEs4sUJQw== +"@graphiql/toolkit@^0.4.2": + version "0.4.2" + resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.4.2.tgz#34de819add64672f3f7d4830dffb2094fb8d5366" + integrity sha512-14uG67QrONbRrhXwvBJFsMfcQfexmGhj7dgkputesx9xuPUkcCDNmVULnVA8sGYt8P/rSvjkfQYx3rtfW+GhAQ== dependencies: - "@n1ru4l/push-pull-async-iterable-iterator" "^3.0.0" - graphql-ws "^4.9.0" + "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0" meros "^1.1.4" "@graphql-codegen/cli@^1.21.3": @@ -3176,6 +3319,16 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/batch-execute@^8.3.1": + version "8.3.1" + resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.3.1.tgz#0b74c54db5ac1c5b9a273baefc034c2343ebbb74" + integrity sha512-63kHY8ZdoO5FoeDXYHnAak1R3ysMViMPwWC2XUblFckuVLMUPmB2ONje8rjr2CvzWBHAW8c1Zsex+U3xhKtGIA== + dependencies: + "@graphql-tools/utils" "^8.5.1" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/code-file-loader@^6.3.1": version "6.3.1" resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.3.1.tgz#42dfd4db5b968acdb453382f172ec684fa0c34ed" @@ -3198,6 +3351,18 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/delegate@^8.4.1", "@graphql-tools/delegate@^8.4.2": + version "8.4.2" + resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.4.2.tgz#a61d45719855720304e3656800342cfa17d82558" + integrity sha512-CjggOhiL4WtyG2I3kux+1/p8lQxSFHBj0gwa0NxnQ6Vsnpw7Ig5VP1ovPnitFuBv2k4QdC37Nj2xv2n7DRn8fw== + dependencies: + "@graphql-tools/batch-execute" "^8.3.1" + "@graphql-tools/schema" "^8.3.1" + "@graphql-tools/utils" "^8.5.3" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/git-loader@^6.2.6": version "6.2.6" resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-6.2.6.tgz#c2226f4b8f51f1c05c9ab2649ba32d49c68cd077" @@ -3226,6 +3391,17 @@ "@graphql-tools/utils" "^7.0.0" tslib "~2.1.0" +"@graphql-tools/graphql-file-loader@^7.3.2": + version "7.3.3" + resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.3.3.tgz#7cee2f84f08dc13fa756820b510248b857583d36" + integrity sha512-6kUJZiNpYKVhum9E5wfl5PyLLupEDYdH7c8l6oMrk6c7EPEVs6iSUyB7yQoWrtJccJLULBW2CRQ5IHp5JYK0mA== + dependencies: + "@graphql-tools/import" "^6.5.7" + "@graphql-tools/utils" "^8.5.1" + globby "^11.0.3" + tslib "~2.3.0" + unixify "^1.0.0" + "@graphql-tools/graphql-tag-pluck@^6.2.6", "@graphql-tools/graphql-tag-pluck@^6.5.1": version "6.5.1" resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.5.1.tgz#5fb227dbb1e19f4b037792b50f646f16a2d4c686" @@ -3245,6 +3421,15 @@ resolve-from "5.0.0" tslib "~2.2.0" +"@graphql-tools/import@^6.5.7": + version "6.6.1" + resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.6.1.tgz#2a7e1ceda10103ffeb8652a48ddc47150b035485" + integrity sha512-i9WA6k+erJMci822o9w9DoX+uncVBK60LGGYW8mdbhX0l7wEubUpA000thJ1aarCusYh0u+ZT9qX0HyVPXu25Q== + dependencies: + "@graphql-tools/utils" "8.5.3" + resolve-from "5.0.0" + tslib "~2.3.0" + "@graphql-tools/json-file-loader@^6.0.0", "@graphql-tools/json-file-loader@^6.2.6": version "6.2.6" resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.6.tgz#830482cfd3721a0799cbf2fe5b09959d9332739a" @@ -3253,6 +3438,16 @@ "@graphql-tools/utils" "^7.0.0" tslib "~2.0.1" +"@graphql-tools/json-file-loader@^7.3.2": + version "7.3.3" + resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.3.3.tgz#45cfde77b9dc4ab6c21575305ae537d2814d237f" + integrity sha512-CN2Qk9rt+Gepa3rb3X/mpxYA5MIYLwZBPj2Njw6lbZ6AaxG+O1ArDCL5ACoiWiBimn1FCOM778uhRM9znd0b3Q== + dependencies: + "@graphql-tools/utils" "^8.5.1" + globby "^11.0.3" + tslib "~2.3.0" + unixify "^1.0.0" + "@graphql-tools/load@^6.0.0", "@graphql-tools/load@^6.2.8": version "6.2.8" resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.8.tgz#16900fb6e75e1d075cad8f7ea439b334feb0b96a" @@ -3268,6 +3463,16 @@ unixify "1.0.0" valid-url "1.0.9" +"@graphql-tools/load@^7.4.1": + version "7.4.1" + resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.4.1.tgz#aa572fcef11d6028097b6ef39c13fa9d62e5a441" + integrity sha512-UvBodW5hRHpgBUBVz5K5VIhJDOTFIbRRAGD6sQ2l9J5FDKBEs3u/6JjZDzbdL96br94D5cEd2Tk6auaHpTn7mQ== + dependencies: + "@graphql-tools/schema" "8.3.1" + "@graphql-tools/utils" "^8.5.1" + p-limit "3.1.0" + tslib "~2.3.0" + "@graphql-tools/merge@^6.0.0", "@graphql-tools/merge@^6.2.12", "@graphql-tools/merge@^6.2.14": version "6.2.14" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz#694e2a2785ba47558e5665687feddd2935e9d94e" @@ -3277,6 +3482,14 @@ "@graphql-tools/utils" "^7.7.0" tslib "~2.2.0" +"@graphql-tools/merge@^8.2.1": + version "8.2.1" + resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz#bf83aa06a0cfc6a839e52a58057a84498d0d51ff" + integrity sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA== + dependencies: + "@graphql-tools/utils" "^8.5.1" + tslib "~2.3.0" + "@graphql-tools/optimize@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.0.1.tgz#9933fffc5a3c63f95102b1cb6076fb16ac7bb22d" @@ -3320,6 +3533,16 @@ relay-compiler "10.1.0" tslib "~2.0.1" +"@graphql-tools/schema@8.3.1", "@graphql-tools/schema@^8.3.1": + version "8.3.1" + resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz#1ee9da494d2da457643b3c93502b94c3c4b68c74" + integrity sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ== + dependencies: + "@graphql-tools/merge" "^8.2.1" + "@graphql-tools/utils" "^8.5.1" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/schema@^7.0.0", "@graphql-tools/schema@^7.1.5": version "7.1.5" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.5.tgz#07b24e52b182e736a6b77c829fc48b84d89aa711" @@ -3354,6 +3577,38 @@ valid-url "1.0.9" ws "7.4.5" +"@graphql-tools/url-loader@^7.4.2": + version "7.5.3" + resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.5.3.tgz#a594be40e3bc68d22f76746356e7f0b8117b7137" + integrity sha512-VKMRJ4TOeVIdulkCLGSBUr4stRRwOGcVRXDeoUF+86K32Ufo0H2V0lz7QwS/bCl8GXV19FMgHZCDl4BMJyOXEA== + dependencies: + "@graphql-tools/delegate" "^8.4.1" + "@graphql-tools/utils" "^8.5.1" + "@graphql-tools/wrap" "^8.3.1" + "@n1ru4l/graphql-live-query" "0.9.0" + "@types/websocket" "1.0.4" + "@types/ws" "^8.0.0" + cross-undici-fetch "^0.0.26" + dset "^3.1.0" + extract-files "11.0.0" + graphql-sse "^1.0.1" + graphql-ws "^5.4.1" + isomorphic-ws "4.0.1" + meros "1.1.4" + subscriptions-transport-ws "^0.11.0" + sync-fetch "0.3.1" + tslib "~2.3.0" + valid-url "1.0.9" + value-or-promise "1.0.11" + ws "8.3.0" + +"@graphql-tools/utils@8.5.3", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.3": + version "8.5.3" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz#404062e62cae9453501197039687749c4885356e" + integrity sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg== + dependencies: + tslib "~2.3.0" + "@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0", "@graphql-tools/utils@^7.1.2", "@graphql-tools/utils@^7.5.0", "@graphql-tools/utils@^7.7.0", "@graphql-tools/utils@^7.7.1", "@graphql-tools/utils@^7.8.1", "@graphql-tools/utils@^7.9.0", "@graphql-tools/utils@^7.9.1": version "7.10.0" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699" @@ -3374,6 +3629,17 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/wrap@^8.3.1": + version "8.3.2" + resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-8.3.2.tgz#d3bcecb7529d071e4ecc4dfc75b9566e3da79d4f" + integrity sha512-7DcOBFB+Dd84x9dxSm7qS4iJONMyfLnCJb8A19vGPffpu4SMJ3sFcgwibKFu5l6mMUiigKgXna2RRgWI+02bKQ== + dependencies: + "@graphql-tools/delegate" "^8.4.2" + "@graphql-tools/schema" "^8.3.1" + "@graphql-tools/utils" "^8.5.3" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@grpc/grpc-js@~1.3.0": version "1.3.2" resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.2.tgz#eae97e6daf5abd49a7818aadeca0744dfb1ebca1" @@ -3676,12 +3942,17 @@ resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@kubernetes/client-node@^0.15.0": - version "0.15.0" - resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.15.0.tgz#aa5cfcfa9ba3055fe0b510c430d19bbda715d8e7" - integrity sha512-AnEcsWWadl5IWOzzvO/gWpTnJb1d1CzA/rbV/qK1c0fD1SOxTDPj6jFllyQ9icGDfCgNw3TafZftmuepm6z9JA== +"@juggle/resize-observer@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" + integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== + +"@kubernetes/client-node@^0.16.0": + version "0.16.1" + resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.16.1.tgz#c78ef667579777c1a532983922807e228dbc9b90" + integrity sha512-/Ah+3gFSjXFeqDMGGTyYBKug44Eu2D2qowKLdiZqxCkHdSNgy+CNk6FU1Vy80WrTvGkF/CZr4az6O5AopAiJEw== dependencies: - "@types/js-yaml" "^3.12.1" + "@types/js-yaml" "^4.0.1" "@types/node" "^10.12.0" "@types/request" "^2.47.1" "@types/stream-buffers" "^3.0.3" @@ -3691,7 +3962,7 @@ byline "^5.0.0" execa "5.0.0" isomorphic-ws "^4.0.1" - js-yaml "^3.13.1" + js-yaml "^4.1.0" jsonpath-plus "^0.19.0" openid-client "^4.1.1" request "^2.88.0" @@ -4375,6 +4646,18 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@lezer/common@^0.15.0", "@lezer/common@^0.15.5": + version "0.15.10" + resolved "https://registry.npmjs.org/@lezer/common/-/common-0.15.10.tgz#662da668f46244fb20bfaada67b43b3d0463b344" + integrity sha512-vlr+be73zTDoQBIknBVOh/633tmbQcjxUu9PIeVeYESeBK3V6TuBW96RRFg93Y2cyK9lglz241gOgSn452HFvA== + +"@lezer/lr@^0.15.0": + version "0.15.5" + resolved "https://registry.npmjs.org/@lezer/lr/-/lr-0.15.5.tgz#4bce44169c441d9dda7be398f5202ea65c5f1138" + integrity sha512-DEcLyhdmBxD1foQe7RegLrSlfS/XaTMGLkO5evkzHWAQKh/JnFWp7j7iNB7s2EpxzRrBCh0U+W7JDCeFhv2mng== + dependencies: + "@lezer/common" "^0.15.0" + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" @@ -4428,7 +4711,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.2": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -4533,13 +4816,6 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@mattiasbuelens/web-streams-polyfill@^0.2.0": - version "0.2.1" - resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" - integrity sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg== - dependencies: - "@types/whatwg-streams" "^0.0.7" - "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -4672,7 +4948,12 @@ outvariant "^1.2.0" strict-event-emitter "^0.2.0" -"@n1ru4l/push-pull-async-iterable-iterator@^3.0.0": +"@n1ru4l/graphql-live-query@0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc" + integrity sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg== + +"@n1ru4l/push-pull-async-iterable-iterator@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.1.0.tgz#be450c97d1c7cd6af1a992d53232704454345df9" integrity sha512-K4scWxGhdQM0masHHy4gIQs2iGiLEXCrXttumknyPJqtdl4J179BjpibWSSQ1fxKdCcHgIlCTKXJU6cMM6D6Wg== @@ -5239,29 +5520,6 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-3.2.1.tgz#84fbf322485aee3a84101e189161f0687779ec8d" integrity sha512-8UiDeDbjCImFSfOegGu13otQ7OdP9FOYpcLjeouppnhs+MPeIEAtYS+jCcBKmi3reyTagC15/KVSRhde1wS1vg== -"@roadiehq/backstage-plugin-buildkite@^1.0.8": - version "1.0.8" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-1.0.8.tgz#c377ae194682426a957366e85263749ff75b8db1" - integrity sha512-v3OOQj5Ksvs/8SZcNBgq6bzmyJvMIzdGsQrwW8uaLgOX076T0nmK8c3Ojz3w1opymEl3s20sLxLUXjhd7V/nIA== - dependencies: - "@backstage/catalog-model" "^0.9.0" - "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.3.0" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.4.0" - "@backstage/theme" "^0.2.6" - "@material-ui/core" "^4.12.1" - "@material-ui/icons" "^4.11.2" - "@material-ui/lab" "4.0.0-alpha.57" - history "^5.0.0" - moment "^2.29.1" - react "^16.13.1" - react-dom "^16.13.1" - react-lazylog "^4.5.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^17.2.4" - "@roadiehq/backstage-plugin-github-insights@^1.1.23": version "1.2.2" resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.2.2.tgz#09d958ed15adbb598afda34187f45deedec2264d" @@ -5346,7 +5604,7 @@ magic-string "^0.25.7" resolve "^1.17.0" -"@rollup/plugin-json@^4.0.2": +"@rollup/plugin-json@^4.1.0": version "4.1.0" resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== @@ -5365,10 +5623,10 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-yaml@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@rollup/plugin-yaml/-/plugin-yaml-3.0.0.tgz#632af397a6faaea10c885680c575ae01f4556c1d" - integrity sha512-nXYI3WsfG7Oq5K5k7evGQ1ZABGCPhHxMaMX4T3GRSDi9VyhkaU1+8R1RzTRFXITFJHx0QEGn8jR+P9pux5FuBQ== +"@rollup/plugin-yaml@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-yaml/-/plugin-yaml-3.1.0.tgz#03a13039ba366fc8d39a1ab94a7debacdd776c2f" + integrity sha512-61PsAXqN7YNYdg/nezK3NkqAu6e3Qu2wjHYW3r52Nx0aLi+rG7gkkIqtvxG8EtSqE2rra5CUcWBZj+v362qt9A== dependencies: "@rollup/pluginutils" "^3.1.0" js-yaml "^3.14.0" @@ -5383,10 +5641,10 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz#0dcc61c780e39257554feb7f77207dceca13c838" - integrity sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ== +"@rollup/pluginutils@^4.1.1": + version "4.1.1" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" + integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ== dependencies: estree-walker "^2.0.1" picomatch "^2.2.2" @@ -5540,10 +5798,10 @@ resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz#4c7af3f74a47668bec0c860b72e2a0103e78a138" integrity sha512-nMVll8ZkN/W8+IHn6Iz3YzCKW0qhrn3TVfyxkAr3qmXm5cex+GzyUdZEuxb8rdN2inZL6A1Il2NFfO5p/UKxog== -"@spotify/prettier-config@^11.0.0": - version "11.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-11.0.0.tgz#d91e0546a8c1c0f7299e2edc7e44306e9be210f6" - integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ== +"@spotify/prettier-config@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-12.0.0.tgz#936ca5e977cfccbccd1731ab98b1f2bf65852b5d" + integrity sha512-64WWqE40U/WwWV8iIQBseTU+b2t+SdJSyQoCLdVPCKM9uf7KOjRivVwXe4KlWoV3y7duNSGuB2UgWhkXzscVmQ== "@storybook/addon-a11y@^6.3.4": version "6.3.12" @@ -6768,6 +7026,13 @@ dependencies: "@types/node" "*" +"@types/ansi-regex@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-5.0.0.tgz#569a5189a92cc46d63fb2ad91e6b130f33d999c1" + integrity sha512-SQafVL3pXFh/5qq/nN6p5858g//zSVzcb8JzCLtoVxm8YNPggMQfEIm7aaTNysxpw1S+lFTaW8kv+aR0/CEhCA== + dependencies: + ansi-regex "*" + "@types/archiver@^5.1.0": version "5.3.0" resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" @@ -6925,9 +7190,9 @@ commander "*" "@types/compression@^1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz#8dc2a56604873cf0dd4e746d9ae4d31ae77b2390" - integrity sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw== + version "1.7.2" + resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz#7cc1cdb01b4730eea284615a68fc70a2cdfd5e71" + integrity sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg== dependencies: "@types/express" "*" @@ -7094,6 +7359,13 @@ "@types/docker-modem" "*" "@types/node" "*" +"@types/dompurify@^2.1.0": + version "2.3.1" + resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd" + integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA== + dependencies: + "@types/trusted-types" "*" + "@types/dompurify@^2.2.2": version "2.2.3" resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08" @@ -7397,16 +7669,16 @@ resolved "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.0.tgz#9541eec4ad6e3ec5633270a3a2b55d981edc44a9" integrity sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ== -"@types/js-yaml@^3.12.1": - version "3.12.5" - resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" - integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== - "@types/js-yaml@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.1.tgz#5544730b65a480b18ace6b6ce914e519cec2d43b" integrity sha512-xdOvNmXmrZqqPy3kuCQ+fz6wA0xU5pji9cd1nDrflWaAWtYLLGk5ykW0H6yg5TVyehHP1pfmuuSaZkhP+kspVA== +"@types/js-yaml@^4.0.1": + version "4.0.5" + resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + "@types/jscodeshift@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.11.0.tgz#7224cf1a4d0383b4fb2694ffed52f57b45c3325b" @@ -7422,7 +7694,7 @@ dependencies: "@types/json-schema" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@7.0.9", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -7823,14 +8095,6 @@ dependencies: "@types/react" "*" -"@types/react-lazylog@^4.5.0": - version "4.5.1" - resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.1.tgz#babb5d814f7035b5434518769975e12f299356a8" - integrity sha512-g4yeosa1zYhu2BUJmuu2H2o0dsdRj0o8Omw3pBiVHdLHJaeYIyArvyMRR3bI/MxZxG4EaiRl8AOQ6zeM8P46jA== - dependencies: - "@types/react" "*" - immutable ">=3.8.2" - "@types/react-redux@^7.1.16": version "7.1.19" resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.19.tgz#477bd0a9b01bae6d6bf809418cdfa7d3c16d4c62" @@ -7883,14 +8147,21 @@ dependencies: "@types/react" "*" -"@types/react-wait@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@types/react-wait/-/react-wait-0.3.1.tgz#193cbd8fe86baa53b6f65dfa73f03d562f462a27" - integrity sha512-BS9AEjWZItDgpx6LlICcuf53M27zBFCsHx/llCbrmrt/WI7ecG2LGquCss3n8O8bwEDiTX4yYLjy8yLeIfgYTg== +"@types/react-virtualized-auto-sizer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" + integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0": +"@types/react-window@^1.8.5": + version "1.8.5" + resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" + integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.13.1 || ^17.0.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -8274,10 +8545,12 @@ dependencies: "@types/node" "*" -"@types/whatwg-streams@^0.0.7": - version "0.0.7" - resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" - integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== +"@types/websocket@1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8" + integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA== + dependencies: + "@types/node" "*" "@types/ws@^6.0.1": version "6.0.4" @@ -8293,6 +8566,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.0.0": + version "8.2.2" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + "@types/xml2js@*", "@types/xml2js@^0.4.7": version "0.4.8" resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a" @@ -8767,6 +9047,11 @@ abab@^2.0.3: resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== +abab@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -8830,6 +9115,11 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.2.4: + version "8.6.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" + integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== + acorn@^8.4.1: version "8.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" @@ -9005,6 +9295,11 @@ ansi-html@0.0.7, ansi-html@^0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= +ansi-regex@*, ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -9025,11 +9320,6 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -9426,16 +9716,16 @@ array-ify@^1.0.0: resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.0.3, array-includes@^3.1.1, array-includes@^3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" - integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== +array-includes@^3.0.3, array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + es-abstract "^1.19.1" get-intrinsic "^1.1.1" - is-string "^1.0.5" + is-string "^1.0.7" array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" @@ -9467,15 +9757,14 @@ array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.2.3, array.prototype.flatmap@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" - integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== +array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.2.4, array.prototype.flatmap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" + integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== dependencies: call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.19.0" array.prototype.map@^1.0.1: version "1.0.2" @@ -11226,7 +11515,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -11262,13 +11551,13 @@ code-point-at@^1.0.0: resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemirror-graphql@^1.0.3: - version "1.1.0" - resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.1.0.tgz#dd22ddf7761efa9131fa99a70a4a85fe653484e5" - integrity sha512-bp2XUg7epL07kJcylM8VCISK6X+rFsHL2lUkPQAw2v721MVhn+80FgjMP8tiZCOfJgHn1+JgsA71L5nOHWgUdA== +codemirror-graphql@^1.2.8: + version "1.2.8" + resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.2.8.tgz#4d3845d786665776eb5c44b0948bfa8f9860077f" + integrity sha512-/SlF24YNWirA8SyyaiFkrwPVIhPS/OkMNRHkL3TeZKhJIv3wuGAa90B4DYqY17MBN2QN1+RYd/71eYZD/fvj1Q== dependencies: - graphql-language-service-interface "^2.9.0" - graphql-language-service-parser "^1.10.0" + "@codemirror/stream-parser" "^0.19.2" + graphql-language-service "^4.1.1" codemirror@^5.58.2: version "5.63.3" @@ -11642,11 +11931,6 @@ constants-browserify@^1.0.0: resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -constate@^1.2.0: - version "1.3.2" - resolved "https://registry.npmjs.org/constate/-/constate-1.3.2.tgz#fa5f0fc292207f1ec21b46a5eb81f59c8b0a8b84" - integrity sha512-aaILV4vXwGTUZaQZHS5F1xBV8wRCR0Ow1505fdkS5/BPg6hbQrhNqdHL4wgxWgaDeEj43mu/Fb+LhqOKTMcrgQ== - contains-path@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -11806,7 +12090,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -copy-to-clipboard@^3, copy-to-clipboard@^3.1.0, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1: +copy-to-clipboard@^3, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== @@ -11875,6 +12159,17 @@ cosmiconfig@7.0.0, cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -12033,6 +12328,16 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +cross-undici-fetch@^0.0.26: + version "0.0.26" + resolved "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.0.26.tgz#29d93d56609f4d2334f9d5333d23ef7a242842a7" + integrity sha512-aMDRrLbWr0TGXfY92stlV+XOGpskeqFmWmrKSWsnc8w6gK5LPE83NBh7O7N6gCb2xjwHcm1Yn2nBXMEVH2RBcA== + dependencies: + abort-controller "^3.0.0" + form-data "^4.0.0" + node-fetch "^2.6.5" + undici "^4.9.3" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -12297,7 +12602,7 @@ cssom@~0.3.6: resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssstyle@^2.2.0: +cssstyle@^2.2.0, cssstyle@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== @@ -12764,6 +13069,11 @@ decimal.js@^10.2.0: resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -13164,7 +13474,7 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" -dom-helpers@^5.0.0, dom-helpers@^5.0.1: +dom-helpers@^5.0.1: version "5.1.4" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== @@ -13242,7 +13552,7 @@ domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -dompurify@^2.1.1, dompurify@^2.2.9: +dompurify@^2.2.7, dompurify@^2.2.9: version "2.3.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== @@ -13561,11 +13871,6 @@ entities@^2.0.0, entities@~2.1.0: resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -entities@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== - env-paths@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -13612,7 +13917,7 @@ error@^10.4.0: resolved "https://registry.npmjs.org/error/-/error-10.4.0.tgz#6fcf0fd64bceb1e750f8ed9a3dd880f00e46a487" integrity sha512-YxIFEJuhgcICugOUvRx5th0UM+ActZ9sjY0QJmeVwsQdvosZ7kYzc9QqS0Da3R5iUmgU5meGIxh0xBeZpMVeLw== -es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: +es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: version "1.18.0" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== @@ -13634,6 +13939,32 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es- string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.0" +es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + es-array-method-boxes-properly@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" @@ -13712,118 +14043,113 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -esbuild-android-arm64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.14.tgz#c85083ece26be3d67e6c720e088968a98409e023" - integrity sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA== +esbuild-android-arm64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.1.tgz#470b99c1c4b49f33fd0a20ed153b15008173fd63" + integrity sha512-elQd3hTg93nU2GQ5PPCDAFe5+utxZX96RG8RixqIPxf8pzmyIzcpKG76L/9FabPf3LT1z+nLF1sajCU8eVRDyg== -esbuild-darwin-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.14.tgz#8e4e237ad847cc54a1d3a5caee26a746b9f0b81f" - integrity sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ== +esbuild-darwin-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.1.tgz#135f48f299f2ce3eb3ca1b1f3ec03d81108ab79e" + integrity sha512-PR3HZgbPRwsQbbOR1fJrfkt/Cs0JDyI3yzOKg2PPWk0H1AseZDBqPUY9b/0+BIjFwA5Jz/aAiq832hppsuJtNw== -esbuild-darwin-arm64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.14.tgz#b3b5ebd40b2cb06ee0f6fb342dd4bdcca54ad273" - integrity sha512-Lp00VTli2jqZghSa68fx3fEFCPsO1hK59RMo1PRap5RUjhf55OmaZTZYnCDI0FVlCtt+gBwX5qwFt4lc6tI1xg== +esbuild-darwin-arm64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.1.tgz#7117a857bac99ece28ebba859a47dce47f565f9f" + integrity sha512-/fiSSOkOEa3co6yYtwgXouz8jZrG0qnXPEKiktFf2BQE8NON3ARTw43ZegaH+xMRFNgYBJEOOZIdzI3sIFEAxw== -esbuild-freebsd-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.14.tgz#175ecb2fa8141428cf70ea2d5f4c27534bad53e0" - integrity sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw== +esbuild-freebsd-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.1.tgz#2b7ca5ec572f2800b1ec88988affc4482c5ac4b7" + integrity sha512-ZJV+nfa8E8PdXnRc05PO3YMfgSj7Ko+kdHyGDE6OaNo1cO8ZyfacqLaWkY35shDDaeacklhD8ZR4qq5nbJKX1A== -esbuild-freebsd-arm64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.14.tgz#a7d64e41d1fa581f8db7775e5200f18e67d70c4d" - integrity sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q== +esbuild-freebsd-arm64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.1.tgz#63e8b77643ea8270d878cfab7dd9201a114f20fb" + integrity sha512-6N9zTD+SecJr2g9Ohl9C10WIk5FpQ+52bNamRy0sJoHwP31G5ObzKzq8jAtg1Jeggpu6P8auz3P/UL+3YioSwQ== -esbuild-linux-32@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.14.tgz#14bdd4f6b6cfd35c65c835894651ba335c2117da" - integrity sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA== +esbuild-linux-32@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.1.tgz#f00ae7f12d2abc0dc37e2a7e7c7c29764da87093" + integrity sha512-RtPgE6e7WefbAxRjVryisKFJ0nUwR2DMjwmYW/a1a0F1+Ge6FR+RqvgiY0DrM9TtxSUU0eryDXNF4n3UfxX3mg== -esbuild-linux-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.14.tgz#7fd56851b2982fdd0cd8447ee9858c2c5711708a" - integrity sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ== +esbuild-linux-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.1.tgz#2ee9dd76be1185abb1e967052e3b6ab16a1d3da4" + integrity sha512-JpxM0ar6Z+2v3vfFrxP7bFb8Wzb6gcGL9MxRqAJplDfGnee8HbfPge6svaazXeX9XJceeEqwxwWGB0qyCcxo7A== -esbuild-linux-arm64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.14.tgz#a55634d70679ba509adeafd68eebb9fd1ec5af6c" - integrity sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ== +esbuild-linux-arm64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.1.tgz#601e855b78e0636e120771296b43eb4f7d68a314" + integrity sha512-cFbeZf171bIf+PPLlQDBzagK85lCCxxVdMV1IVUA96Y3kvEgqcy2n9mha+QE1M/T+lIOPDsmLRgH1XqMFwLTSg== -esbuild-linux-arm@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.14.tgz#bb96a99677e608b31ff61f37564326d38e846ca2" - integrity sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg== +esbuild-linux-arm@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.1.tgz#c0d364a20f12a653bdd2f41436788b99502dc287" + integrity sha512-eBRHexCijAYWzcvQLGHxyxIlYOkYhXvcb/O7HvzJfCAVWCnTx9TxxYJ3UppBC6dDFbAq4HwKhskvmesQdKMeBg== -esbuild-linux-mips64le@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.14.tgz#6a55362a8fd1e593dea2ecc41877beed8b8184b9" - integrity sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ== +esbuild-linux-mips64le@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.1.tgz#a5f6e9c6e7950a3fad08bb3653bc3f5d71b4e249" + integrity sha512-UGb+sqHkL7wOQFLH0RoFhcRAlJNqbqs6GtJd1It5jJ2juOGqAkCv8V12aGDX9oRB6a+Om7cdHcH+6AMZ+qlaww== -esbuild-linux-ppc64le@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.14.tgz#9e0048587ece0a7f184ab147f20d077098045e7f" - integrity sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw== +esbuild-linux-ppc64le@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.1.tgz#762cec24cf5afeee3f805a4679a3f5e29702173a" + integrity sha512-LIHGkGdy9wYlmkkoVHm6feWhkoi4VBXDiEVyNjXEhlzsBcP/CaRy+B8IJulzaU1ALLiGcsCQ2MC5UbFn/iTvmA== -esbuild-netbsd-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.14.tgz#dcab16a4bbcfa16e2e8535dadc5f64fdc891c63b" - integrity sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q== +esbuild-netbsd-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.1.tgz#66ec7ac0b3eeb84f8c1ac27eecf16f59d93706a8" + integrity sha512-TWc1QIgtPwaK5nC1GT2ASTuy/CJhNKHN4h5PJRP1186VfI+k2uvXakS7bqO/M26F6jAMy8jDeCtilacqpwsvfA== -esbuild-openbsd-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.14.tgz#3c7453b155ebb68dc34d5aec3bd6505337bdda08" - integrity sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA== +esbuild-openbsd-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.1.tgz#7515049bc7032ca2fb6811dc260f5ec9e1d9fe65" + integrity sha512-Z9/Zb77K+pK9s7mAsvwS56K8tCbLvNZ9UI4QVJSYqDgOmmDJOBT4owWnCqZ5cJI+2y4/F9KwCpFFTNUdPglPKA== -esbuild-sunos-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.14.tgz#85addf5fef6b5db154a955d4f2e88953359d75ce" - integrity sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w== +esbuild-sunos-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.1.tgz#795f6bc7ce8c5177afb65f8d6c161a02f0c3e125" + integrity sha512-c4sF8146kNW8529wfkB6vO0ZqPgokyS2hORqKa4p/QKZdp+xrF2NPmvX5aN+Zt14oe6wVZuhYo6LGv7V4Gg04g== -esbuild-windows-32@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.14.tgz#f77f98f30a5c636c44db2428ecdf9bcbbaedb1a7" - integrity sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg== +esbuild-windows-32@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.1.tgz#ffffa6378733eeaa23ed5cfe539e2fbe1e635ef6" + integrity sha512-XP8yElaJtLGGjH7D72t5IWtP0jmc1Jqm4IjQARB17l0LTJO/n+N2X64rDWePJv6qimYxa5p2vTjkZc5v+YZTSQ== -esbuild-windows-64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.14.tgz#bc778674c40d65150d12385e0f23eb3a0badbd0d" - integrity sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw== +esbuild-windows-64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.1.tgz#46f3b4a90f937a8ad6456cd70478ebfc6771814f" + integrity sha512-fe+ShdyfiuGcCEdVKW//6MaM4MwikiWBWSBn8mebNAbjRqicH0injDOFVI7aUovAfrEt7+FGkf402s//hi0BVg== -esbuild-windows-arm64@0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.14.tgz#91a8dad35ab2c4dd27cd83860742955b25a354d7" - integrity sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A== +esbuild-windows-arm64@0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.1.tgz#c7067389d28139e6a18db1996178c3a3e07a22b3" + integrity sha512-wBVakhcIzQ3NZ33DFM6TjIObXPHaXOsqzvPwefXHvwBSC/N/e/g6fBeM7N/Moj3AmxLjKaB+vePvTGdxk6RPCg== -esbuild@^0.13.14: - version "0.13.14" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.13.14.tgz#98a3f7f42809abdc2b57c84565d0f713382dc1a5" - integrity sha512-xu4D+1ji9x53ocuomcY+KOrwAnWzhBu/wTEjpdgZ8I1c8i5vboYIeigMdzgY1UowYBKa2vZgVgUB32bu7gkxeg== +esbuild@^0.14.1: + version "0.14.1" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.1.tgz#b834da3aa5858073205a6d4f948ffde0d650e4e3" + integrity sha512-J/LhUwELcmz0+CJfiaKzu7Rnj9ffWFLvMx+dKvdOfg+fQmoP6q9glla26LCm9BxpnPUjXChHeubLiMlKab/PYg== optionalDependencies: - esbuild-android-arm64 "0.13.14" - esbuild-darwin-64 "0.13.14" - esbuild-darwin-arm64 "0.13.14" - esbuild-freebsd-64 "0.13.14" - esbuild-freebsd-arm64 "0.13.14" - esbuild-linux-32 "0.13.14" - esbuild-linux-64 "0.13.14" - esbuild-linux-arm "0.13.14" - esbuild-linux-arm64 "0.13.14" - esbuild-linux-mips64le "0.13.14" - esbuild-linux-ppc64le "0.13.14" - esbuild-netbsd-64 "0.13.14" - esbuild-openbsd-64 "0.13.14" - esbuild-sunos-64 "0.13.14" - esbuild-windows-32 "0.13.14" - esbuild-windows-64 "0.13.14" - esbuild-windows-arm64 "0.13.14" - -esbuild@^0.8.56: - version "0.8.57" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.57.tgz#a42d02bc2b57c70bcd0ef897fe244766bb6dd926" - integrity sha512-j02SFrUwFTRUqiY0Kjplwjm1psuzO1d6AjaXKuOR9hrY0HuPsT6sV42B6myW34h1q4CRy+Y3g4RU/cGJeI/nNA== + esbuild-android-arm64 "0.14.1" + esbuild-darwin-64 "0.14.1" + esbuild-darwin-arm64 "0.14.1" + esbuild-freebsd-64 "0.14.1" + esbuild-freebsd-arm64 "0.14.1" + esbuild-linux-32 "0.14.1" + esbuild-linux-64 "0.14.1" + esbuild-linux-arm "0.14.1" + esbuild-linux-arm64 "0.14.1" + esbuild-linux-mips64le "0.14.1" + esbuild-linux-ppc64le "0.14.1" + esbuild-netbsd-64 "0.14.1" + esbuild-openbsd-64 "0.14.1" + esbuild-sunos-64 "0.14.1" + esbuild-windows-32 "0.14.1" + esbuild-windows-64 "0.14.1" + esbuild-windows-arm64 "0.14.1" escalade@^3.0.2, escalade@^3.1.1: version "3.1.1" @@ -13872,6 +14198,18 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^8.3.0: version "8.3.0" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" @@ -13992,21 +14330,24 @@ eslint-plugin-react-hooks@^4.0.0: integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== eslint-plugin-react@^7.12.4: - version "7.22.0" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" - integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== + version "7.27.1" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz#469202442506616f77a854d91babaae1ec174b45" + integrity sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA== dependencies: - array-includes "^3.1.1" - array.prototype.flatmap "^1.2.3" + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" doctrine "^2.1.0" - has "^1.0.3" + estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" - object.entries "^1.1.2" - object.fromentries "^2.0.2" - object.values "^1.1.1" + minimatch "^3.0.4" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" prop-types "^15.7.2" - resolve "^1.18.1" - string.prototype.matchall "^4.0.2" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.6" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" @@ -14132,10 +14473,10 @@ estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== estree-walker@^0.6.1: version "0.6.1" @@ -14377,9 +14718,9 @@ expect@^26.6.2: jest-regex-util "^26.0.0" express-prom-bundle@^6.3.6: - version "6.3.6" - resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.3.6.tgz#c8da1c1024edfcc54953c365991aca57ffd0cfda" - integrity sha512-IRsTRCEKCVCHEriQlZ1FuutjEFc89KASsveXh+1HcGEnuZKiAC4LugxrsGEIdySqYvqOYSr2SWHJ6L8/BK2SHA== + version "6.4.1" + resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.4.1.tgz#a688050b9e090f6969825c33143106d3e0e5a70e" + integrity sha512-Sg0svLQe/SS5z1tHDTVfZVjNumobiDlXM0jmemt5Dm9K6BX8z9yCwEr93zbko6fNMR4zKav77iPfxUWi6gAjNA== dependencies: on-finished "^2.3.0" url-value-parser "^2.0.0" @@ -14505,6 +14846,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-files@11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" + integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== + extract-files@9.0.0, extract-files@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" @@ -14690,11 +15036,6 @@ fecha@^4.2.0: resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== -fetch-readablestream@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" - integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== - figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -15219,7 +15560,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@^2.1.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -15343,7 +15684,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -15415,6 +15756,14 @@ get-stream@^6.0.0: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718" integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -15843,19 +16192,19 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphiql@^1.0.0-alpha.10: - version "1.4.7" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.7.tgz#6a35acf0786d7518fbb986b75bf0a3d752c19c1a" - integrity sha512-oHsBTzdWTbRJhqazbjrC6wY7YInViErAeXLqetCxdFFu2Zk5FV3V3rs7KPrCyr7kM6lW0nfXMzIfKuIgxAqx7g== +graphiql@^1.5.12: + version "1.5.13" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.5.13.tgz#7706c56504213598641e8300a612a7dccaba95d5" + integrity sha512-lfG7FZzjDb4jwAP5mU+IiYvkTcZ/OygbIlk6JJrB0CPBqMl4k/3dEaZFmyKjmy54xTo8bkiH28VgpcF8VPc1eg== dependencies: - "@graphiql/toolkit" "^0.3.2" + "@graphiql/toolkit" "^0.4.2" codemirror "^5.58.2" - codemirror-graphql "^1.0.3" + codemirror-graphql "^1.2.8" copy-to-clipboard "^3.2.0" dset "^3.1.0" entities "^2.0.0" escape-html "^1.0.3" - graphql-language-service "^3.1.6" + graphql-language-service "^4.1.1" markdown-it "^12.2.0" graphlib@^2.1.8: @@ -15882,6 +16231,23 @@ graphql-config@^3.0.2, graphql-config@^3.3.0: minimatch "3.0.4" string-env-interpolation "1.0.1" +graphql-config@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/graphql-config/-/graphql-config-4.1.0.tgz#a3b28d3fb537952ebeb69c75e4430605a10695e3" + integrity sha512-Myqay6pmdcmX3KqoH+bMbeKZ1cTODpHS2CxF1ZzNnfTE+YUpGTcp01bOw6LpzamRb0T/WTYtGFbZeXGo9Hab2Q== + dependencies: + "@endemolshinegroup/cosmiconfig-typescript-loader" "3.0.2" + "@graphql-tools/graphql-file-loader" "^7.3.2" + "@graphql-tools/json-file-loader" "^7.3.2" + "@graphql-tools/load" "^7.4.1" + "@graphql-tools/merge" "^8.2.1" + "@graphql-tools/url-loader" "^7.4.2" + "@graphql-tools/utils" "^8.5.1" + cosmiconfig "7.0.1" + cosmiconfig-toml-loader "1.0.0" + minimatch "3.0.4" + string-env-interpolation "1.0.1" + graphql-extensions@^0.15.0: version "0.15.0" resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817" @@ -15891,63 +16257,51 @@ graphql-extensions@^0.15.0: apollo-server-env "^3.1.0" apollo-server-types "^0.9.0" -graphql-language-service-interface@^2.9.0: - version "2.9.1" - resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.9.1.tgz#be0b11b06b78730ea9d250e0e2290e7ed9c8d283" - integrity sha512-yGsE67fxJBXxY82+rLDMvUpmzpOUM8XFB+k+xOTUyABWs27osKaoGiuDDXAVGg1adhm+cpunWbipe763ZJkAVA== +graphql-language-service-interface@^2.10.1: + version "2.10.1" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.10.1.tgz#0f81a3da576bad61da878b59c228901236358426" + integrity sha512-2n/nrt0QD8UnxzDKKHWomYuLK9uxGxyPztA6wI24Kng8Iw7jQk/doIti63z4xPKNi4CtbumSjk1TRYUZ/bgViw== dependencies: - graphql-language-service-parser "^1.10.0" - graphql-language-service-types "^1.8.3" - graphql-language-service-utils "^2.6.0" + graphql-config "^4.1.0" + graphql-language-service-parser "^1.10.4" + graphql-language-service-types "^1.8.7" + graphql-language-service-utils "^2.7.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.0.tgz#116f4be849754f6afb4c196421a43fe96d87b278" - integrity sha512-cLExv0EjqT2hsKdwVTPmKU6eMfjZAjxqywgCPnWD48eJn6tyuePMyG7ye+jpX1PRPPx/cDHfFJGf8sUclchvng== +graphql-language-service-parser@^1.10.4: + version "1.10.4" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.4.tgz#b2979deefc5c0df571dacd409b2d5fbf1cdf7a9d" + integrity sha512-duDE+0aeKLFVrb9Kf28U84ZEHhHcvTjWIT6dJbIAQJWBaDoht0D4BK9EIhd94I3DtKRc1JCJb2+70y1lvP/hiA== dependencies: - graphql-language-service-types "^1.8.0" + graphql-language-service-types "^1.8.7" -graphql-language-service-types@^1.8.0: - version "1.8.1" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.1.tgz#963810010924f2b5eaea415d5b8eb0b7d42c479b" - integrity sha512-IpYS0mEHEmRsFlq+loWCpSYYYizAID7Alri6GoFN1QqUdux+8rp1Tkp2NGsGDpDmm3Dbz5ojmJWzNWQGpuwveA== - -graphql-language-service-types@^1.8.2: - version "1.8.2" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.2.tgz#50ae56f69cc24fcfc3daa129b68b0eb9421e8578" - integrity sha512-Sj07RHnMwAhEvAt7Jdt1l/x56ZpoNh+V6g+T58CF6GiYqI5l4vXqqRB4d4xHDcNQX98GpJfnf3o8BqPgP3C5Sw== - -graphql-language-service-types@^1.8.3: - version "1.8.3" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.3.tgz#d7d688d74c122c4d9cc4cceae761a1f2a3c396a1" - integrity sha512-m+RHnlGkKDcesW/gC4M7I2pSmWJB84uWS6LtnjplO/07JN312nJCJYCwV/DBny2m1fmSOxN7H/o+JW0l56KwBA== - -graphql-language-service-utils@^2.5.3: - version "2.5.3" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.3.tgz#185f4f65cf8c010871eb9405452a3a0bfdf88748" - integrity sha512-ydevEZ0AgzEKQF3hiCbLXuS0o7189Ww/T30WtCKCLaRHDYk9Yyb2PZWdhSTWLxYZTaX2TccV6NtFWvzIC7UP3g== +graphql-language-service-types@^1.8.7: + version "1.8.7" + resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.7.tgz#f5e909e6d9334ea2d8d1f7281b695b6f5602c07f" + integrity sha512-LP/Mx0nFBshYEyD0Ny6EVGfacJAGVx+qXtlJP4hLzUdBNOGimfDNtMVIdZANBXHXcM41MDgMHTnyEx2g6/Ttbw== dependencies: - graphql-language-service-types "^1.8.0" + graphql-config "^4.1.0" + vscode-languageserver-types "^3.15.1" + +graphql-language-service-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.7.1.tgz#c97c8d744a761480aba7e03e4a42adf28b6fce39" + integrity sha512-Wci5MbrQj+6d7rfvbORrA9uDlfMysBWYaG49ST5TKylNaXYFf3ixFOa74iM1KtM9eidosUbI3E1JlWi0JaidJA== + dependencies: + "@types/json-schema" "7.0.9" + graphql-language-service-types "^1.8.7" nullthrows "^1.0.0" -graphql-language-service-utils@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.6.0.tgz#d04904641248167ccbb381d8705ba97daa784954" - integrity sha512-idqwmbREixhDuQMcYp8WH0btQT02xZny8MO/HduNTVjnPrmTYnZUbpZ9AejdflmaKoS0o8nNvgXQ0GpIOzbG5g== +graphql-language-service@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-4.1.1.tgz#8093655c23af6a3f3eee3f92e6a5b0a3e2a21d70" + integrity sha512-7XP+XSzTnpnmh+EYbofnK/YdSaI7/mj7bS8ffP3A1UQtNBLb9mUErgYju6VegcV6Df0bJObLG+XxSBSlgV4kXQ== dependencies: - graphql-language-service-types "^1.8.3" - nullthrows "^1.0.0" - -graphql-language-service@^3.1.6: - version "3.2.0" - resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.2.0.tgz#e0eb6d5dea2cab92549a253d7a6b4fa0cce178b7" - integrity sha512-xM5Ua5p7ttG/oEaDy2zk35FP2O2I9qD2N0DOrjCDUVDRC06FNDG+/CvF4qX9+i8DWOI65xch5vAhSQEfS2jFsA== - dependencies: - graphql-language-service-interface "^2.9.0" - graphql-language-service-parser "^1.10.0" - graphql-language-service-types "^1.8.2" - graphql-language-service-utils "^2.5.3" + graphql-language-service-interface "^2.10.1" + graphql-language-service-parser "^1.10.4" + graphql-language-service-types "^1.8.7" + graphql-language-service-utils "^2.7.1" + picomatch "^2.3.0" graphql-request@^3.3.0: version "3.4.0" @@ -15958,6 +16312,11 @@ graphql-request@^3.3.0: extract-files "^9.0.0" form-data "^3.0.0" +graphql-sse@^1.0.1: + version "1.0.6" + resolved "https://registry.npmjs.org/graphql-sse/-/graphql-sse-1.0.6.tgz#4f98e0a06f2020542ed054399116108491263224" + integrity sha512-y2mVBN2KwNrzxX2KBncQ6kzc6JWvecxuBernrl0j65hsr6MAS3+Yn8PTFSOgRmtolxugepxveyZVQEuaNEbw3w== + graphql-subscriptions@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" @@ -16007,15 +16366,10 @@ graphql-ws@^4.4.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.7.0.tgz#b323fbf35a3736eed85dac24c0054d6d10c93e62" integrity sha512-Md8SsmC9ZlsogFPd3Ot8HbIAAqsHh8Xoq7j4AmcIat1Bh6k91tjVyQvA0Au1/BolXSYq+RDvib6rATU2Hcf1Xw== -graphql-ws@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz#5cfd8bb490b35e86583d8322f5d5d099c26e365c" - integrity sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag== - -graphql@15.5.0: - version "15.5.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +graphql-ws@^5.4.1: + version "5.5.5" + resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.5.5.tgz#f375486d3f196e2a2527b503644693ae3a8670a9" + integrity sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw== graphql@^15.3.0: version "15.5.1" @@ -16027,6 +16381,11 @@ graphql@^15.5.1: resolved "https://registry.npmjs.org/graphql/-/graphql-15.6.1.tgz#9125bdf057553525da251e19e96dab3d3855ddfc" integrity sha512-3i5lu0z6dRvJ48QP9kFxBkJ7h4Kso7PS8eahyTFz5Jm6CvQfLtNIE8LX9N6JLnXTuwR+sIYnXzaWp6anOg0QQw== +graphql@^16.0.0: + version "16.0.1" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.0.1.tgz#93a13cd4e0e38ca8d0832e79614c8578bfd34f10" + integrity sha512-oPvCuu6dlLdiz8gZupJ47o1clgb72r1u8NDBcQYjcV6G/iEdmE11B1bBlkhXRvV0LisP/SXRFP7tT6AgaTjpzg== + grouped-queue@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/grouped-queue/-/grouped-queue-2.0.0.tgz#a2c6713f2171e45db2c300a3a9d7c119d694dac8" @@ -16107,7 +16466,7 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" -has-bigints@^1.0.0: +has-bigints@^1.0.0, has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== @@ -16134,6 +16493,13 @@ has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -16304,15 +16670,15 @@ highlight.js@^10.1.0, highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@^ resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg== -highlight.js@~10.7.0: +highlight.js@^10.7.2, highlight.js@~10.7.0: version "10.7.3" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== history@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/history/-/history-5.0.1.tgz#de35025ed08bce0db62364b47ebbf9d97b5eb06a" - integrity sha512-5qC/tFUKfVci5kzgRxZxN5Mf1CV8NmJx9ByaPX0YTLx5Vz3Svh7NYp6eA4CpDq4iA9D0C1t8BNIfvQIrUI3mVw== + version "5.1.0" + resolved "https://registry.npmjs.org/history/-/history-5.1.0.tgz#2e93c09c064194d38d52ed62afd0afc9d9b01ece" + integrity sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg== dependencies: "@babel/runtime" "^7.7.6" @@ -16579,6 +16945,15 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + http2-wrapper@^1.0.0-beta.5.2: version "1.0.0-beta.5.2" resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" @@ -16727,7 +17102,7 @@ immer@^9.0.1, immer@^9.0.6: resolved "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== -immutable@>=3.8.2, immutable@^3.8.2, immutable@^3.x.x: +immutable@^3.x.x: version "3.8.2" resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= @@ -16949,14 +17324,14 @@ internal-ip@^6.2.0: is-ip "^3.1.0" p-event "^4.2.0" -internal-slot@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" - integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== dependencies: - es-abstract "^1.17.0-next.1" + get-intrinsic "^1.1.0" has "^1.0.3" - side-channel "^1.0.2" + side-channel "^1.0.4" interpret@^1.0.0: version "1.2.0" @@ -17122,6 +17497,11 @@ is-callable@^1.1.4, is-callable@^1.2.3: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -17461,6 +17841,11 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-promise@4.0.0, is-promise@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" @@ -17491,6 +17876,14 @@ is-regex@^1.0.4, is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.1" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -17525,6 +17918,11 @@ is-set@^2.0.1: resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + is-ssh@^1.3.0: version "1.3.1" resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3" @@ -17552,6 +17950,13 @@ is-string@^1.0.4, is-string@^1.0.5: resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + is-subdir@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-subdir/-/is-subdir-1.1.1.tgz#423e66902f9c5f159b9cc4826c820df083059538" @@ -17613,6 +18018,13 @@ is-utf8@^0.2.0, is-utf8@^0.2.1: resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-weakref@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" + integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== + dependencies: + call-bind "^1.0.0" + is-whitespace-character@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" @@ -17692,6 +18104,15 @@ isobject@^4.0.0: resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== +isomorphic-dompurify@^0.13.0: + version "0.13.0" + resolved "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-0.13.0.tgz#a4dde357e8531018a85ebb2dd56c4794b6739ba3" + integrity sha512-j2/kt/PGbxvfeEm1uiRLlttZkQdn3hFe1rMr/wm3qFnMXSIw0Nmqu79k+TIoSj+KOwO98Sz9TbuNHU7ejv7IZA== + dependencies: + "@types/dompurify" "^2.1.0" + dompurify "^2.2.7" + jsdom "^16.5.2" + isomorphic-fetch@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" @@ -18294,10 +18715,10 @@ jose@^2.0.5: dependencies: "@panva/asn1.js" "^1.0.0" -joycon@^2.2.5: - version "2.2.5" - resolved "https://registry.npmjs.org/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" - integrity sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ== +joycon@^3.0.1: + version "3.1.0" + resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.0.tgz#33bb2b6b5a6849a1e251bed623bdf610f477d49f" + integrity sha512-5Y/YJghKF/IzaUXTut0JtbQyHfBShTaIsH7hHhGXEzYO07zWdWZm5hr3Q6miqhrwsRqqm3mgOnUEZdn+1aRxKQ== js-base64@^3.6.0: version "3.7.2" @@ -18427,6 +18848,39 @@ jsdom@^16.4.0: ws "^7.2.3" xml-name-validator "^3.0.0" +jsdom@^16.5.2: + version "16.7.0" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -18464,7 +18918,7 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== -json-pointer@^0.6.0: +json-pointer@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.1.tgz#3c6caa6ac139e2599f5a1659d39852154015054d" integrity sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q== @@ -18522,7 +18976,7 @@ json-schema@0.2.3: resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-schema@^0.4.0: +json-schema@0.4.0, json-schema@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== @@ -18584,6 +19038,11 @@ json5@^2.1.2, json5@^2.1.3: dependencies: minimist "^1.2.5" +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -18663,6 +19122,16 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + jss-plugin-camel-case@^10.5.1: version "10.6.0" resolved "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz#93d2cd704bf0c4af70cc40fb52d74b8a2554b170" @@ -19494,7 +19963,7 @@ lodash@4.17.15: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.0, lodash@~4.17.15, lodash@~4.17.4: +lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.0, lodash@~4.17.15, lodash@~4.17.4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -19564,7 +20033,7 @@ longest-streak@^3.0.0: resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.0.tgz#f127e2bded83caa6a35ac5f7a2f2b2f94b36f3dc" integrity sha512-XhUjWR5CFaQ03JOP+iSDS9koy8T5jfoImCZ4XprElw3BXsSk4MpVYOLw/6LTDKZhO13PlAXnB5gS4MHQTpkSOw== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -19786,17 +20255,6 @@ markdown-escapes@^1.0.0: resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== -markdown-it@^11.0.1: - version "11.0.1" - resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6" - integrity sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ== - dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdown-it@^12.2.0: version "12.2.0" resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz#091f720fd5db206f80de7a8d1f1a7035fd0d38db" @@ -19833,6 +20291,11 @@ markdown-to-jsx@^7.1.3: resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz#f00bae66c0abe7dd2d274123f84cb6bd2a2c7c6a" integrity sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w== +marked@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== + math-expression-evaluator@^1.2.14: version "1.2.22" resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e" @@ -20116,6 +20579,11 @@ memjs@^1.3.0: resolved "https://registry.npmjs.org/memjs/-/memjs-1.3.0.tgz#b7959b4ff3770e4c785463fd147f1e4fafd47a24" integrity sha512-y/V9a0auepA9Lgyr4QieK6K2FczjHucEdTpSS+hHVNmVEkYxruXhkHu8n6DSRQ4HXHEE3cc6Sf9f88WCJXGXsQ== +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -20232,11 +20700,6 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -merge@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" - integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== - meros@1.1.4, meros@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948" @@ -20812,11 +21275,6 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" -mitt@^1.1.2: - version "1.2.0" - resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" - integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -21055,7 +21513,7 @@ nan@^2.14.1, nan@^2.15.0: resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nano-css@^5.1.0, nano-css@^5.3.1: +nano-css@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.1.tgz#b709383e07ad3be61f64edffacb9d98250b87a1f" integrity sha512-ENPIyNzANQRyYVvb62ajDd7PAyIgS2LIUnT9ewih4yrXSZX4hKoUwssy8WjUH++kEOA5wUTMgNnV7ko5n34kUA== @@ -21224,6 +21682,13 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.5: + version "2.6.6" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" + integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -21675,6 +22140,11 @@ object-hash@^2.0.1, object-hash@^2.1.1: resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== +object-inspect@^1.11.0: + version "1.11.0" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + object-inspect@^1.9.0: version "1.9.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" @@ -21712,24 +22182,23 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.entries@^1.1.0, object.entries@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" - integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== +object.entries@^1.1.0, object.entries@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.5" - has "^1.0.3" + es-abstract "^1.19.1" -"object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" - integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== +"object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" + integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + es-abstract "^1.19.1" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: version "2.1.0" @@ -21739,6 +22208,14 @@ object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0 define-properties "^1.1.3" es-abstract "^1.17.0-next.1" +object.hasown@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" + integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.19.1" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -21746,15 +22223,14 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== +object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + es-abstract "^1.19.1" objectorarray@^1.0.4: version "1.0.4" @@ -21840,12 +22316,13 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -openapi-sampler@^1.0.0-beta.15: - version "1.0.0-beta.16" - resolved "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.16.tgz#7813524d5b88d222efb772ceb5a809075d6d9174" - integrity sha512-05+GvwMagTY7GxoDQoWJfmAUFlxfebciiEzqKmu4iq6+MqBEn62AMUkn0CTxyKhnUGIaR2KXjTeslxIeJwVIOw== +openapi-sampler@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.1.1.tgz#7bba7000a03cd8a4630bfbe5b3ef258990c78400" + integrity sha512-WAFsl5SPYuhQwaMTDFOcKhnEY1G1rmamrMiPmJdqwfl1lr81g63/befcsN9BNi0w5/R0L+hfcUj13PANEBeLgg== dependencies: - json-pointer "^0.6.0" + "@types/json-schema" "^7.0.7" + json-pointer "^0.6.1" openid-client@^4.1.1, openid-client@^4.2.1: version "4.9.0" @@ -22342,7 +22819,7 @@ parse5@5.1.1: resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parse5@^6.0.0: +parse5@6.0.1, parse5@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -22747,7 +23224,7 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -23878,9 +24355,9 @@ raw-loader@^4.0.2: schema-utils "^3.0.0" rc-progress@^3.0.0: - version "3.1.3" - resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.3.tgz#d77d8fd26d9d948d72c2a28b64b71a6e86df2426" - integrity sha512-Jl4fzbBExHYMoC6HBPzel0a9VmhcSXx24LVt/mdhDM90MuzoMCJjXZAlhA0V0CJi+SKjMhfBoIQ6Lla1nD4QNw== + version "3.1.4" + resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.4.tgz#66040d0fae7d8ced2b38588378eccb2864bad615" + integrity sha512-XBAif08eunHssGeIdxMXOmRQRULdHaDdIFENQ578CMb4dyewahmmfJRyab+hw4KH4XssEzzYOkAInTLS7JJG+Q== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.6" @@ -24048,11 +24525,6 @@ react-error-overlay@^6.0.9: resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== -react-fast-compare@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -24144,21 +24616,6 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-lazylog@^4.5.2, react-lazylog@^4.5.3: - version "4.5.3" - resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.3.tgz#289e24995b5599e75943556ac63f5e2c04d0001e" - integrity sha512-lyov32A/4BqihgXgtNXTHCajXSXkYHPlIEmV8RbYjHIMxCFSnmtdg4kDCI3vATz7dURtiFTvrw5yonHnrS+NNg== - dependencies: - "@mattiasbuelens/web-streams-polyfill" "^0.2.0" - fetch-readablestream "^0.2.0" - immutable "^3.8.2" - mitt "^1.1.2" - prop-types "^15.6.1" - react-string-replace "^0.4.1" - react-virtualized "^9.21.0" - text-encoding-utf-8 "^1.0.1" - whatwg-fetch "^2.0.4" - react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -24290,13 +24747,6 @@ react-sparklines@^1.7.0: dependencies: prop-types "^15.5.10" -react-string-replace@^0.4.1: - version "0.4.4" - resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-0.4.4.tgz#24006fbe0db573d5be583133df38b1a735cb4225" - integrity sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA== - dependencies: - lodash "^4.17.4" - react-syntax-highlighter@^13.5.3: version "13.5.3" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6" @@ -24370,23 +24820,6 @@ react-universal-interface@^0.6.2: resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== -react-use@^12.2.0: - version "12.13.0" - resolved "https://registry.npmjs.org/react-use/-/react-use-12.13.0.tgz#dfefd8145552841f1c2213c2e79966b505a264ba" - integrity sha512-Kh0m9ezIn9xfRycx4jdAgvsYGstGfjTBO2ecIM3+G0RqrpMxTIL5jjYraHYfUzjGBHf7dUhNYBzm5vw5LItVZA== - dependencies: - "@types/react-wait" "^0.3.0" - copy-to-clipboard "^3.1.0" - nano-css "^5.1.0" - react-fast-compare "^2.0.4" - react-wait "^0.3.0" - resize-observer-polyfill "^1.5.1" - screenfull "^5.0.0" - set-harmonic-interval "^1.0.1" - throttle-debounce "^2.0.1" - ts-easing "^0.2.0" - tslib "^1.10.0" - react-use@^17.2.4: version "17.2.4" resolved "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1" @@ -24407,24 +24840,20 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" -react-virtualized@^9.21.0: - version "9.21.2" - resolved "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" - integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== +react-virtualized-auto-sizer@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" + integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== + +react-window@^1.8.6: + version "1.8.6" + resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" + integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== dependencies: - babel-runtime "^6.26.0" - clsx "^1.0.1" - dom-helpers "^5.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" -react-wait@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b" - integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g== - -react@^16.0.0, react@^16.12.0, react@^16.13.1: +react@^16.12.0, react@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== @@ -24798,7 +25227,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: +regexp.prototype.flags@^1.2.0: version "1.3.0" resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== @@ -24806,6 +25235,14 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" +regexp.prototype.flags@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + regexpp@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" @@ -25175,6 +25612,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14 is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^2.0.0-next.3: + version "2.0.0-next.3" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" + integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + resolve@~1.17.0: version "1.17.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" @@ -25311,23 +25756,23 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-dts@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-3.0.2.tgz#2b628d88f864d271d6eaec2e4c2a60ae4e944c5c" - integrity sha512-hswlsdWu/x7k5pXzaLP6OvKRKcx8Bzprksz9i9mUe72zvt8LvqAb/AZpzs6FkLgmyRaN8B6rUQOVtzA3yEt9Yw== +rollup-plugin-dts@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-4.0.1.tgz#930cbd5aaaa64a55e895ecd6ae8234e1a5467710" + integrity sha512-DNv5F8pro/r0Hkx3JWKRtJZocDnqXfgypoajeiaNq134rYaFcEIl/oas5PogD1qexMadVijsHyVko1Chig0OOQ== dependencies: magic-string "^0.25.7" optionalDependencies: - "@babel/code-frame" "^7.12.13" + "@babel/code-frame" "^7.14.5" -rollup-plugin-esbuild@2.6.x: - version "2.6.1" - resolved "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-2.6.1.tgz#5785532940d49adf1bff5b38e9bd9089262d4e7a" - integrity sha512-hskMEQQ4Vxlyoeg1OWlFTwWHIhpNaw6q+diOT7p9pdkk34m9Mbk3aymS/JbTqLXy/AbJi22iuXrucknKpeczfg== +rollup-plugin-esbuild@^4.7.2: + version "4.7.2" + resolved "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.7.2.tgz#1a496a9f96257cdf5ed800e818932859232471f8" + integrity sha512-rBS2hTedtG+wL/yyIWQ84zju5rtfF15gkaCLN0vsWGmBdRd0UPm52meAwkmrsPQf3mB/H2o+k9Q8Ce8A66SE5A== dependencies: - "@rollup/pluginutils" "^4.1.0" - joycon "^2.2.5" - strip-json-comments "^3.1.1" + "@rollup/pluginutils" "^4.1.1" + joycon "^3.0.1" + jsonc-parser "^3.0.0" rollup-plugin-peer-deps-external@^2.2.2: version "2.2.4" @@ -25360,13 +25805,6 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@2.44.x: - version "2.44.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.44.0.tgz#8da324d1c4fd12beef9ae6e12f4068265b6d95eb" - integrity sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ== - optionalDependencies: - fsevents "~2.3.1" - rollup@^0.63.4: version "0.63.5" resolved "https://registry.npmjs.org/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db" @@ -25375,6 +25813,13 @@ rollup@^0.63.4: "@types/estree" "0.0.39" "@types/node" "*" +rollup@^2.60.2: + version "2.60.2" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.60.2.tgz#3f45ace36a9b10b4297181831ea0719922513463" + integrity sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -25513,7 +25958,7 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -saxes@^5.0.0: +saxes@^5.0.0, saxes@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== @@ -25569,7 +26014,7 @@ scoped-regex@^2.0.0: resolved "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f" integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ== -screenfull@^5.0.0, screenfull@^5.1.0: +screenfull@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.1.0.tgz#85c13c70f4ead4c1b8a935c70010dfdcd2c0e5c8" integrity sha512-dYaNuOdzr+kc6J6CFcBrzkLCfyGcMg+gWkJ8us93IQ7y1cevhQAugFsaCdMHb6lw8KV3xPzSxzH7zM1dQap9mA== @@ -25885,7 +26330,7 @@ shx@^0.3.2: minimist "^1.2.3" shelljs "^0.8.4" -side-channel@^1.0.2, side-channel@^1.0.4: +side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== @@ -26340,7 +26785,7 @@ ssh2@^1.4.0: cpu-features "0.0.2" nan "^2.15.0" -sshpk@^1.7.0: +sshpk@^1.14.1, sshpk@^1.7.0: version "1.16.1" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== @@ -26614,17 +27059,19 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -"string.prototype.matchall@^4.0.0 || ^3.0.1", string.prototype.matchall@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" - integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== +"string.prototype.matchall@^4.0.0 || ^3.0.1", string.prototype.matchall@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" + integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0" - has-symbols "^1.0.1" - internal-slot "^1.0.2" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.3.1" + side-channel "^1.0.4" string.prototype.padend@^3.0.0: version "3.1.0" @@ -26822,6 +27269,11 @@ style-loader@^3.3.1: resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== +style-mod@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01" + integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw== + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" @@ -26842,6 +27294,17 @@ stylis@^4.0.6: resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904" integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== +subscriptions-transport-ws@^0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883" + integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0 || ^6.0.0 || ^7.0.0" + subscriptions-transport-ws@^0.9.18, subscriptions-transport-ws@^0.9.19: version "0.9.19" resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz#10ca32f7e291d5ee8eb728b9c02e43c52606cdcf" @@ -27092,6 +27555,14 @@ sync-fetch@0.3.0: buffer "^5.7.0" node-fetch "^2.6.1" +sync-fetch@0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.3.1.tgz#62aa82c4b4d43afd6906bfd7b5f92056458509f0" + integrity sha512-xj5qiCDap/03kpci5a+qc5wSJjc8ZSixgG2EUmH1B8Ea2sfWclQA7eH40hiHPCtkCn6MCk4Wb+dqcXdCy2PP3g== + dependencies: + buffer "^5.7.0" + node-fetch "^2.6.1" + table@^6.0.9: version "6.7.1" resolved "https://registry.npmjs.org/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" @@ -27365,11 +27836,6 @@ testcontainers@^7.23.0: ssh-remote-port-forward "^1.0.4" tar-fs "^2.1.1" -text-encoding-utf-8@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -27409,11 +27875,6 @@ throat@^5.0.0: resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== -throttle-debounce@^2.0.1: - version "2.2.1" - resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137" - integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA== - throttle-debounce@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" @@ -27637,6 +28098,18 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -28050,6 +28523,16 @@ unbox-primitive@^1.0.0: has-symbols "^1.0.0" which-boxed-primitive "^1.0.1" +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + unbzip2-stream@1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" @@ -28075,6 +28558,11 @@ underscore@^1.12.1, underscore@^1.9.1: resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== +undici@^4.9.3: + version "4.11.0" + resolved "https://registry.npmjs.org/undici/-/undici-4.11.0.tgz#41fb4f944704d77e1c9fb472d40d2dbece64ccf2" + integrity sha512-gofXRqAdm81rzaZgPbMf98qvrNGd3ptJ26+mCcF3EXoC817p//MtL8XcDpTvHUXxdW27rAM2jvTae+KyAchorw== + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -28333,7 +28821,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unixify@1.0.0: +unixify@1.0.0, unixify@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= @@ -28508,6 +28996,13 @@ use-memo-one@^1.1.1: resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== +use-resize-observer@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-7.1.0.tgz#709ea7540fbe0a60ceae41ee2bef933d7782e4d4" + integrity sha512-6DGWOnZpjAGP/MtslGg7OunZptyueQduMi0i8DC5nVKXtJ8Bdt0wR/1tSxugFRndzYCi/jtD+SlNs5PK8ijvXQ== + dependencies: + "@juggle/resize-observer" "^3.3.1" + use@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -28673,6 +29168,11 @@ validator@^8.0.0: resolved "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA== +value-or-promise@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140" + integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== + value-or-promise@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.6.tgz#218aa4794aa2ee24dcf48a29aba4413ed584747f" @@ -28786,6 +29286,11 @@ w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" +w3c-keyname@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b" + integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw== + w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" @@ -28878,6 +29383,11 @@ web-streams-polyfill@4.0.0-beta.1: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95" integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -29078,11 +29588,6 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-fetch@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3" @@ -29093,6 +29598,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" @@ -29102,7 +29615,16 @@ whatwg-url@^8.0.0, whatwg-url@^8.4.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.1: +whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== @@ -29339,6 +29861,11 @@ ws@7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d" + integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw== + "ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1: version "7.5.0" resolved "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691"