diff --git a/.changeset/big-cougars-glow.md b/.changeset/big-cougars-glow.md new file mode 100644 index 0000000000..d1630905db --- /dev/null +++ b/.changeset/big-cougars-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Add theme switcher to sidebar of dev app. diff --git a/.changeset/bright-plants-travel.md b/.changeset/bright-plants-travel.md deleted file mode 100644 index ad8a77d17b..0000000000 --- a/.changeset/bright-plants-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. diff --git a/.changeset/cold-wolves-eat.md b/.changeset/cold-wolves-eat.md deleted file mode 100644 index fa3a2180b2..0000000000 --- a/.changeset/cold-wolves-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -feature: sentry-plugin allows passing search query for listing issues diff --git a/.changeset/dirty-bugs-care.md b/.changeset/dirty-bugs-care.md deleted file mode 100644 index 4d90a9c939..0000000000 --- a/.changeset/dirty-bugs-care.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/test-utils': patch -'@backstage/test-utils-core': patch ---- - -Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. -This should have no impact since this package is considered internal and have no usages outside core packages. - -Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. diff --git a/.changeset/dull-news-visit.md b/.changeset/dull-news-visit.md deleted file mode 100644 index e1fab41ebf..0000000000 --- a/.changeset/dull-news-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Reader will now scroll to the top of the page when navigating between pages diff --git a/.changeset/early-cobras-explode.md b/.changeset/early-cobras-explode.md new file mode 100644 index 0000000000..34b4cb0e98 --- /dev/null +++ b/.changeset/early-cobras-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Update to the right version of @backstage/errors diff --git a/.changeset/eight-months-agree.md b/.changeset/eight-months-agree.md new file mode 100644 index 0000000000..0f117d8a40 --- /dev/null +++ b/.changeset/eight-months-agree.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-gcp-projects': patch +--- + +UI updates to GCP-projects plugin + +Adds the following to the project list page: + +- pagination +- filtering +- sorting +- rows per page +- show/hide columns + +Makes breadcrumb a link back to project list for the project details and new project views. + +In project list page, updates New project button to use RouterLink instead of `href` to avoid login prompt. + +In project details view, links to project details and logs now work, clicking on these will open the project or logs in GCP in new tab. diff --git a/.changeset/forty-ligers-protect.md b/.changeset/forty-ligers-protect.md new file mode 100644 index 0000000000..e3e46fb7f3 --- /dev/null +++ b/.changeset/forty-ligers-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +remove double config dep diff --git a/.changeset/friendly-olives-drop.md b/.changeset/friendly-olives-drop.md deleted file mode 100644 index 371cf0e4db..0000000000 --- a/.changeset/friendly-olives-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md new file mode 100644 index 0000000000..ecf4c8fbdd --- /dev/null +++ b/.changeset/giant-drinks-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Migrated to using `@backstage/app-defaults`. diff --git a/.changeset/great-tomatoes-cheat.md b/.changeset/great-tomatoes-cheat.md deleted file mode 100644 index 547e89c6d0..0000000000 --- a/.changeset/great-tomatoes-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-common': patch ---- - -Add docs to the last export of cli-common diff --git a/.changeset/green-parrots-thank.md b/.changeset/green-parrots-thank.md deleted file mode 100644 index 9296a5119a..0000000000 --- a/.changeset/green-parrots-thank.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-azure-devops': patch ---- - -Azure DevOps frontend refactoring items from issue #7641 - -- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions -- Improved documentation to be easier to expand with new features in the future -- Removed Router based on feedback from maintainers -- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable diff --git a/.changeset/green-tips-eat.md b/.changeset/green-tips-eat.md deleted file mode 100644 index b8c631fbf4..0000000000 --- a/.changeset/green-tips-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Scaffolder: Enable back Template action buttons if template fails to execute diff --git a/.changeset/happy-rice-tickle.md b/.changeset/happy-rice-tickle.md new file mode 100644 index 0000000000..1e39dc2e9c --- /dev/null +++ b/.changeset/happy-rice-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added the `isDatabaseConflictError` function. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md new file mode 100644 index 0000000000..3728e10ff6 --- /dev/null +++ b/.changeset/hot-walls-fail.md @@ -0,0 +1,13 @@ +--- +'@backstage/create-app': patch +--- + +Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future. + +To migrate an existing application, add the latest version of `@backstage/app-defaults` as a dependency in `packages/app/package.json`, and make the following change to `packages/app/src/App.tsx`: + +```diff +-import { createApp, FlatRoutes } from '@backstage/core-app-api'; ++import { createApp } from '@backstage/app-defaults'; ++import { FlatRoutes } from '@backstage/core-app-api'; +``` diff --git a/.changeset/kind-islands-attack.md b/.changeset/kind-islands-attack.md deleted file mode 100644 index a549839bc7..0000000000 --- a/.changeset/kind-islands-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops-backend': patch ---- - -Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 diff --git a/.changeset/moody-snails-admire.md b/.changeset/moody-snails-admire.md new file mode 100644 index 0000000000..7622546989 --- /dev/null +++ b/.changeset/moody-snails-admire.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +Tweaked style insertion logic to make sure that JSS stylesheets always receive the highest priority. diff --git a/.changeset/popular-apples-sparkle.md b/.changeset/popular-apples-sparkle.md deleted file mode 100644 index b8437795da..0000000000 --- a/.changeset/popular-apples-sparkle.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/catalog-model': patch -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/errors': patch -'@backstage/search-common': patch -'@backstage/test-utils': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-graphql': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-scaffolder-backend-module-yeoman': patch -'@backstage/plugin-scaffolder-common': patch -'@backstage/plugin-search': patch -'@backstage/plugin-shortcuts': patch ---- - -Switch to use the json and observable types from `@backstage/types` diff --git a/.changeset/quick-walls-know.md b/.changeset/quick-walls-know.md deleted file mode 100644 index 14dd78364f..0000000000 --- a/.changeset/quick-walls-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Support optional bucketRootPath configuration parameter in S3 and GCS publishers diff --git a/.changeset/rich-pillows-cough.md b/.changeset/rich-pillows-cough.md new file mode 100644 index 0000000000..959eb69214 --- /dev/null +++ b/.changeset/rich-pillows-cough.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': patch +--- + +Make sure that server builder `start()` propagates errors (such as failing to bind to the required port) properly and doesn't resolve the promise prematurely. + +After this change, the backend logger will be able to actually capture the error as it happens: + +``` +2021-11-11T10:54:21.334Z backstage info Initializing http server +2021-11-11T10:54:21.335Z backstage error listen EADDRINUSE: address already in use :::7000 code=EADDRINUSE errno=-48 syscall=listen address=:: port=7000 +``` diff --git a/.changeset/rotten-melons-carry.md b/.changeset/rotten-melons-carry.md deleted file mode 100644 index 2f1429c399..0000000000 --- a/.changeset/rotten-melons-carry.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/config': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. diff --git a/.changeset/rude-starfishes-walk.md b/.changeset/rude-starfishes-walk.md deleted file mode 100644 index 87ec76fd0a..0000000000 --- a/.changeset/rude-starfishes-walk.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/config': patch -'@backstage/core-app-api': patch -'@backstage/core-plugin-api': patch ---- - -Replace usage of test-utils-core with test-utils diff --git a/.changeset/search-spicy-foxes-joke.md b/.changeset/search-spicy-foxes-joke.md deleted file mode 100644 index c1ed318f6d..0000000000 --- a/.changeset/search-spicy-foxes-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': patch ---- - -Handle special case when filter array has single value optimizing Lunr search behaviour. diff --git a/.changeset/sharp-meals-fetch.md b/.changeset/sharp-meals-fetch.md new file mode 100644 index 0000000000..1d3df65ba4 --- /dev/null +++ b/.changeset/sharp-meals-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed the unused `UserFlags` type. diff --git a/.changeset/sharp-moons-jog.md b/.changeset/sharp-moons-jog.md new file mode 100644 index 0000000000..d3329af08e --- /dev/null +++ b/.changeset/sharp-moons-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated the `theme` property on `AppTheme`, replacing it with `Provider`. See https://backstage.io/docs/api/deprecations#app-theme for more details. diff --git a/.changeset/slow-moles-act.md b/.changeset/slow-moles-act.md new file mode 100644 index 0000000000..ab920bfa5d --- /dev/null +++ b/.changeset/slow-moles-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Migrated to using `createSpecializedApp`. diff --git a/.changeset/tough-avocados-give.md b/.changeset/tough-avocados-give.md deleted file mode 100644 index 08b8a7ea72..0000000000 --- a/.changeset/tough-avocados-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -fix: sentry-plugin correction for allowed period values diff --git a/.changeset/twenty-cups-suffer.md b/.changeset/twenty-cups-suffer.md deleted file mode 100644 index 1dcb494ee8..0000000000 --- a/.changeset/twenty-cups-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Api cleanup, adding `@public` where necessary and tweaking some comments diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md new file mode 100644 index 0000000000..55cd3d6dc3 --- /dev/null +++ b/.changeset/twenty-swans-matter.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-app-api': patch +--- + +The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement. + +The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`. + +The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or MUI, as it allows you to avoid those dependencies completely. diff --git a/.changeset/unlucky-hotels-roll.md b/.changeset/unlucky-hotels-roll.md deleted file mode 100644 index 0ba616c2c3..0000000000 --- a/.changeset/unlucky-hotels-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` diff --git a/.changeset/wet-socks-grow.md b/.changeset/wet-socks-grow.md deleted file mode 100644 index 8ccbb7a2a2..0000000000 --- a/.changeset/wet-socks-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/errors': patch ---- - -Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 7e478953bc..c14047cd4d 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -52,6 +52,7 @@ configmaps configs const cookiecutter +cron css Datadog dataflow @@ -162,6 +163,8 @@ Monorepo monorepos msgraph msw +mutex +mutexes mysql namespace namespaced diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 73ec0c782d..36b3f75578 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -21,7 +21,7 @@ jobs: env: CI: true - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=8192 name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: @@ -40,6 +40,8 @@ jobs: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.0.2 + - name: setup chrome + uses: browser-actions/setup-chrome@latest - name: yarn install run: yarn install --frozen-lockfile diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d7754a0046..287ef91f16 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -53,6 +53,8 @@ jobs: id: yarn-cache if: steps.cache-modules.outputs.cache-hit != 'true' run: echo "::set-output name=dir::$(yarn cache dir)" + - name: setup chrome + uses: browser-actions/setup-chrome@latest - name: cache global yarn cache uses: actions/cache@v2 if: steps.cache-modules.outputs.cache-hit != 'true' @@ -72,8 +74,3 @@ jobs: run: | sudo sysctl fs.inotify.max_user_watches=524288 yarn e2e-test run - env: - POSTGRES_HOST: localhost - POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres diff --git a/.github/workflows/snyk-github-issue-sync.yml b/.github/workflows/snyk-github-issue-sync.yml new file mode 100644 index 0000000000..0c50b2c505 --- /dev/null +++ b/.github/workflows/snyk-github-issue-sync.yml @@ -0,0 +1,61 @@ +name: 'Snyk Github Issue Sync' + +on: + schedule: + - cron: '0 */4 * * *' + +jobs: + sync: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + steps: + - uses: actions/checkout@v2 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: Create Snyk report + uses: snyk/actions/node@master + continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found + with: + args: > + --yarn-workspaces + --org=backstage-dgh + --strict-out-of-sync=false + --json-file-output=snyk.json + json: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Update Github issues + run: yarn ts-node scripts/snyk-github-issue-sync.ts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ADOPTERS.md b/ADOPTERS.md index db751bfb9b..6736538fa0 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,61 +1,67 @@ -| Organization | Contact | Description of Use | -| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teamโ€™s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process ๐ŸŒ•๐Ÿš€๐Ÿง‘โ€๐Ÿš€ | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | -| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | -| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | -| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | -| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | -| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | -| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems๐Ÿš€. | -| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | -| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | -| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| Organization | Contact | Description of Use | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teamโ€™s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process ๐ŸŒ•๐Ÿš€๐Ÿง‘โ€๐Ÿš€ | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | +| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems๐Ÿš€. | +| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | +| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | +| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | +| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | +| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | +| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md new file mode 100644 index 0000000000..774c8ef03d --- /dev/null +++ b/docs/api/deprecations.md @@ -0,0 +1,56 @@ +--- +id: deprecations +title: Deprecations +description: A list of active and past deprecations +--- + +## Introduction + +This page contains extended documentation for some of the deprecations in +various parts of Backstage. It is not an exhaustive list as most deprecation +only come in the form of a changelog notice and a console warning. The +deprecations listed here are the ones that need a bit more guidance than what +fits in a console message. + +### App Theme + +`Released 2021-11-12 in @backstage/core-plugin-api v0.1.13` + +In order to provide more flexibility in what types of themes can be used and how +they are applied, the `theme` property on the `AppTheme` type is being +deprecated and replaced by a `Provider` property instead. The `Provider` +property is a React component that will be mounted at the root of the app +whenever that theme is active. This also removes the tight connection to MUI and +opens up for other type of themes, and removes the hardcoded usage of +``. + +To migrate an existing theme, remove the `theme` property and move it over to a +new `Provider` component, using `ThemeProvider` from MUI to provide the new +theme, along with ``. For example a theme that currently looks like +this: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, +}; +``` + +Would be migrated to the following: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + {children} + + ), +}; +``` diff --git a/docs/conf/writing.md b/docs/conf/writing.md index 4288d23b17..a0e29820ed 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -67,13 +67,15 @@ production build. ## Configuration Files -It is possible to have multiple configuration files, both to support different -environments, but also to define configuration that is local to specific -packages. The configuration files to load are selected using a `--config ` -flag, and it is possible to load any number of files. Paths are relative to the -working directory of the executed process, for example `package/backend`. This -means that to select a config file in the repo root when running the backend, -you would use `--config ../../my-config.yaml`. +It is possible to have multiple configuration files (bundled and/or remote), +both to support different environments, but also to define configuration that is +local to specific packages. The configuration files to load are selected using a +`--config ` flag, and it is possible to load any number of +files. Paths are relative to the working directory of the executed process, for +example `package/backend`. This means that to select a config file in the repo +root when running the backend, you would use `--config ../../my-config.yaml`, +and for config file on a config server you would use +`--config https://some.domain.io/app-config.yaml` If no `config` flags are specified, the default behavior is to load `app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root. @@ -85,7 +87,7 @@ are NOT loaded. To include them you need to explicitly include them with a flag, for example: ```shell -yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml +yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml --config https://some.domain.io/app-config.yaml ``` All loaded configuration files are merged together using the following rules: diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index aa40c1e539..27219e529a 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -109,6 +109,25 @@ const routes = ( ); ``` +### Using the Search Modal + +In `Root.tsx`, add the `SidebarSearchModal` component: + +```bash +import { SidebarSearchModal } from '@backstage/plugin-search'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + + +... +``` + +For more information about using `Root.tsx`, please see +[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315). + ## Adding Search to the Backend Add the following plugins into your backend app: diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 8f855776a0..e80e8a1842 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -172,6 +172,7 @@ This page itself can be exported as a routable extension in the plugin: ```ts export const CustomCatalogIndexPage = myPlugin.provide( createRoutableExtension({ + name: 'CustomCatalogIndexPage', component: () => import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage), mountPoint: catalogRouteRef, diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index c174b94557..7100e86ed0 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -502,6 +502,8 @@ spec: lifecycle: production owner: artist-relations-team system: artist-engagement-portal + dependsOn: + - resource:default/artists-db providesApis: - artist-api ``` diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md index 280eab083d..4cc9a6a8e6 100644 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -125,8 +125,8 @@ input schema. input: - address: '{{ json parameters.address }}' + address: ${{ parameters.address }} -- number: '{{ parameters.number }}' -+ number: ${{ parameters.number }} # this will now make sure that the type of number is a number ๐Ÿ™ +- test: '{{ parameters.test }}' ++ test: ${{ parameters.test }} # this will now make sure that the type of test is a number ๐Ÿ™ ``` ## `parseRepoUrl` is now a `filter` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 581a974455..07c4891a6c 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -156,4 +156,16 @@ return await createRouter({ }); ``` +### List of custom action packages + +Here is a list of Open Source custom actions that you can add to your Backstage +scaffolder backend: + +| Name | Package | Owner | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) | +| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) | +| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) | +| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) | + Have fun! ๐Ÿš€ diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 4f2d3bdf5d..ebb7af4398 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -27,20 +27,27 @@ You can create your own Field Extension by using the ```tsx //packages/app/scaffolder/MyCustomExtension/MyCustomExtension.tsx +import React from 'react'; import { FieldProps, FieldValidation } from '@rjsf/core'; +import FormControl from '@material-ui/core/FormControl'; import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; /* This is the actual component that will get rendered in the form */ -export const MyCustomExtension = ({ onChange, required }: FieldProps) => { +export const MyCustomExtension = ({ + onChange, + rawErrors, + required, + formData, +}: FieldProps) => { return ( - 0 && !formData} onChange={onChange} - > - ) + /> + ); }; /* @@ -63,10 +70,10 @@ export const myCustomValidation = ( ```tsx // packages/app/scaffolder/MyCustomExtension/extensions.ts -/* +/* This is where the magic happens and creates the custom field extension. - Note that if you're writing extensions part of a separate plugin, + Note that if you're writing extensions part of a separate plugin, then please use `plugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`. */ diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 0397ffa09c..0b14a2e136 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -512,7 +512,8 @@ Usage: backstage-cli config:check [options] Options: --package <name> Only load config schema that applies to the given package - --lax Do not require environment variables to be set + --lax Do not require environment variables to be set + --frontend Only validate the frontend configuration --config <path> Config files to load instead of app-config.yaml (default: []) -h, --help display help for command ``` diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 8d22c05a79..33568ac561 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -136,6 +136,7 @@ a component: ```ts export const FooPage = plugin.provide( createRoutableExtension({ + name: 'FooPage', component: () => import('./components/FooPage').then(m => m.FooPage), mountPoint: fooPageRouteRef, }), @@ -417,6 +418,7 @@ export const myPlugin = createPlugin({ export const MyPage = myPlugin.provide( createRoutableExtension({ + name: 'MyPage', component: () => import('./components/MyPage').then(m => m.MyPage), mountPoint: rootRouteRef, }), diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index f61fae0968..362ecc5d5a 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -65,6 +65,7 @@ export const examplePlugin = createPlugin({ // Each extension should also be exported from your plugin package. export const ExamplePage = examplePlugin.provide( createRoutableExtension({ + name: 'ExamplePage', // The component needs to be lazy-loaded. It's what will actually be rendered in the end. component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 83fcfbddba..9e8b67710a 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -73,6 +73,7 @@ export const examplePlugin = createPlugin({ export const ExamplePage = examplePlugin.provide( createRoutableExtension({ + name: 'ExamplePage', component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), mountPoint: rootRouteRef, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index bee9cd7180..ec5c5d8d68 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -151,7 +151,10 @@ { "type": "subcategory", "label": "GitLab", - "ids": ["integrations/gitlab/locations"] + "ids": [ + "integrations/gitlab/locations", + "integrations/gitlab/discovery" + ] }, { "type": "subcategory", @@ -253,7 +256,8 @@ "type": "subcategory", "label": "API Reference", "ids": ["reference/index"] - } + }, + "api/deprecations" ], "Tutorials": [ "tutorials/journey", @@ -275,7 +279,8 @@ "architecture-decisions/adrs-adr008", "architecture-decisions/adrs-adr009", "architecture-decisions/adrs-adr010", - "architecture-decisions/adrs-adr011" + "architecture-decisions/adrs-adr011", + "architecture-decisions/adrs-adr012" ], "FAQ": ["FAQ"] } diff --git a/mkdocs.yml b/mkdocs.yml index 2f4aa4203f..8c7114abbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,6 +100,7 @@ nav: - Org Data: 'integrations/github/org.md' - GitLab: - Locations: 'integrations/gitlab/locations.md' + - Discovery: 'integrations/gitlab/discovery.md' - Google GCS: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: @@ -163,6 +164,7 @@ nav: - API Reference: - Guides: - Utility APIs: 'api/utility-apis.md' + - Deprecations: 'api/deprecations.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' diff --git a/package.json b/package.json index 4aa6905085..ab0440fa67 100644 --- a/package.json +++ b/package.json @@ -45,23 +45,21 @@ }, "resolutions": { "**/@graphql-codegen/cli/**/ws": "^7.4.6", - "**/@roadiehq/**/@backstage/core": "*", "**/@roadiehq/**/@backstage/plugin-catalog": "*", - "**/@roadiehq/**/@backstage/catalog-model": "*", - "graphql-language-service-interface": "2.8.2", - "graphql-language-service-parser": "1.9.0" + "**/@roadiehq/**/@backstage/catalog-model": "*" }, "version": "1.0.0", "dependencies": { - "@microsoft/api-documenter": "^7.13.47", + "@microsoft/api-documenter": "^7.13.68", "@microsoft/api-extractor": "^7.18.7", "@microsoft/api-extractor-model": "^7.13.5", "@microsoft/tsdoc": "^0.13.2" }, "devDependencies": { - "@types/webpack": "^5.28.0", "@changesets/cli": "^2.14.0", + "@octokit/rest": "^18.12.0", "@spotify/prettier-config": "^11.0.0", + "@types/webpack": "^5.28.0", "command-exists": "^1.2.9", "concurrently": "^6.0.0", "eslint-plugin-notice": "^0.9.10", @@ -69,8 +67,10 @@ "husky": "^6.0.0", "lerna": "^4.0.0", "lint-staged": "^11.1.2", + "minimist": "^1.2.5", "prettier": "^2.2.1", "shx": "^0.3.2", + "ts-node": "^10.4.0", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config", diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/app-defaults/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/app-defaults/README.md b/packages/app-defaults/README.md new file mode 100644 index 0000000000..15a0bd3d24 --- /dev/null +++ b/packages/app-defaults/README.md @@ -0,0 +1,17 @@ +# @backstage/app-defaults + +This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app. + +## Installation + +Install the package via Yarn: + +```sh +cd packages/app +yarn add @backstage/app-defaults +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/app-defaults/api-report.md b/packages/app-defaults/api-report.md new file mode 100644 index 0000000000..57ea61ad8b --- /dev/null +++ b/packages/app-defaults/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/app-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AppComponents } from '@backstage/core-app-api'; +import { AppIcons } from '@backstage/core-app-api'; +import { AppOptions } from '@backstage/core-app-api'; +import { AppTheme } from '@backstage/core-plugin-api'; +import { BackstageApp } from '@backstage/core-app-api'; +import { IconComponent } from '@backstage/core-plugin-api'; + +// @public +export function createApp( + options?: Omit & OptionalAppOptions, +): BackstageApp; + +// @public +export type OptionalAppOptions = { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; + components?: Partial; +}; +``` diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json new file mode 100644 index 0000000000..9438966b3b --- /dev/null +++ b/packages/app-defaults/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/app-defaults", + "description": "Provides the default wiring of a Backstage App", + "version": "0.1.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/app-defaults" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli build --outputs types,esm", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.7.3", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "react": "^16.12.0", + "react-router-dom": "6.0.0-beta.0" + }, + "devDependencies": { + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/react": "*" + }, + "files": [ + "dist" + ] +} diff --git a/packages/app-defaults/src/createApp.test.tsx b/packages/app-defaults/src/createApp.test.tsx new file mode 100644 index 0000000000..45988a4840 --- /dev/null +++ b/packages/app-defaults/src/createApp.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { screen } from '@testing-library/react'; +import { renderWithEffects } from '@backstage/test-utils'; +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { createApp } from './createApp'; + +describe('Optional ThemeProvider', () => { + it('should render app with user-provided ThemeProvider', async () => { + const components = { + NotFoundErrorPage: () => null, + BootErrorPage: () => null, + Progress: () => null, + Router: MemoryRouter, + ErrorBoundaryFallback: () => null, + ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( +
{children}
+ ), + }; + + const App = createApp({ components }).getProvider(); + + await renderWithEffects(); + + expect(screen.getByRole('main')).toBeInTheDocument(); + }); +}); diff --git a/packages/app-defaults/src/createApp.tsx b/packages/app-defaults/src/createApp.tsx new file mode 100644 index 0000000000..d85dfa04eb --- /dev/null +++ b/packages/app-defaults/src/createApp.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { apis, components, icons, themes } from './defaults'; +import { + AppTheme, + BackstagePlugin, + IconComponent, +} from '@backstage/core-plugin-api'; +import { + AppComponents, + AppOptions, + AppIcons, + createSpecializedApp, +} from '@backstage/core-app-api'; + +/** + * Creates a new Backstage App using a default set of components, icons and themes unless + * they are explicitly provided. + * + * @public + */ +export function createApp( + options?: Omit & OptionalAppOptions, +) { + return createSpecializedApp({ + ...options, + apis: options?.apis ?? [], + bindRoutes: options?.bindRoutes, + components: { + ...components, + ...options?.components, + }, + configLoader: options?.configLoader, + defaultApis: apis, + icons: { + ...icons, + ...options?.icons, + }, + plugins: (options?.plugins as BackstagePlugin[]) ?? [], + themes: options?.themes ?? themes, + }); +} + +/** + * The set of app options that {@link createApp} will provide defaults for + * if they are not passed in explicitly. + * + * @public + */ +export type OptionalAppOptions = { + /** + * A set of icons to override the default icons with. + * + * The override is applied for each icon individually. + * + * @public + */ + icons?: Partial & { + [key in string]: IconComponent; + }; + + /** + * A set of themes that override all of the default app themes. + * + * If this option is provided none of the default themes will be used. + * + * @public + */ + themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated + + /** + * A set of components to override the default components with. + * + * The override is applied for each icon individually. + * + * @public + */ + components?: Partial; +}; diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts new file mode 100644 index 0000000000..d3c76d52bd --- /dev/null +++ b/packages/app-defaults/src/defaults/apis.ts @@ -0,0 +1,274 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AlertApiForwarder, + NoOpAnalyticsApi, + ErrorApiForwarder, + ErrorAlerter, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + BitbucketAuth, + OAuthRequestManager, + WebStorage, + UrlPatternDiscovery, + SamlAuth, + OneLoginAuth, + UnhandledErrorForwarder, + AtlassianAuth, +} from '@backstage/core-app-api'; + +import { + createApiFactory, + alertApiRef, + analyticsApiRef, + errorApiRef, + discoveryApiRef, + oauthRequestApiRef, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + configApiRef, + samlAuthApiRef, + oneloginAuthApiRef, + oidcAuthApiRef, + bitbucketAuthApiRef, + atlassianAuthApiRef, +} from '@backstage/core-plugin-api'; + +export const apis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory({ + api: alertApiRef, + deps: {}, + factory: () => new AlertApiForwarder(), + }), + createApiFactory({ + api: analyticsApiRef, + deps: {}, + factory: () => new NoOpAnalyticsApi(), + }), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory({ + api: oauthRequestApiRef, + deps: {}, + factory: () => new OAuthRequestManager(), + }), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), +]; diff --git a/packages/app-defaults/src/defaults/components.test.tsx b/packages/app-defaults/src/defaults/components.test.tsx new file mode 100644 index 0000000000..865eeed8a7 --- /dev/null +++ b/packages/app-defaults/src/defaults/components.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { OptionallyWrapInRouter } from './components'; + +describe('OptionallyWrapInRouter', () => { + it('should wrap with router if not yet inside a router', async () => { + render(Test); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + it('should not wrap with router if already inside a router', async () => { + render( + + Test + , + ); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); +}); diff --git a/packages/app-defaults/src/defaults/components.tsx b/packages/app-defaults/src/defaults/components.tsx new file mode 100644 index 0000000000..139bea4862 --- /dev/null +++ b/packages/app-defaults/src/defaults/components.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 React, { ReactNode } from 'react'; +import Button from '@material-ui/core/Button'; +import { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components'; +import { + MemoryRouter, + useInRouterContext, + BrowserRouter, +} from 'react-router-dom'; +import { + AppComponents, + BootErrorPageProps, + ErrorBoundaryFallbackProps, +} from '@backstage/core-plugin-api'; + +export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { + if (useInRouterContext()) { + return <>{children}; + } + return {children}; +} + +const DefaultNotFoundPage = () => ( + +); + +const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { + let message = ''; + if (step === 'load-config') { + message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; + } else if (step === 'load-chunk') { + message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; + } + // TODO: figure out a nicer way to handle routing on the error page, when it can be done. + return ( + + + + ); +}; + +const DefaultErrorBoundaryFallback = ({ + error, + resetError, + plugin, +}: ErrorBoundaryFallbackProps) => { + return ( + + + + ); +}; + +/** + * Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}. + * + * @public + */ +export const components: AppComponents = { + Progress, + Router: BrowserRouter, + NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, +}; diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx similarity index 58% rename from packages/core-app-api/src/app/icons.tsx rename to packages/app-defaults/src/defaults/icons.tsx index a102026b29..c4f2d2b7e4 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -18,6 +18,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import MuiApartmentIcon from '@material-ui/icons/Apartment'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; +import MuiSubjectIcon from '@material-ui/icons/Subject'; +import MuiSearchIcon from '@material-ui/icons/Search'; import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; import MuiDocsIcon from '@material-ui/icons/Description'; @@ -32,46 +35,27 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -type AppIconsKey = - | 'brokenImage' - | 'catalog' - | 'chat' - | 'dashboard' - | 'docs' - | 'email' - | 'github' - | 'group' - | 'help' - | 'kind:api' - | 'kind:component' - | 'kind:domain' - | 'kind:group' - | 'kind:location' - | 'kind:system' - | 'kind:user' - | 'user' - | 'warning'; - -export type AppIcons = { [key in AppIconsKey]: IconComponent }; - -export const defaultAppIcons: AppIcons = { - brokenImage: MuiBrokenImageIcon, +export const icons = { + brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - docs: MuiDocsIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - 'kind:api': MuiExtensionIcon, - 'kind:component': MuiMemoryIcon, - 'kind:domain': MuiApartmentIcon, - 'kind:group': MuiPeopleIcon, - 'kind:location': MuiLocationOnIcon, - 'kind:system': MuiCategoryIcon, - 'kind:user': MuiPersonIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, + catalog: MuiMenuBookIcon as IconComponent, + scaffolder: MuiCreateNewFolderIcon as IconComponent, + techdocs: MuiSubjectIcon as IconComponent, + search: MuiSearchIcon as IconComponent, + chat: MuiChatIcon as IconComponent, + dashboard: MuiDashboardIcon as IconComponent, + docs: MuiDocsIcon as IconComponent, + email: MuiEmailIcon as IconComponent, + github: MuiGitHubIcon as IconComponent, + group: MuiPeopleIcon as IconComponent, + help: MuiHelpIcon as IconComponent, + 'kind:api': MuiExtensionIcon as IconComponent, + 'kind:component': MuiMemoryIcon as IconComponent, + 'kind:domain': MuiApartmentIcon as IconComponent, + 'kind:group': MuiPeopleIcon as IconComponent, + 'kind:location': MuiLocationOnIcon as IconComponent, + 'kind:system': MuiCategoryIcon as IconComponent, + 'kind:user': MuiPersonIcon as IconComponent, + user: MuiPersonIcon as IconComponent, + warning: MuiWarningIcon as IconComponent, }; diff --git a/packages/app-defaults/src/defaults/index.ts b/packages/app-defaults/src/defaults/index.ts new file mode 100644 index 0000000000..d9ff18bc8c --- /dev/null +++ b/packages/app-defaults/src/defaults/index.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 { apis } from './apis'; +export { components } from './components'; +export { icons } from './icons'; +export { themes } from './themes'; diff --git a/packages/app-defaults/src/defaults/themes.tsx b/packages/app-defaults/src/defaults/themes.tsx new file mode 100644 index 0000000000..90a4a0f47f --- /dev/null +++ b/packages/app-defaults/src/defaults/themes.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { darkTheme, lightTheme } from '@backstage/theme'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import LightIcon from '@material-ui/icons/WbSunny'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { AppTheme } from '@backstage/core-plugin-api'; + +export const themes: AppTheme[] = [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, +]; diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts new file mode 100644 index 0000000000..16a6a693a5 --- /dev/null +++ b/packages/app-defaults/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides the default wiring of a Backstage App + * + * @packageDocumentation + */ + +export { createApp } from './createApp'; +export type { OptionalAppOptions } from './createApp'; diff --git a/packages/app-defaults/src/setupTests.ts b/packages/app-defaults/src/setupTests.ts new file mode 100644 index 0000000000..963c0f188b --- /dev/null +++ b/packages/app-defaults/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/packages/app/package.json b/packages/app/package.json index 833e146d45..9a1fb108f6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/catalog-model": "^0.9.5", "@backstage/cli": "^0.8.0", "@backstage/core-app-api": "^0.1.18", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 109ae7922e..401665b62e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -26,7 +26,8 @@ import { RELATION_PART_OF, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; import { AlertDisplay, OAuthRequestDialog, @@ -93,7 +94,6 @@ const app = createApp({ // Custom icon example alert: AlarmIcon, }, - components: { SignInPage: props => { return ( diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 7a666a020a..f365310595 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -29,7 +29,7 @@ import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { GraphiQLIcon } from '@backstage/plugin-graphiql'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearch } from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; import { Shortcuts } from '@backstage/plugin-shortcuts'; import { Sidebar, @@ -79,7 +79,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + {/* Global nav, not org-specific */} diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 131b160a36..f3464917aa 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/backend-common +## 0.9.9 + +### Patch Changes + +- 8c4cad0bf2: AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response. +- 0611f3b3e2: Reading app config from a remote server +- Updated dependencies + - @backstage/config-loader@0.7.2 + +## 0.9.8 + +### Patch Changes + +- 96cfa561eb: Adjusted some API exports +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 1be8d2abdb: Any set configurations which have been tagged with a visibility 'secret', are now redacted from log lines. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/config-loader@0.7.1 + ## 0.9.7 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2336a57ec3..7242c32a84 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -211,6 +211,12 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; +// @public +export type FromReadableArrayOptions = Array<{ + data: Readable; + path: string; +}>; + // @public (undocumented) export function getRootLogger(): winston.Logger; @@ -368,6 +374,9 @@ export class GitlabUrlReader implements UrlReader { export { isChildPath }; +// @public +export function isDatabaseConflictError(e: unknown): boolean; + // @public export function loadBackendConfig(options: { logger: Logger_2; @@ -419,15 +428,13 @@ export type ReadTreeResponse = { etag: string; }; -// @public (undocumented) +// @public export type ReadTreeResponseDirOptions = { targetDir?: string; }; // @public (undocumented) export interface ReadTreeResponseFactory { - // Warning: (ae-forgotten-export) The symbol "FromReadableArrayOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) fromReadableArray( options: FromReadableArrayOptions, @@ -442,7 +449,7 @@ export interface ReadTreeResponseFactory { ): Promise; } -// @public (undocumented) +// @public export type ReadTreeResponseFactoryOptions = { stream: Readable; subpath?: string; @@ -580,7 +587,7 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; -// @public (undocumented) +// @public export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 60824f0c40..03ae1fbe62 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.7", + "version": "0.9.9", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,13 +29,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.7.0", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.7", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/config-loader": "^0.7.2", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", + "@lerna/project": "^4.0.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", @@ -78,8 +79,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", @@ -99,7 +100,7 @@ "http-errors": "^1.7.3", "jest": "^26.0.1", "mock-fs": "^5.1.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", "supertest": "^6.1.3" diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index fa49d5f015..3fd5e1b44e 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,9 +18,41 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; +import { + loadConfigSchema, + loadConfig, + ConfigSchema, + ConfigTarget, +} from '@backstage/config-loader'; +import { AppConfig, Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; -import { loadConfig } from '@backstage/config-loader'; + +import { isValidUrl } from './urls'; + +import { setRootLoggerRedactionList } from './logging/rootLogger'; + +// Fetch the schema and get all the secrets to pass to the rootLogger for redaction +const updateRedactionList = ( + schema: ConfigSchema, + configs: AppConfig[], + logger: Logger, +) => { + const secretAppConfigs = schema.process(configs, { visibility: ['secret'] }); + const secretConfig = ConfigReader.fromConfigs(secretAppConfigs); + const values = new Set(); + const data = secretConfig.get(); + + JSON.parse( + JSON.stringify(data), + (_, v) => typeof v === 'string' && values.add(v), + ); + + logger.info( + `${values.size} secrets found in the config which will be redacted`, + ); + + setRootLoggerRedactionList(Array.from(values)); +}; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -149,16 +181,29 @@ export async function loadBackendConfig(options: { argv: string[]; }): Promise { const args = parseArgs(options.argv); - const configPaths: string[] = [args.config ?? []].flat(); - const config = new ObservableConfigProxy(options.logger); + const configTargets: ConfigTarget[] = [args.config ?? []] + .flat() + .map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) })); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + // TODO(hhogg): This is fetching _all_ of the packages of the monorepo + // in order to find the secrets for redactions, however we only care about + // the backend ones, we need to find a way to exclude the frontend packages. + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); + const packages = await project.getPackages(); + const schema = await loadConfigSchema({ + dependencies: packages.map((p: any) => p.name), + }); + + const config = new ObservableConfigProxy(options.logger); const configs = await loadConfig({ configRoot: paths.targetRoot, - configPaths: configPaths.map(opt => resolvePath(opt)), + configPaths: [], + configTargets: configTargets, watch: { onChange(newConfigs) { options.logger.info( @@ -187,5 +232,9 @@ export async function loadBackendConfig(options: { config.setConfig(ConfigReader.fromConfigs(configs)); + // Subscribe to config changes and update the redaction list for logging + updateRedactionList(schema, configs, options.logger); + config.subscribe(() => updateRedactionList(schema, configs, options.logger)); + return config; } diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 6a2b6cff60..cf5e801d66 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Knex } from 'knex'; import { omit } from 'lodash'; import { Config, ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { - ensureDatabaseExists, createNameOverride, + ensureDatabaseExists, normalizeConnection, createSchemaOverride, ensureSchemaExists, @@ -182,7 +183,7 @@ export class DatabaseManager { ); return { - // include base connection if client type has not been overriden + // include base connection if client type has not been overridden ...(overridden ? {} : baseConnection), ...connection, }; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 7fcb8bf930..2a820f7367 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -28,3 +28,4 @@ export { } from './connection'; export type { PluginDatabaseManager } from './types'; +export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/database/util.ts b/packages/backend-common/src/database/util.ts new file mode 100644 index 0000000000..607554f294 --- /dev/null +++ b/packages/backend-common/src/database/util.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tries to deduce whether a thrown error is a database conflict. + * + * @public + * @param e - A thrown error + * @returns True if the error looks like it was a conflict error thrown by a + * known database engine + */ +export function isDatabaseConflictError(e: unknown) { + const message = (e as any)?.message; + + return ( + typeof message === 'string' && + (/SQLITE_CONSTRAINT: UNIQUE/.test(message) || + /unique constraint/.test(message)) + ); +} diff --git a/packages/backend-common/src/logging/index.ts b/packages/backend-common/src/logging/index.ts index 71e9618f0c..f50114a9ae 100644 --- a/packages/backend-common/src/logging/index.ts +++ b/packages/backend-common/src/logging/index.ts @@ -15,5 +15,5 @@ */ export * from './formats'; -export * from './rootLogger'; +export { createRootLogger, getRootLogger, setRootLogger } from './rootLogger'; export * from './voidLogger'; diff --git a/packages/backend-common/src/logging/rootLogger.test.ts b/packages/backend-common/src/logging/rootLogger.test.ts index 50cf867c85..192decb653 100644 --- a/packages/backend-common/src/logging/rootLogger.test.ts +++ b/packages/backend-common/src/logging/rootLogger.test.ts @@ -15,7 +15,12 @@ */ import * as winston from 'winston'; -import { createRootLogger, getRootLogger, setRootLogger } from './rootLogger'; +import { + createRootLogger, + getRootLogger, + setRootLogger, + setRootLoggerRedactionList, +} from './rootLogger'; describe('rootLogger', () => { it('can replace the default logger', () => { @@ -30,7 +35,20 @@ describe('rootLogger', () => { ); }); - describe('createRootLoger', () => { + it('redacts given secrets', () => { + const logger = createRootLogger(); + jest.spyOn(logger, 'write'); + setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'SECRET.3']); + logger.info('Logging SECRET-1 and SECRET_2 and SECRET.3'); + + expect(logger.write).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Logging [REDACTED] and [REDACTED] and [REDACTED]', + }), + ); + }); + + describe('createRootLogger', () => { it('creates a new logger', () => { const oldLogger = getRootLogger(); const newLogger = createRootLogger(); diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index d06037863c..b05763a42c 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -18,8 +18,10 @@ import { merge } from 'lodash'; import * as winston from 'winston'; import { LoggerOptions } from 'winston'; import { coloredFormat } from './formats'; +import { escapeRegExp } from '../util/escapeRegExp'; let rootLogger: winston.Logger; +let redactionRegExp: RegExp; /** @public */ export function getRootLogger(): winston.Logger { @@ -31,6 +33,30 @@ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } +export function setRootLoggerRedactionList(redactionList: string[]) { + if (redactionList.length) { + redactionRegExp = new RegExp( + `(${redactionList.map(escapeRegExp).join('|')})`, + 'g', + ); + } +} + +/** + * A winston formatting function that finds occurrences of filteredKeys + * and replaces them with the corresponding identifier. + */ +function redactLogLine(info: winston.Logform.TransformableInfo) { + // TODO(hhogg): The logger is created before the config is loaded, + // because the logger is needed in the config loader. There is a risk of + // a secret being logged out during the config loading stage. + if (redactionRegExp) { + info.message = info.message.replace(redactionRegExp, '[REDACTED]'); + } + + return info; +} + /** @public */ export function createRootLogger( options: winston.LoggerOptions = {}, @@ -41,6 +67,7 @@ export function createRootLogger( { level: env.LOG_LEVEL || 'info', format: winston.format.combine( + winston.format(redactLogLine)(), env.NODE_ENV === 'production' ? winston.format.json() : coloredFormat, ), defaultMeta: { diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 301543e8c9..a3ae8b647f 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -27,6 +27,7 @@ import { UrlReaderPredicateTuple } from './types'; import AWSMock from 'aws-sdk-mock'; import aws from 'aws-sdk'; import path from 'path'; +import { NotModifiedError } from '@backstage/errors'; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), @@ -131,29 +132,37 @@ describe('AwsS3UrlReader', () => { }); describe('read', () => { - AWSMock.setSDKInstance(aws); - AWSMock.mock( - 'S3', - 'getObject', - Buffer.from( - require('fs').readFileSync( - path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), + let awsS3UrlReader: AwsS3UrlReader; + + beforeAll(() => { + AWSMock.setSDKInstance(aws); + AWSMock.mock( + 'S3', + 'getObject', + Buffer.from( + require('fs').readFileSync( + path.resolve( + __dirname, + '__fixtures__/awsS3/awsS3-mock-object.yaml', + ), + ), ), - ), - ); - const s3 = new aws.S3(); - const awsS3UrlReader = new AwsS3UrlReader( - new AwsS3Integration( - readAwsS3IntegrationConfig( - new ConfigReader({ - host: 'amazonaws.com', - accessKeyId: 'fake-access-key', - secretAccessKey: 'fake-secret-key', - }), + ); + + const s3 = new aws.S3(); + awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: 'amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), ), - ), - { s3, treeResponseFactory }, - ); + { s3, treeResponseFactory }, + ); + }); it('returns contents of an object in a bucket', async () => { const response = await awsS3UrlReader.read( @@ -176,32 +185,39 @@ describe('AwsS3UrlReader', () => { }); describe('readUrl', () => { - AWSMock.setSDKInstance(aws); + let awsS3UrlReader: AwsS3UrlReader; - AWSMock.mock( - 'S3', - 'getObject', - Buffer.from( - require('fs').readFileSync( - path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), + beforeAll(() => { + AWSMock.setSDKInstance(aws); + + AWSMock.mock( + 'S3', + 'getObject', + Buffer.from( + require('fs').readFileSync( + path.resolve( + __dirname, + '__fixtures__/awsS3/awsS3-mock-object.yaml', + ), + ), ), - ), - ); + ); - const s3 = new aws.S3(); + const s3 = new aws.S3(); - const awsS3UrlReader = new AwsS3UrlReader( - new AwsS3Integration( - readAwsS3IntegrationConfig( - new ConfigReader({ - host: 'amazonaws.com', - accessKeyId: 'fake-access-key', - secretAccessKey: 'fake-secret-key', - }), + awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: 'amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), ), - ), - { s3, treeResponseFactory }, - ); + { s3, treeResponseFactory }, + ); + }); it('returns contents of an object in a bucket', async () => { const response = await awsS3UrlReader.readUrl( @@ -223,40 +239,89 @@ describe('AwsS3UrlReader', () => { ); }); }); + + describe('readUrl with etag', () => { + let awsS3UrlReader: AwsS3UrlReader; + + beforeAll(() => { + AWSMock.setSDKInstance(aws); + + AWSMock.mock('S3', 'getObject', (_, callback) => { + callback({ statusCode: 304 }, null); + }); + + const s3 = new aws.S3(); + + awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: 'amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), + ), + { s3, treeResponseFactory }, + ); + }); + + it('returns contents of an object in a bucket', async () => { + await expect( + awsS3UrlReader.readUrl( + 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', + { + etag: 'abc123', + }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); + describe('readTree', () => { - const object: aws.S3.Types.Object = { - Key: 'awsS3-mock-object.yaml', - }; - const objectList: aws.S3.ObjectList = [object]; - const output: aws.S3.Types.ListObjectsV2Output = { - Contents: objectList, - }; - AWSMock.setSDKInstance(aws); - AWSMock.mock('S3', 'listObjectsV2', output); + let awsS3UrlReader: AwsS3UrlReader; - AWSMock.mock( - 'S3', - 'getObject', - Buffer.from( - require('fs').readFileSync( - path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), - ), - ), - ); + beforeAll(() => { + const object: aws.S3.Types.Object = { + Key: 'awsS3-mock-object.yaml', + }; - const s3 = new aws.S3(); - const awsS3UrlReader = new AwsS3UrlReader( - new AwsS3Integration( - readAwsS3IntegrationConfig( - new ConfigReader({ - host: '.amazonaws.com', - accessKeyId: 'fake-access-key', - secretAccessKey: 'fake-secret-key', - }), + const objectList: aws.S3.ObjectList = [object]; + const output: aws.S3.Types.ListObjectsV2Output = { + Contents: objectList, + }; + + AWSMock.setSDKInstance(aws); + AWSMock.mock('S3', 'listObjectsV2', output); + + AWSMock.mock( + 'S3', + 'getObject', + Buffer.from( + require('fs').readFileSync( + path.resolve( + __dirname, + '__fixtures__/awsS3/awsS3-mock-object.yaml', + ), + ), ), - ), - { s3, treeResponseFactory }, - ); + ); + + const s3 = new aws.S3(); + awsS3UrlReader = new AwsS3UrlReader( + new AwsS3Integration( + readAwsS3IntegrationConfig( + new ConfigReader({ + host: '.amazonaws.com', + accessKeyId: 'fake-access-key', + secretAccessKey: 'fake-secret-key', + }), + ), + ), + { s3, treeResponseFactory }, + ); + }); + it('returns contents of an object in a bucket', async () => { const response = await awsS3UrlReader.readTree( 'https://test.s3.us-east-2.amazonaws.com', diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts index 7fd0f6ba19..05a14e26ec 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -27,7 +27,7 @@ import { } from './types'; import getRawBody from 'raw-body'; import { AwsS3Integration, ScmIntegrations } from '@backstage/integration'; -import { ForwardedError } from '@backstage/errors'; +import { ForwardedError, NotModifiedError } from '@backstage/errors'; import { ListObjectsV2Output, ObjectList } from 'aws-sdk/clients/s3'; const parseURL = ( @@ -163,6 +163,10 @@ export class AwsS3UrlReader implements UrlReader { etag: etag, }; } catch (e) { + if (e.statusCode === 304) { + throw new NotModifiedError(); + } + throw new ForwardedError('Could not retrieve file from S3', e); } } diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 59968a18e9..23de8389ad 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -19,7 +19,7 @@ import { AzureIntegration, readAzureIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -51,7 +51,7 @@ describe('AzureUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 5bbf3725ea..ea8e4626be 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -19,7 +19,7 @@ import { BitbucketIntegration, readBitbucketIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -72,7 +72,7 @@ describe('BitbucketUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('readUrl', () => { worker.use( diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index e8c16c5f0a..124abb799c 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; @@ -28,7 +28,7 @@ const fetchUrlReader = new FetchUrlReader(); describe('FetchUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index c76364976d..ff73718d50 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -20,7 +20,7 @@ import { GitHubIntegration, readGitHubIntegrationConfig, } from '@backstage/integration'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -73,7 +73,7 @@ const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('GithubUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockFs({ diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 331150bde3..c8a7e17ae3 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; @@ -77,7 +77,7 @@ describe('GitlabUrlReader', () => { }); const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 77096795ba..2c3394e4fc 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -20,6 +20,7 @@ export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export { AwsS3UrlReader } from './AwsS3UrlReader'; export type { + FromReadableArrayOptions, ReaderFactory, ReadTreeOptions, ReadTreeResponse, diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index ff799f548a..7e0904f18b 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -24,24 +24,41 @@ import { Config } from '@backstage/config'; * @public */ export type UrlReader = { - /* Used to read a single file and return its content. */ + /** + * Reads a single file and return its content. + */ read(url: string): Promise; /** - * A replacement for the read method that supports options and complex responses. + * Reads a single file and return its content. * - * Use this whenever it is available, as the read method will be deprecated and - * eventually removed in the future. + * @remarks + * + * This is a replacement for the read method that supports options and + * complex responses. + * + * Use this whenever it is available, as the read method will be + * deprecated and eventually removed in a future release. */ readUrl?(url: string, options?: ReadUrlOptions): Promise; - /* Used to read a file tree and download as a directory. */ + /** + * Reads a full or partial file tree. + */ readTree(url: string, options?: ReadTreeOptions): Promise; - /* Used to search a file in a tree using a glob pattern. */ + + /** + * Searches for a file in a tree using a glob pattern. + */ search(url: string, options?: SearchOptions): Promise; }; -/** @public */ +/** + * A predicate that decides whether a specific {@link UrlReader} can handle a + * given URL. + * + * @public + */ export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; @@ -49,7 +66,7 @@ export type UrlReaderPredicateTuple = { /** * A factory function that can read config to construct zero or more - * UrlReaders along with a predicate for when it should be used. + * {@link UrlReader}s along with a predicate for when it should be used. * * @public */ @@ -66,21 +83,28 @@ export type ReaderFactory = (options: { */ export type ReadUrlOptions = { /** - * An etag can be provided to check whether readUrl's response has changed from a previous execution. + * An ETag which can be provided to check whether a + * {@link UrlReader.readUrl} response has changed from a previous execution. * - * In the readUrl() response, an etag is returned along with the data. The etag is a unique identifer - * of the data, usually the commit SHA or etag from the target. + * @remarks * - * When an etag is given in ReadUrlOptions, readUrl will first compare the etag against the etag - * on the target. If they match, readUrl will throw a NotModifiedError indicating that the readUrl - * response will not differ from the previous response which included this particular etag. If they - * do not match, readUrl will return the rest of ReadUrlResponse along with a new etag. + * In the {@link UrlReader.readUrl} response, an ETag is returned along with + * the data. The ETag is a unique identifier of the data, usually the commit + * SHA or ETag from the target. + * + * When an ETag is given in ReadUrlOptions, {@link UrlReader.readUrl} will + * first compare the ETag against the ETag of the target. If they match, + * {@link UrlReader.readUrl} will throw a + * {@link @backstage/errors#NotModifiedError} indicating that the response + * will not differ from the previous response which included this particular + * ETag. If they do not match, {@link UrlReader.readUrl} will return the rest + * of the response along with a new ETag. */ etag?: string; }; /** - * A response object for readUrl operations. + * A response object for {@link UrlReader.readUrl} operations. * * @public */ @@ -92,13 +116,16 @@ export type ReadUrlResponse = { /** * Etag returned by content provider. + * + * @remarks + * * Can be used to compare and cache responses when doing subsequent calls. */ etag?: string; }; /** - * An options object for readTree operations. + * An options object for {@link UrlReader.readTree} operations. * * @public */ @@ -106,6 +133,8 @@ export type ReadTreeOptions = { /** * A filter that can be used to select which files should be included. * + * @remarks + * * The path passed to the filter function is the relative path from the URL * that the file tree is fetched from, without any leading '/'. * @@ -113,56 +142,82 @@ export type ReadTreeOptions = { * at https://github.com/my/repo/blob/master/my-dir/my-subdir/my-file.txt will * be represented as my-subdir/my-file.txt * - * If no filter is provided all files are extracted. + * If no filter is provided, all files are extracted. */ filter?(path: string, info?: { size: number }): boolean; /** - * An etag can be provided to check whether readTree's response has changed from a previous execution. + * An ETag which can be provided to check whether a + * {@link UrlReader.readTree} response has changed from a previous execution. * - * In the readTree() response, an etag is returned along with the tree blob. The etag is a unique identifer - * of the tree blob, usually the commit SHA or etag from the target. + * @remarks * - * When an etag is given in ReadTreeOptions, readTree will first compare the etag against the etag - * on the target branch. If they match, readTree will throw a NotModifiedError indicating that the readTree - * response will not differ from the previous response which included this particular etag. If they - * do not match, readTree will return the rest of ReadTreeResponse along with a new etag. + * In the {@link UrlReader.readTree} response, an ETag is returned along with + * the tree blob. The ETag is a unique identifier of the tree blob, usually + * the commit SHA or ETag from the target. + * + * When an ETag is given as a request option, {@link UrlReader.readTree} will + * first compare the ETag against the ETag on the target branch. If they + * match, {@link UrlReader.readTree} will throw a + * {@link @backstage/errors#NotModifiedError} indicating that the response + * will not differ from the previous response which included this particular + * ETag. If they do not match, {@link UrlReader.readTree} will return the + * rest of the response along with a new ETag. */ etag?: string; }; -/** @public */ +/** + * Options that control {@link ReadTreeResponse.dir} execution. + * + * @public + */ export type ReadTreeResponseDirOptions = { - /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ + /** + * The directory to write files to. + * + * @remarks + * + * Defaults to the OS tmpdir, or `backend.workingDirectory` if set in config. + */ targetDir?: string; }; /** - * A response object for readTree operations. + * A response object for {@link UrlReader.readTree} operations. * * @public */ export type ReadTreeResponse = { /** - * files() returns an array of all the files inside the tree and corresponding functions to read their content. + * Returns an array of all the files inside the tree, and corresponding + * functions to read their content. */ files(): Promise; + + /** + * Returns the tree contents as a binary archive, using a stream. + */ archive(): Promise; /** - * dir() extracts the tree response into a directory and returns the path of the directory. + * Extracts the tree response into a directory and returns the path of the + * directory. */ dir(options?: ReadTreeResponseDirOptions): Promise; /** * Etag returned by content provider. + * + * @remarks + * * Can be used to compare and cache responses when doing subsequent calls. */ etag: string; }; /** - * Represents a single file in a readTree response. + * Represents a single file in a {@link UrlReader.readTree} response. * * @public */ @@ -171,7 +226,11 @@ export type ReadTreeResponseFile = { content(): Promise; }; -/** @public */ +/** + * Options that control execution of {@link ReadTreeResponseFactory} methods. + * + * @public + */ export type ReadTreeResponseFactoryOptions = { // A binary stream of a tar archive. stream: Readable; @@ -183,11 +242,21 @@ export type ReadTreeResponseFactoryOptions = { // Filter passed on from the ReadTreeOptions filter?: (path: string, info?: { size: number }) => boolean; }; -/** @public */ + +/** + * Options that control {@link ReadTreeResponseFactory.fromReadableArray} + * execution. + * + * @public + */ export type FromReadableArrayOptions = Array<{ - // Data in the form of a readable + /** + * The raw data itself. + */ data: Readable; - // A string containing the filepath of the data + /** + * The filepath of the data. + */ path: string; }>; @@ -213,7 +282,7 @@ export type SearchOptions = { /** * An etag can be provided to check whether the search response has changed from a previous execution. * - * In the search() response, an etag is returned along with the files. The etag is a unique identifer + * In the search() response, an etag is returned along with the files. The etag is a unique identifier * of the current tree, usually the commit SHA or etag from the target. * * When an etag is given in SearchOptions, search will first compare the etag against the etag @@ -236,7 +305,7 @@ export type SearchResponse = { files: SearchResponseFile[]; /** - * A unique identifer of the current remote tree, usually the commit SHA or etag from the target. + * A unique identifier of the current remote tree, usually the commit SHA or etag from the target. */ etag: string; }; diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index ed16c9c9a7..885baba54b 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -174,27 +174,27 @@ export class ServiceBuilderImpl implements ServiceBuilder { const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) : createHttpServer(app, logger); + const stoppableServer = stoppable(server, 0); + + useHotCleanup(this.module, () => + stoppableServer.stop((e: any) => { + if (e) console.error(e); + }), + ); return new Promise((resolve, reject) => { - app.on('error', e => { - logger.error(`Failed to start up on port ${port}, ${e}`); + function handleStartupError(e: unknown) { + server.close(); reject(e); + } + + server.on('error', handleStartupError); + + server.listen(port, host, () => { + server.off('error', handleStartupError); + logger.info(`Listening on ${host}:${port}`); + resolve(stoppableServer); }); - - const stoppableServer = stoppable( - server.listen(port, host, () => { - logger.info(`Listening on ${host}:${port}`); - }), - 0, - ); - - useHotCleanup(this.module, () => - stoppableServer.stop((e: any) => { - if (e) console.error(e); - }), - ); - - resolve(stoppableServer); }); } diff --git a/packages/backend-common/src/urls.test.ts b/packages/backend-common/src/urls.test.ts new file mode 100644 index 0000000000..c2a67fb849 --- /dev/null +++ b/packages/backend-common/src/urls.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isValidUrl } from './urls'; + +describe('isValidUrl', () => { + it('should return true for url', () => { + const validUrl = isValidUrl('http://some.valid.url'); + expect(validUrl).toBe(true); + }); + + it('should return false for absolute path', () => { + const validUrl = isValidUrl('/some/absolute/path'); + expect(validUrl).toBe(false); + }); + + it('should return false for relative path', () => { + const validUrl = isValidUrl('../some/relative/path'); + expect(validUrl).toBe(false); + }); +}); diff --git a/packages/backend-common/src/urls.ts b/packages/backend-common/src/urls.ts new file mode 100644 index 0000000000..848cea25d9 --- /dev/null +++ b/packages/backend-common/src/urls.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. + */ + +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } +} diff --git a/packages/backend-common/src/util/escapeRegExp.test.ts b/packages/backend-common/src/util/escapeRegExp.test.ts new file mode 100644 index 0000000000..13c6ae4a5a --- /dev/null +++ b/packages/backend-common/src/util/escapeRegExp.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { escapeRegExp } from './escapeRegExp'; + +describe('escapeRegExp', () => { + test('does not escape non-regex characters', () => { + expect(escapeRegExp('Backstage Backstage')).toBe('Backstage Backstage'); + }); + + test('all the characters', () => { + expect(escapeRegExp('^$\\.*+?()[]{}|')).toBe( + '\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|', + ); + }); + + test('character: ^', () => { + expect(escapeRegExp('^')).toBe('\\^'); + }); + + test('character: $', () => { + expect(escapeRegExp('$')).toBe('\\$'); + }); + + test('character: \\', () => { + expect(escapeRegExp('\\')).toBe('\\\\'); + }); + + test('character: .', () => { + expect(escapeRegExp('.')).toBe('\\.'); + }); + + test('character: *', () => { + expect(escapeRegExp('*')).toBe('\\*'); + }); + + test('character: +', () => { + expect(escapeRegExp('+')).toBe('\\+'); + }); + + test('character: ?', () => { + expect(escapeRegExp('?')).toBe('\\?'); + }); + + test('character: (', () => { + expect(escapeRegExp('(')).toBe('\\('); + }); + + test('character: )', () => { + expect(escapeRegExp(')')).toBe('\\)'); + }); + + test('character: [', () => { + expect(escapeRegExp('[')).toBe('\\['); + }); + + test('character: ]', () => { + expect(escapeRegExp(']')).toBe('\\]'); + }); + + test('character: {', () => { + expect(escapeRegExp('{')).toBe('\\{'); + }); + + test('character: }', () => { + expect(escapeRegExp('}')).toBe('\\}'); + }); + + test('character: |', () => { + expect(escapeRegExp('|')).toBe('\\|'); + }); +}); diff --git a/plugins/azure-devops/src/api/types.ts b/packages/backend-common/src/util/escapeRegExp.ts similarity index 66% rename from plugins/azure-devops/src/api/types.ts rename to packages/backend-common/src/util/escapeRegExp.ts index 1fbc34231e..bc78967ebe 100644 --- a/plugins/azure-devops/src/api/types.ts +++ b/packages/backend-common/src/util/escapeRegExp.ts @@ -14,21 +14,11 @@ * limitations under the License. */ -import { - BuildResult, - BuildStatus, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; - -export type RepoBuild = { - id?: number; - title: string; - link?: string; - status?: BuildStatus; - result?: BuildResult; - queueTime?: Date; - source: string; -}; - -export type RepoBuildOptions = { - top?: number; +/** + * Escapes a given string to be used inside a RegExp. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ +export const escapeRegExp = (text: string) => { + return text.replace(/[.*+?^${}(\)|[\]\\]/g, '\\$&'); }; diff --git a/packages/backend-tasks/.eslintrc.js b/packages/backend-tasks/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/backend-tasks/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/backend-tasks/README.md b/packages/backend-tasks/README.md new file mode 100644 index 0000000000..d3c361c02c --- /dev/null +++ b/packages/backend-tasks/README.md @@ -0,0 +1,35 @@ +# @backstage/backend-tasks + +Common distributed task management for Backstage backends. + +## Usage + +Add the library to your backend package: + +```sh +# From your Backstage root directory +cd packages/backend +yarn add @backstage/backend-tasks +``` + +then make use of its facilities as necessary: + +```typescript +import { TaskScheduler } from '@backstage/backend-tasks'; +import { Duration } from 'luxon'; + +const scheduler = TaskScheduler.fromConfig(rootConfig).forPlugin('my-plugin'); + +await scheduler.scheduleTask({ + id: 'refresh-things', + frequency: Duration.fromObject({ minutes: 10 }), + fn: async () => { + await entityProvider.run(); + }, +}); +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md new file mode 100644 index 0000000000..eeb6412840 --- /dev/null +++ b/packages/backend-tasks/api-report.md @@ -0,0 +1,45 @@ +## API Report File for "@backstage/backend-tasks" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; +import { Config } from '@backstage/config'; +import { DatabaseManager } from '@backstage/backend-common'; +import { Duration } from 'luxon'; +import { Logger as Logger_2 } from 'winston'; + +// @public +export interface PluginTaskScheduler { + scheduleTask(task: TaskDefinition): Promise; +} + +// @public +export interface TaskDefinition { + fn: TaskFunction; + frequency: Duration; + id: string; + initialDelay?: Duration; + signal?: AbortSignal_2; + timeout: Duration; +} + +// @public +export type TaskFunction = + | ((abortSignal: AbortSignal_2) => void | Promise) + | (() => void | Promise); + +// @public +export class TaskScheduler { + constructor(databaseManager: DatabaseManager, logger: Logger_2); + forPlugin(pluginId: string): PluginTaskScheduler; + // (undocumented) + static fromConfig( + config: Config, + options?: { + databaseManager?: DatabaseManager; + logger?: Logger_2; + }, + ): TaskScheduler; +} +``` diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js new file mode 100644 index 0000000000..21d80ae24c --- /dev/null +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // + // tasks + // + await knex.schema.createTable('backstage_backend_tasks__tasks', table => { + table.comment('Tasks used for scheduling work on multiple workers'); + table + .text('id') + .primary() + .notNullable() + .comment('The unique ID of this particular task'); + table + .text('settings_json') + .notNullable() + .comment('JSON serialized object with properties for this task'); + table + .dateTime('next_run_start_at') + .notNullable() + .comment('The next time that the task should be started'); + table + .text('current_run_ticket') + .nullable() + .comment('A unique ticket for the current task run'); + table + .dateTime('current_run_started_at') + .nullable() + .comment('The time that the current task run started'); + table + .dateTime('current_run_expires_at') + .nullable() + .comment('The time that the current task run will time out'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + // + // tasks + // + await knex.schema.dropTable('backstage_backend_tasks__tasks'); +}; diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json new file mode 100644 index 0000000000..10db5cfaac --- /dev/null +++ b/packages/backend-tasks/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/backend-tasks", + "description": "Common distributed task management library for Backstage backends", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-tasks" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli build --outputs cjs,types", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.8", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/types": "^0.1.1", + "@types/luxon": "^2.0.4", + "knex": "^0.95.1", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "node-abort-controller": "^3.0.1", + "uuid": "^8.0.0", + "winston": "^3.2.1", + "zod": "^3.9.5" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.8", + "@backstage/cli": "^0.8.1", + "jest": "^26.0.1", + "wait-for-expect": "^3.0.2" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/packages/backend-tasks/src/database/migrateBackendTasks.ts b/packages/backend-tasks/src/database/migrateBackendTasks.ts new file mode 100644 index 0000000000..cc0f4349ac --- /dev/null +++ b/packages/backend-tasks/src/database/migrateBackendTasks.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { DB_MIGRATIONS_TABLE } from './tables'; + +const migrationsDir = resolvePackagePath( + '@backstage/backend-tasks', + 'migrations', +); + +export async function migrateBackendTasks(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + tableName: DB_MIGRATIONS_TABLE, + }); +} diff --git a/packages/backend-tasks/src/database/tables.ts b/packages/backend-tasks/src/database/tables.ts new file mode 100644 index 0000000000..63aad6e42a --- /dev/null +++ b/packages/backend-tasks/src/database/tables.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations'; +export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks'; + +export type DbTasksRow = { + id: string; + settings_json: string; + next_run_start_at: Date; + current_run_ticket?: string; + current_run_started_at?: Date | string; + current_run_expires_at?: Date | string; +}; diff --git a/packages/backend-tasks/src/index.ts b/packages/backend-tasks/src/index.ts new file mode 100644 index 0000000000..dd75aca68c --- /dev/null +++ b/packages/backend-tasks/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common distributed task management library for Backstage backends + * + * @packageDocumentation + */ + +export * from './tasks'; diff --git a/packages/backend-tasks/src/setupTests.ts b/packages/backend-tasks/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/packages/backend-tasks/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts new file mode 100644 index 0000000000..e387b85413 --- /dev/null +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; + +describe('PluginTaskManagerImpl', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + async function init(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + const manager = new PluginTaskSchedulerImpl( + async () => knex, + getVoidLogger(), + ); + return { knex, manager }; + } + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('scheduleTask', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + }); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + }, + 60_000, + ); + }); +}); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts new file mode 100644 index 0000000000..93975bc327 --- /dev/null +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import { Logger } from 'winston'; +import { TaskWorker } from './TaskWorker'; +import { PluginTaskScheduler, TaskDefinition } from './types'; +import { validateId } from './util'; + +/** + * Implements the actual task management. + */ +export class PluginTaskSchedulerImpl implements PluginTaskScheduler { + constructor( + private readonly databaseFactory: () => Promise, + private readonly logger: Logger, + ) {} + + async scheduleTask(task: TaskDefinition): Promise { + validateId(task.id); + + const knex = await this.databaseFactory(); + + const worker = new TaskWorker(task.id, task.fn, knex, this.logger); + await worker.start( + { + version: 1, + initialDelayDuration: task.initialDelay?.toISO(), + recurringAtMostEveryDuration: task.frequency.toISO(), + timeoutAfterDuration: task.timeout.toISO(), + }, + { + signal: task.signal, + }, + ); + } +} diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.ts new file mode 100644 index 0000000000..8b90afff42 --- /dev/null +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.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 { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; +import { Logger } from 'winston'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { sleep } from './util'; + +/** + * Makes sure to auto-expire and clean up things that time out or for other + * reasons should not be left lingering. + */ +export class PluginTaskSchedulerJanitor { + private readonly knex: Knex; + private readonly waitBetweenRuns: Duration; + private readonly logger: Logger; + + constructor(options: { + knex: Knex; + waitBetweenRuns: Duration; + logger: Logger; + }) { + this.knex = options.knex; + this.waitBetweenRuns = options.waitBetweenRuns; + this.logger = options.logger; + } + + async start(abortSignal?: AbortSignal) { + while (!abortSignal?.aborted) { + try { + await this.runOnce(); + } catch (e) { + this.logger.warn(`Error while performing janitorial tasks, ${e}`); + } + + await sleep(this.waitBetweenRuns, abortSignal); + } + } + + private async runOnce() { + // SQLite currently (Oct 1 2021) returns a number for returning() + // statements, effectively ignoring them and instead returning the outcome + // of the delete() - and knex also emits a warning about that fact, which + // is why we avoid that entirely for the sqlite3 driver. + // https://github.com/knex/knex/issues/4370 + // https://github.com/mapbox/node-sqlite3/issues/1453 + + const dbNull = this.knex.raw('null'); + + const tasksQuery = this.knex(DB_TASKS_TABLE) + .where('current_run_expires_at', '<', this.knex.fn.now()) + .update({ + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); + + if (this.knex.client.config.client === 'sqlite3') { + const tasks = await tasksQuery; + this.logger.warn(`${tasks} tasks timed out and were lost`); + } else { + const tasks = await tasksQuery.returning(['id']); + for (const { id } of tasks) { + this.logger.warn(`Task timed out and was lost: ${id}`); + } + } + } +} diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts new file mode 100644 index 0000000000..ce8e797503 --- /dev/null +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import { TaskScheduler } from './TaskScheduler'; +import waitForExpect from 'wait-for-expect'; + +describe('TaskScheduler', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + async function createDatabase( + databaseId: TestDatabaseId, + ): Promise { + const knex = await databases.init(databaseId); + const databaseManager: Partial = { + forPlugin: () => ({ + getClient: async () => knex, + }), + }; + return databaseManager as DatabaseManager; + } + + it.each(databases.eachSupportedId())( + 'can return a working plugin impl, %p', + async databaseId => { + const database = await createDatabase(databaseId); + const manager = new TaskScheduler(database, logger).forPlugin('test'); + const fn = jest.fn(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + }); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + }, + 60_000, + ); +}); diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts new file mode 100644 index 0000000000..f1668a6691 --- /dev/null +++ b/packages/backend-tasks/src/tasks/TaskScheduler.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 { DatabaseManager, getRootLogger } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { once } from 'lodash'; +import { Duration } from 'luxon'; +import { Logger } from 'winston'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; +import { PluginTaskScheduler } from './types'; + +/** + * Deals with the scheduling of distributed tasks. + * + * @public + */ +export class TaskScheduler { + static fromConfig( + config: Config, + options?: { + databaseManager?: DatabaseManager; + logger?: Logger; + }, + ): TaskScheduler { + const databaseManager = + options?.databaseManager ?? DatabaseManager.fromConfig(config); + const logger = (options?.logger || getRootLogger()).child({ + type: 'taskManager', + }); + return new TaskScheduler(databaseManager, logger); + } + + constructor( + private readonly databaseManager: DatabaseManager, + private readonly logger: Logger, + ) {} + + /** + * Instantiates a task manager instance for the given plugin. + * + * @param pluginId - The unique ID of the plugin, for example "catalog" + * @returns A {@link PluginTaskScheduler} instance + */ + forPlugin(pluginId: string): PluginTaskScheduler { + const databaseFactory = once(async () => { + const knex = await this.databaseManager.forPlugin(pluginId).getClient(); + + await migrateBackendTasks(knex); + + const janitor = new PluginTaskSchedulerJanitor({ + knex, + waitBetweenRuns: Duration.fromObject({ minutes: 1 }), + logger: this.logger, + }); + janitor.start(); + + return knex; + }); + + return new PluginTaskSchedulerImpl( + databaseFactory, + this.logger.child({ plugin: pluginId }), + ); + } +} diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts new file mode 100644 index 0000000000..2f977ca639 --- /dev/null +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -0,0 +1,245 @@ +/* + * 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 { TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { TaskWorker } from './TaskWorker'; +import { TaskSettingsV1 } from './types'; + +describe('TaskWorker', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'goes through the expected states, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV1 = { + version: 1, + initialDelayDuration: Duration.fromMillis(1000).toISO(), + recurringAtMostEveryDuration: Duration.fromMillis(2000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + expect(JSON.parse(row.settings_json)).toEqual({ + version: 1, + initialDelayDuration: 'PT1S', + recurringAtMostEveryDuration: 'PT2S', + timeoutAfterDuration: 'PT60S', + }); + + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'not-ready-yet', + }); + + waitForExpect(async () => { + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + }); + }); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + true, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'runs tasks more than once even when the task throws, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn().mockRejectedValue(new Error('failed')); + const settings: TaskSettingsV1 = { + version: 1, + initialDelayDuration: undefined, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + worker.start(settings); + + waitForExpect(() => { + expect(fn).toBeCalledTimes(3); + }); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'does not clobber ticket lock when stolen, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV1 = { + version: 1, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + + await knex(DB_TASKS_TABLE) + .where('id', '=', 'task1') + .update({ current_run_ticket: 'stolen' }); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'stolen', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'gracefully handles a disappeared task row, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn(async () => {}); + const settings: TaskSettingsV1 = { + version: 1, + recurringAtMostEveryDuration: Duration.fromMillis(0).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + }; + + const worker1 = new TaskWorker('task1', fn, knex, logger); + await worker1.persistTask(settings); + await knex(DB_TASKS_TABLE).where('id', '=', 'task1').delete(); + await expect(worker1.findReadyTask()).resolves.toEqual({ + result: 'abort', + }); + + const worker2 = new TaskWorker('task2', fn, knex, logger); + await worker2.persistTask(settings); + await expect(worker2.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await knex(DB_TASKS_TABLE).where('id', '=', 'task2').delete(); + await expect(worker2.tryClaimTask('ticket', settings)).resolves.toBe( + false, + ); + + const worker3 = new TaskWorker('task3', fn, knex, logger); + await worker3.persistTask(settings); + await expect(worker3.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + await expect(worker3.tryClaimTask('ticket', settings)).resolves.toBe( + true, + ); + await knex(DB_TASKS_TABLE).where('id', '=', 'task3').delete(); + await expect(worker3.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + }, + 60_000, + ); +}); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts new file mode 100644 index 0000000000..991d97b956 --- /dev/null +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -0,0 +1,249 @@ +/* + * 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 { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; +import { v4 as uuid } from 'uuid'; +import { Logger } from 'winston'; +import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; +import { TaskFunction, TaskSettingsV1, taskSettingsV1Schema } from './types'; +import { delegateAbortController, nowPlus, sleep } from './util'; + +const WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); + +/** + * Performs the actual work of a task. + * + * @private + */ +export class TaskWorker { + private readonly taskId: string; + private readonly fn: TaskFunction; + private readonly knex: Knex; + private readonly logger: Logger; + + constructor(taskId: string, fn: TaskFunction, knex: Knex, logger: Logger) { + this.taskId = taskId; + this.fn = fn; + this.knex = knex; + this.logger = logger; + } + + async start(settings: TaskSettingsV1, options?: { signal?: AbortSignal }) { + try { + await this.persistTask(settings); + } catch (e) { + throw new Error(`Failed to persist task, ${e}`); + } + + this.logger.info( + `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`, + ); + + (async () => { + try { + while (!options?.signal?.aborted) { + const runResult = await this.runOnce(options?.signal); + if (runResult.result === 'abort') { + break; + } + + await sleep(WORK_CHECK_FREQUENCY, options?.signal); + } + this.logger.info(`Task worker finished: ${this.taskId}`); + } catch (e) { + this.logger.warn(`Task worker failed unexpectedly, ${e}`); + } + })(); + } + + /** + * Makes a single attempt at running the task to completion, if ready. + * + * @returns The outcome of the attempt + */ + async runOnce( + signal?: AbortSignal, + ): Promise< + | { result: 'not-ready-yet' } + | { result: 'abort' } + | { result: 'failed' } + | { result: 'completed' } + > { + const findResult = await this.findReadyTask(); + if ( + findResult.result === 'not-ready-yet' || + findResult.result === 'abort' + ) { + return findResult; + } + + const taskSettings = findResult.settings; + const ticket = uuid(); + + const claimed = await this.tryClaimTask(ticket, taskSettings); + if (!claimed) { + return { result: 'not-ready-yet' }; + } + + // Abort the task execution either if the worker is stopped, or if the + // task timeout is hit + const taskAbortController = delegateAbortController(signal); + const timeoutHandle = setTimeout(() => { + taskAbortController.abort(); + }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds')); + + try { + await this.fn(taskAbortController.signal); + } catch (e) { + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'failed' }; + } finally { + clearTimeout(timeoutHandle); + } + + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'completed' }; + } + + /** + * Perform the initial store of the task info + */ + async persistTask(settings: TaskSettingsV1) { + // Perform an initial parse to ensure that we will definitely be able to + // read it back again. + taskSettingsV1Schema.parse(settings); + + const settingsJson = JSON.stringify(settings); + const startAt = settings.initialDelayDuration + ? nowPlus(Duration.fromISO(settings.initialDelayDuration), this.knex) + : this.knex.fn.now(); + + // It's OK if the task already exists; if it does, just replace its + // settings with the new value and start the loop as usual. + await this.knex(DB_TASKS_TABLE) + .insert({ + id: this.taskId, + settings_json: settingsJson, + next_run_start_at: startAt, + }) + .onConflict('id') + .merge(['settings_json']); + } + + /** + * Check if the task is ready to run + */ + async findReadyTask(): Promise< + | { result: 'not-ready-yet' } + | { result: 'abort' } + | { result: 'ready'; settings: TaskSettingsV1 } + > { + const [row] = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .select({ + settingsJson: 'settings_json', + ready: this.knex.raw( + ` + CASE + WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE + ELSE FALSE + END`, + [this.knex.fn.now()], + ), + }); + + if (!row) { + this.logger.info( + 'No longer able to find task; aborting and assuming that it has been unregistered or expired', + ); + return { result: 'abort' }; + } else if (!row.ready) { + return { result: 'not-ready-yet' }; + } + + try { + const settings = taskSettingsV1Schema.parse(JSON.parse(row.settingsJson)); + return { result: 'ready', settings }; + } catch (e) { + this.logger.info( + `Task "${this.taskId}" is no longer able to parse task settings; aborting and assuming that a ` + + `newer version of the task has been issued and being handled by other workers, ${e}`, + ); + return { result: 'abort' }; + } + } + + /** + * Attempts to claim a task that's ready for execution, on this worker's + * behalf. We should not attempt to perform the work unless the claim really + * goes through. + * + * @param ticket - A globally unique string that changes for each invocation + * @param settings - The settings of the task to claim + * @returns True if it was successfully claimed + */ + async tryClaimTask( + ticket: string, + settings: TaskSettingsV1, + ): Promise { + const startedAt = this.knex.fn.now(); + const expiresAt = settings.timeoutAfterDuration + ? nowPlus(Duration.fromISO(settings.timeoutAfterDuration), this.knex) + : this.knex.raw('null'); + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .whereNull('current_run_ticket') + .update({ + current_run_ticket: ticket, + current_run_started_at: startedAt, + current_run_expires_at: expiresAt, + }); + + return rows === 1; + } + + async tryReleaseTask( + ticket: string, + settings: TaskSettingsV1, + ): Promise { + const { recurringAtMostEveryDuration } = settings; + + // We make an effort to keep the datetime calculations in the database + // layer, making sure to not have to perform conversions back and forth and + // leaning on the database as a central clock source + const dbNull = this.knex.raw('null'); + const dt = Duration.fromISO(recurringAtMostEveryDuration).as('seconds'); + const nextRun = + this.knex.client.config.client === 'sqlite3' + ? this.knex.raw('datetime(next_run_start_at, ?)', [`+${dt} seconds`]) + : this.knex.raw(`next_run_start_at + interval '${dt} seconds'`); + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .where('current_run_ticket', '=', ticket) + .update({ + next_run_start_at: nextRun, + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); + + return rows === 1; + } +} diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts new file mode 100644 index 0000000000..9e0a06f71c --- /dev/null +++ b/packages/backend-tasks/src/tasks/index.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 { TaskScheduler } from './TaskScheduler'; +export type { + PluginTaskScheduler, + TaskDefinition, + TaskFunction, +} from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts new file mode 100644 index 0000000000..4693af7ef3 --- /dev/null +++ b/packages/backend-tasks/src/tasks/types.ts @@ -0,0 +1,138 @@ +/* + * 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 { Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; +import { z } from 'zod'; + +/** + * A function that can be called as a scheduled task. + * + * It may optionally accept an abort signal argument. When the signal triggers, + * processing should abort and return as quickly as possible. + * + * @public + */ +export type TaskFunction = + | ((abortSignal: AbortSignal) => void | Promise) + | (() => void | Promise); + +/** + * Options that apply to the invocation of a given task. + * + * @public + */ +export interface TaskDefinition { + /** + * A unique ID (within the scope of the plugin) for the task. + */ + id: string; + + /** + * The actual task function to be invoked regularly. + */ + fn: TaskFunction; + + /** + * An abort signal that, when triggered, will stop the recurring execution of + * the task. + */ + signal?: AbortSignal; + + /** + * The maximum amount of time that a single task invocation can take, before + * it's considered timed out and gets "released" such that a new invocation + * is permitted to take place (possibly, then, on a different worker). + * + * If no value is given for this field then there is no timeout. This is + * potentially dangerous. + */ + timeout: Duration; + + /** + * The amount of time that should pass between task invocation starts. + * Essentially, this equals roughly how often you want the task to run. + * + * This is a best effort value; under some circumstances there can be + * deviations. For example, if the task runtime is longer than the frequency + * and the timeout has not been given or not been exceeded yet, the next + * invocation of this task will be delayed until after the previous one + * finishes. + * + * The system does its best to avoid overlapping invocations. + * + * If no value is given for this field then the task will only be invoked + * once (on any worker) and then unscheduled automatically. + */ + frequency: Duration; + + /** + * The amount of time that should pass before the first invocation happens. + * + * This can be useful in cold start scenarios to stagger or delay some heavy + * compute jobs. + * + * If no value is given for this field then the first invocation will happen + * as soon as possible. + */ + initialDelay?: Duration; +} + +/** + * Deals with the scheduling of distributed tasks, for a given plugin. + * + * @public + */ +export interface PluginTaskScheduler { + /** + * Schedules a task function for coordinated exclusive invocation across + * workers. + * + * If the task was already scheduled since before by us or by another party, + * its options are just overwritten with the given options, and things + * continue from there. + * + * @param definition - The task definition + */ + scheduleTask(task: TaskDefinition): Promise; +} + +function isValidOptionalDurationString(d: string | undefined): boolean { + try { + return !d || Duration.fromISO(d).isValid === true; + } catch { + return false; + } +} + +export const taskSettingsV1Schema = z.object({ + version: z.literal(1), + initialDelayDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), + recurringAtMostEveryDuration: z + .string() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), + timeoutAfterDuration: z + .string() + .refine(isValidOptionalDurationString, { message: 'Invalid duration' }), +}); + +/** + * The properties that control a scheduled task (version 1). + */ +export type TaskSettingsV1 = z.infer; diff --git a/packages/backend-tasks/src/tasks/util.test.ts b/packages/backend-tasks/src/tasks/util.test.ts new file mode 100644 index 0000000000..f74669a7f4 --- /dev/null +++ b/packages/backend-tasks/src/tasks/util.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { Duration } from 'luxon'; +import { AbortController } from 'node-abort-controller'; +import { delegateAbortController, sleep, validateId } from './util'; + +describe('util', () => { + describe('validateId', () => { + it.each(['a', 'a_b', 'ab123c_2'])( + 'accepts valid inputs, %p', + async input => { + expect(validateId(input)).toBeUndefined(); + }, + ); + + it.each(['', 'a!', 'A', 'a-b', 'a.b', '_a', 'a_', null, Symbol('a')])( + 'rejects invalid inputs, %p', + async input => { + expect(() => validateId(input as any)).toThrow(); + }, + ); + }); + + describe('sleep', () => { + it('finishes the wait as expected with no signal', async () => { + const ac = new AbortController(); + const start = Date.now(); + await sleep(Duration.fromObject({ seconds: 1 }), ac.signal); + expect(Date.now() - start).toBeGreaterThan(800); + }, 5_000); + + it('aborts properly on the signal', async () => { + const ac = new AbortController(); + const promise = sleep(Duration.fromObject({ seconds: 10 }), ac.signal); + ac.abort(); + await promise; + expect(true).toBe(true); + }, 1_000); + }); + + describe('delegateAbortController', () => { + it('inherits parent abort state', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + parent.abort(); + expect(parent.signal.aborted).toBe(true); + expect(child.signal.aborted).toBe(true); + }); + + it('does not inherit from child to parent', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + child.abort(); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(true); + }); + }); +}); diff --git a/packages/backend-tasks/src/tasks/util.ts b/packages/backend-tasks/src/tasks/util.ts new file mode 100644 index 0000000000..0509f29363 --- /dev/null +++ b/packages/backend-tasks/src/tasks/util.ts @@ -0,0 +1,107 @@ +/* + * 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 { InputError } from '@backstage/errors'; +import { Knex } from 'knex'; +import { DateTime, Duration } from 'luxon'; +import { AbortController, AbortSignal } from 'node-abort-controller'; + +// Keep the IDs compatible with e.g. Prometheus +export function validateId(id: string) { + if (typeof id !== 'string' || !/^[a-z0-9]+(?:_[a-z0-9]+)*$/.test(id)) { + throw new InputError( + `${id} is not a valid ID, expected string of lowercase characters and digits separated by underscores`, + ); + } +} + +export function dbTime(t: Date | string): DateTime { + if (typeof t === 'string') { + return DateTime.fromSQL(t); + } + return DateTime.fromJSDate(t); +} + +export function nowPlus(duration: Duration | undefined, knex: Knex) { + const seconds = duration?.as('seconds') ?? 0; + if (!seconds) { + return knex.fn.now(); + } + return knex.client.config.client === 'sqlite3' + ? knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]) + : knex.raw(`now() + interval '${seconds} seconds'`); +} + +/** + * Sleep for the given duration, but return sooner if the abort signal + * triggers. + * + * @param duration - The amount of time to sleep, at most + * @param abortSignal - An optional abort signal that short circuits the wait + */ +export async function sleep( + duration: Duration, + abortSignal?: AbortSignal, +): Promise { + if (abortSignal?.aborted) { + return; + } + + await new Promise(resolve => { + let timeoutHandle: NodeJS.Timeout | undefined = undefined; + + const done = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + abortSignal?.removeEventListener('abort', done); + resolve(); + }; + + timeoutHandle = setTimeout(done, duration.as('milliseconds')); + abortSignal?.addEventListener('abort', done); + }); +} + +/** + * Creates a new AbortController that, in addition to working as a regular + * standalone controller, also gets aborted if the given parent signal + * reaches aborted state. + * + * @param parent - The "parent" signal that can trigger the delegate + */ +export function delegateAbortController(parent?: AbortSignal): AbortController { + const delegate = new AbortController(); + + if (parent) { + if (parent.aborted) { + delegate.abort(); + } else { + const onParentAborted = () => { + delegate.abort(); + }; + + const onChildAborted = () => { + parent.removeEventListener('abort', onParentAborted); + }; + + parent.addEventListener('abort', onParentAborted, { once: true }); + delegate.signal.addEventListener('abort', onChildAborted, { once: true }); + } + } + + return delegate; +} diff --git a/packages/backend-test-utils/.snyk b/packages/backend-test-utils/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/packages/backend-test-utils/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} diff --git a/packages/backend/.snyk b/packages/backend/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/packages/backend/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 19e9bcdd67..7cf7d9ddaa 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,20 @@ # example-backend +## 0.2.52 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-jenkins-backend@0.1.7 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.5 + - @backstage/plugin-scaffolder-backend@0.15.12 + - @backstage/plugin-azure-devops-backend@0.2.0 + - @backstage/catalog-client@0.5.1 + - @backstage/plugin-auth-backend@0.4.7 + - @backstage/plugin-catalog-backend@0.17.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.7 + ## 0.2.50 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index b9a28c2853..4c854241d4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.50", + "version": "0.2.52", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,30 +24,33 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@backstage/integration": "^0.6.8", "@backstage/plugin-app-backend": "^0.3.17", - "@backstage/plugin-auth-backend": "^0.4.5", - "@backstage/plugin-azure-devops-backend": "^0.1.3", + "@backstage/plugin-auth-backend": "^0.4.7", + "@backstage/plugin-azure-devops-backend": "^0.2.0", "@backstage/plugin-badges-backend": "^0.1.11", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-catalog-backend": "^0.17.3", "@backstage/plugin-code-coverage-backend": "^0.1.14", "@backstage/plugin-graphql-backend": "^0.1.9", - "@backstage/plugin-jenkins-backend": "^0.1.6", + "@backstage/plugin-jenkins-backend": "^0.1.7", "@backstage/plugin-kubernetes-backend": "^0.3.18", "@backstage/plugin-kafka-backend": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.13", "@backstage/plugin-rollbar-backend": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", + "@backstage/plugin-scaffolder-backend": "^0.15.12", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.7", "@backstage/plugin-search-backend": "^0.2.6", "@backstage/plugin-search-backend-node": "^0.4.2", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.5", "@backstage/plugin-search-backend-module-pg": "^0.2.1", "@backstage/plugin-techdocs-backend": "^0.10.5", + "@backstage/plugin-tech-insights-backend": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0", "@backstage/plugin-todo-backend": "^0.1.13", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", @@ -65,7 +68,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b6c148dfba..f978e84da9 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -48,6 +48,7 @@ import scaffolder from './plugins/scaffolder'; import proxy from './plugins/proxy'; import search from './plugins/search'; import techdocs from './plugins/techdocs'; +import techInsights from './plugins/techInsights'; import todo from './plugins/todo'; import graphql from './plugins/graphql'; import app from './plugins/app'; @@ -107,12 +108,16 @@ async function main() { const appEnv = useHotMemoize(module, () => createEnv('app')); const badgesEnv = useHotMemoize(module, () => createEnv('badges')); const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); + const techInsightsEnv = useHotMemoize(module, () => + createEnv('tech-insights'), + ); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv)); apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv)); apiRouter.use('/search', await search(searchEnv)); diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts new file mode 100644 index 0000000000..abab1d133d --- /dev/null +++ b/packages/backend/src/plugins/techInsights.ts @@ -0,0 +1,107 @@ +/* + * 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 { + createRouter, + buildTechInsightsContext, + createFactRetrieverRegistration, +} from '@backstage/plugin-tech-insights-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; +import { + JsonRulesEngineFactCheckerFactory, + JSON_RULE_ENGINE_CHECK_TYPE, +} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + +export default async function createPlugin({ + logger, + config, + discovery, + database, +}: PluginEnvironment): Promise { + const techInsightsContext = await buildTechInsightsContext({ + logger, + config, + database, + discovery, + factRetrievers: [ + createFactRetrieverRegistration('5 4 * * 6', { + // Example cron, At 04:05 on Saturday. + id: 'testRetriever', + version: '1.1.2', + entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. + schema: { + examplenumberfact: { + type: 'integer', + description: 'Example fact returning a number', + }, + }, + handler: async _ctx => { + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities({ + filter: [{ kind: 'component' }], + }); + + return Promise.resolve( + entities.items.map(it => { + return { + entity: { + namespace: it.metadata.namespace!!, + kind: it.kind, + name: it.metadata.name, + }, + facts: { + examplenumberfact: 2, + }, + }; + }), + ); + }, + }), + ], + factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ + checks: [ + { + id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factIds: ['testRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'examplenumberfact', + operator: 'lessThan', + value: 5, + }, + ], + }, + }, + }, + ], + logger, + }), + }); + + return await createRouter({ + ...techInsightsContext, + logger, + config, + }); +} diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 8243d09486..f5ec2116f7 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-client +## 0.5.1 + +### Patch Changes + +- 39e92897e4: Improved API documentation for catalog-client. + ## 0.5.0 ### Minor Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index b7bb713957..e16ca740f6 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -7,7 +7,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { Location as Location_2 } from '@backstage/catalog-model'; -// @public (undocumented) +// @public export type AddLocationRequest = { type?: string; target: string; @@ -15,126 +15,106 @@ export type AddLocationRequest = { presence?: 'optional' | 'required'; }; -// @public (undocumented) +// @public export type AddLocationResponse = { location: Location_2; entities: Entity[]; exists?: boolean; }; -// @public (undocumented) +// @public export const CATALOG_FILTER_EXISTS: unique symbol; -// @public (undocumented) +// @public export interface CatalogApi { - // (undocumented) addLocation( location: AddLocationRequest, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getEntities( request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise>; - // (undocumented) getEntityAncestors( request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getEntityByName( name: EntityName, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getLocationById( id: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; - // (undocumented) refreshEntity( entityRef: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) removeEntityByUid( uid: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) removeLocationById( id: string, options?: CatalogRequestOptions, ): Promise; } -// @public (undocumented) +// @public export class CatalogClient implements CatalogApi { constructor(options: { discoveryApi: DiscoveryApi }); - // (undocumented) addLocation( { type, target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getEntities( request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise>; - // (undocumented) getEntityAncestors( request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getLocationById( id: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; - // (undocumented) refreshEntity( entityRef: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) removeEntityByUid( uid: string, options?: CatalogRequestOptions, ): Promise; - // (undocumented) removeLocationById( id: string, options?: CatalogRequestOptions, ): Promise; } -// @public (undocumented) +// @public export type CatalogEntitiesRequest = { filter?: | Record[] @@ -143,12 +123,12 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; -// @public (undocumented) +// @public export type CatalogEntityAncestorsRequest = { entityRef: string; }; -// @public (undocumented) +// @public export type CatalogEntityAncestorsResponse = { root: EntityName; items: { @@ -157,12 +137,12 @@ export type CatalogEntityAncestorsResponse = { }[]; }; -// @public (undocumented) +// @public export type CatalogListResponse = { items: T[]; }; -// @public (undocumented) +// @public export type CatalogRequestOptions = { token?: string; }; diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index f8dfd59e41..9caac50c90 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/errors": "^0.1.3", + "@backstage/errors": "^0.1.4", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 75944467d8..29d01fbe5c 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -39,7 +39,11 @@ import { } from './types/api'; import { DiscoveryApi } from './types/discovery'; -/** @public */ +/** + * A frontend and backend compatible client for communicating with the Backstage Catalog. + * + * @public + * */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; @@ -47,6 +51,16 @@ export class CatalogClient implements CatalogApi { this.discoveryApi = options.discoveryApi; } + /** + * Gets the Ancestors of an Entity. + * + * @param request - A request type for retrieving Entity ancestors. + * @param options - An object with your preferred options. + * + * @returns A CatalogEntityAncestorsResponse. + * + * @public + */ async getEntityAncestors( request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, @@ -61,6 +75,16 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Gets a Location by Id. + * + * @param id - A string containing the Id. + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + * + * @public + */ async getLocationById( id: string, options?: CatalogRequestOptions, @@ -72,6 +96,16 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Gets a set of Entities. + * + * @param request - A request type for retrieving an Entity. + * @param options - An object with your preferred options. + * + * @returns A CatalogListResponse. + * + * @public + */ async getEntities( request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, @@ -139,6 +173,16 @@ export class CatalogClient implements CatalogApi { return { items: entities.sort(refCompare) }; } + /** + * Gets a given Entity based on a provided name. + * + * @param compoundName - A string containing the name. + * @param options - An object with your preferred options. + * + * @returns An {@link catalog-model#Entity}. + * + * @public + */ async getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, @@ -153,6 +197,14 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Refreshes an Entity. + * + * @param entityRef - A string containing the entityREf + * @param options - An object with your preferred options. + * + * @public + */ async refreshEntity(entityRef: string, options?: CatalogRequestOptions) { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/refresh`, @@ -171,6 +223,16 @@ export class CatalogClient implements CatalogApi { } } + /** + * Adds a location. + * + * @param options - An object with your preferred options. + * @param AddLocationRequest - A request object for adding locations. + * + * @returns An AddLocationResponse + * + * @public + */ async addLocation( { type = 'url', target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, @@ -206,6 +268,16 @@ export class CatalogClient implements CatalogApi { }; } + /** + * Gets an origin Location By Entity. + * + * @param entity - An Entity + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + * + * @public + */ async getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, @@ -225,6 +297,16 @@ export class CatalogClient implements CatalogApi { .find(l => locationCompound === stringifyLocationReference(l)); } + /** + * Gets a Location by Entity. + * + * @param entity - An Entity + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + * + * @public + */ async getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, @@ -243,6 +325,14 @@ export class CatalogClient implements CatalogApi { .find(l => locationCompound === stringifyLocationReference(l)); } + /** + * Removes a location as identified by Id. + * + * @param id - A string containing the Id + * @param options - An object with your preferred options. + * + * @public + */ async removeLocationById( id: string, options?: CatalogRequestOptions, @@ -254,6 +344,14 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Removes an Entity as identified by Uid. + * + * @param uid - A string containing the Uid + * @param options - An object with your preferred options. + * + * @public + */ async removeEntityByUid( uid: string, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index c7025e9452..6344caff7c 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -16,10 +16,18 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; -/** @public */ +/** + * A Symbol to define if a catalog filter exists or not. + * + * @public + */ export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS'); -/** @public */ +/** + * A request type for retrieving catalog Entities. + * + * @public + */ export type CatalogEntitiesRequest = { filter?: | Record[] @@ -28,75 +36,176 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; -/** @public */ +/** + * A request type for Catalog Entity Ancestor information. + * + * @public + */ export type CatalogEntityAncestorsRequest = { entityRef: string; }; -/** @public */ +/** + * A response type for Catalog Entity Ancestor information. + * + * @public + */ export type CatalogEntityAncestorsResponse = { root: EntityName; items: { entity: Entity; parents: EntityName[] }[]; }; -/** @public */ +/** + * A response type for the result of a catalog operation in list form. + * + * @public + */ export type CatalogListResponse = { items: T[]; }; -/** @public */ +/** + * Options you can pass into a catalog request for additional information. + * + * @public + */ export type CatalogRequestOptions = { token?: string; }; -/** @public */ +/** + * Public functions for interacting with the Catalog API. + * + * @public + */ export interface CatalogApi { - // Entities + /** + * Gets the Entities from the catalog based on your request and options. + * + * @param request - An object with your filters and fields. + * @param options - An object with your preferred options. + * + * @returns A CatalogListResponse with items typed Catalog Model Entity. + * + */ getEntities( request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise>; + /** + * Gets the Entity ancestor information from the catalog based on your request and options. + * + * @param request - An object with your filters and fields. + * @param options - An object with your preferred options. + * + * @returns A CatalogEntityAncestorsResponse. + */ getEntityAncestors( request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; + /** + * Gets a single Entity from the catalog by Entity name. + * + * @param name - A complete Entity name, with the full kind-namespace-name triplet. + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Entity}. + */ getEntityByName( name: EntityName, options?: CatalogRequestOptions, ): Promise; + /** + * Removes a single Entity from the catalog by Entity UID. + * + * @param uid - A string of the Entity UID. + * @param options - An object with your preferred options. + * + */ removeEntityByUid( uid: string, options?: CatalogRequestOptions, ): Promise; + /** + * Refreshes an Entity in the catalog. + * + * @param entityRef - A string in the form of 'Kind/default:foo'. + * @param options - An object with your preferred options. + * + */ refreshEntity( entityRef: string, options?: CatalogRequestOptions, ): Promise; // Locations + /** + * Gets a Location object by ID from the catalog. + * + * @param id - A string in of the Location Id. + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + */ getLocationById( id: string, options?: CatalogRequestOptions, ): Promise; + /** + * Gets origin location by Entity. + * + * @param entity - An {@link catalog-model#Entity}. + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + */ getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; + /** + * Gets Location by Entity. + * + * @param entity - An {@link catalog-model#Entity}. + * @param options - An object with your preferred options. + * + * @returns A {@link catalog-model#Location_2}. + */ getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise; + /** + * Adds a Location. + * + * @param location - A request type for adding a Location to the catalog. + * @param options - An object with your preferred options. + * + * @returns A AddLocationResponse. + */ addLocation( location: AddLocationRequest, options?: CatalogRequestOptions, ): Promise; + /** + * Removes a Location by Id. + * + * @param id - A string in of the Location Id. + * @param options - An object with your preferred options. + * + */ removeLocationById( id: string, options?: CatalogRequestOptions, ): Promise; } -/** @public */ +/** + * A request type for adding a Location to the catalog. + * + * @public + */ export type AddLocationRequest = { type?: string; target: string; @@ -104,7 +213,11 @@ export type AddLocationRequest = { presence?: 'optional' | 'required'; }; -/** @public */ +/** + * A response type for adding a Location to the catalog. + * + * @public + */ export type AddLocationResponse = { location: Location; entities: Entity[]; diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 066b01cd50..e0c067c9c7 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/catalog-model +## 0.9.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + ## 0.9.5 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 615e80b684..b64e536578 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.5", + "version": "0.9.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", @@ -42,7 +42,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index b4b87fa103..f3978798c6 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli-common +## 0.1.5 + +### Patch Changes + +- c2d50a0073: Add docs to the last export of cli-common + ## 0.1.4 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 0693b555a6..5916cdb017 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.4", + "version": "0.1.5", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7cb3c5be91..0fc702851d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/cli +## 0.8.2 + +### Patch Changes + +- dd355bca46: Switched to dynamically determining the packages that are unsafe to repack when executing the CLI within the Backstage main repo. +- b393c4d4be: Fixed the `config:check` command that was incorrectly only validating frontend configuration. Also added a `--frontend` flag to the command which maintains that behavior. +- 0611f3b3e2: Reading app config from a remote server +- ec64d9590c: Make `ExitCodeError` call `super` early to avoid compiler warnings +- 8af66229e7: Bumped `@spotify/eslint-config-react` from `v10` to `v12`, dropping support for Node.js v12. +- a197708da9: Bumped `@spotify/eslint-config-typescript` from `v10` to `v12`, dropping support for Node.js v12. +- Updated dependencies + - @backstage/config-loader@0.7.2 + +## 0.8.1 + +### Patch Changes + +- f1e96dc5b1: Update usage of msw in default plugin template +- b0dc1fd241: bump `@spotify/eslint-config-base` from 9.0.2 to 12.0.0 +- c5bb1df55d: Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + - @backstage/config-loader@0.7.1 + ## 0.8.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 62a008087e..546a1597e1 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.8.0", + "version": "0.8.2", "private": false, "publishConfig": { "access": "public" @@ -28,10 +28,10 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.7.0", - "@backstage/errors": "^0.1.3", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/config-loader": "^0.7.2", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", @@ -41,9 +41,9 @@ "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^13.0.0", "@rollup/plugin-yaml": "^3.0.0", - "@spotify/eslint-config-base": "^9.0.0", - "@spotify/eslint-config-react": "^10.0.0", - "@spotify/eslint-config-typescript": "^10.0.0", + "@spotify/eslint-config-base": "^12.0.0", + "@spotify/eslint-config-react": "^12.0.0", + "@spotify/eslint-config-typescript": "^12.0.0", "@sucrase/jest-plugin": "^2.1.1", "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "5.5.x", @@ -110,21 +110,21 @@ "typescript": "^4.0.3", "util": "^0.12.3", "webpack": "^5.48.0", - "webpack-dev-server": "4.0.0-rc.0", + "webpack-dev-server": "4.3.1", "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yml-loader": "^2.1.0", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/core-app-api": "^0.1.18", + "@backstage/backend-common": "^0.9.9", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", - "@backstage/theme": "^0.2.11", + "@backstage/test-utils": "^0.1.21", + "@backstage/theme": "^0.2.13", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/commands/config/print.ts index 6bd575f9bf..b640ee4f37 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/commands/config/print.ts @@ -25,6 +25,7 @@ export default async (cmd: Command) => { args: cmd.config, fromPackage: cmd.package, mockEnv: cmd.lax, + fullVisibility: !cmd.frontend, }); const visibility = getVisibilityOption(cmd); const data = serializeConfigData(appConfigs, schema, visibility); diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts index 9041272c5a..2d3ce60366 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/commands/config/validate.ts @@ -22,5 +22,6 @@ export default async (cmd: Command) => { args: cmd.config, fromPackage: cmd.package, mockEnv: cmd.lax, + fullVisibility: !cmd.frontend, }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 302f76cc5d..4e01a8ca9f 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -172,6 +172,7 @@ export function registerCommands(program: CommanderStatic) { 'Only load config schema that applies to the given package', ) .option('--lax', 'Do not require environment variables to be set') + .option('--frontend', 'Only validate the frontend configuration') .option(...configOption) .description( 'Validate that the given configuration loads and matches schema', diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index cf9e873a66..863728b8f8 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -32,6 +32,23 @@ export const transforms = (options: TransformOptions): Transforms => { const extraTransforms = isDev ? ['react-hot-loader'] : []; + // This ensures that styles inserted from the style-loader and any + // async style chunks are always given lower priority than JSS styles. + // Note that this function is stringified and executed in the browser + // after transpilation, so stick to simple syntax + function insertBeforeJssStyles(element: any) { + const head = document.head; + // This makes sure that any style elements we insert get put before the + // dynamic styles from JSS, such as the ones from `makeStyles()`. + // TODO(Rugvip): This will likely break in material-ui v5, keep an eye on it. + const firstJssNode = head.querySelector('style[data-jss]'); + if (!firstJssNode) { + head.appendChild(element); + } else { + head.insertBefore(element, firstJssNode); + } + } + const loaders = [ { test: /\.(tsx?)$/, @@ -112,7 +129,14 @@ export const transforms = (options: TransformOptions): Transforms => { { test: /\.css$/i, use: [ - isDev ? require.resolve('style-loader') : MiniCssExtractPlugin.loader, + isDev + ? { + loader: require.resolve('style-loader'), + options: { + insert: insertBeforeJssStyles, + }, + } + : MiniCssExtractPlugin.loader, { loader: require.resolve('css-loader'), options: { @@ -132,6 +156,7 @@ export const transforms = (options: TransformOptions): Transforms => { new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash:8].css', chunkFilename: 'static/[name].[id].[contenthash:8].css', + insert: insertBeforeJssStyles, // Only applies to async chunks }), ); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 3688d60089..6f93318ead 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -14,19 +14,30 @@ * limitations under the License. */ -import { loadConfig, loadConfigSchema } from '@backstage/config-loader'; +import { + ConfigTarget, + loadConfig, + loadConfigSchema, +} from '@backstage/config-loader'; import { ConfigReader } from '@backstage/config'; import { paths } from './paths'; +import { isValidUrl } from './urls'; type Options = { args: string[]; fromPackage?: string; mockEnv?: boolean; withFilteredKeys?: boolean; + fullVisibility?: boolean; }; export async function loadCliConfig(options: Options) { - const configPaths = options.args.map(arg => paths.resolveTarget(arg)); + const configTargets: ConfigTarget[] = []; + options.args.forEach(arg => { + if (!isValidUrl(arg)) { + configTargets.push({ path: paths.resolveTarget(arg) }); + } + }); // Consider all packages in the monorepo when loading in config const { Project } = require('@lerna/project'); @@ -48,7 +59,8 @@ export async function loadCliConfig(options: Options) { ? async name => process.env[name] || 'x' : undefined, configRoot: paths.targetRoot, - configPaths, + configPaths: [], + configTargets: configTargets, }); // printing to stderr to not clobber stdout in case the cli command @@ -59,7 +71,9 @@ export async function loadCliConfig(options: Options) { try { const frontendAppConfigs = schema.process(appConfigs, { - visibility: ['frontend'], + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], withFilteredKeys: options.withFilteredKeys, }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index c0b4c45b6c..dd3955a10a 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -26,11 +26,11 @@ export class ExitCodeError extends CustomError { readonly code: number; constructor(code: number, command?: string) { - if (command) { - super(`Command '${command}' exited with code ${code}`); - } else { - super(`Child exited with code ${code}`); - } + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); this.code = code; } } diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 6917795e83..dba42e6013 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -24,14 +24,16 @@ import { tmpdir } from 'os'; import tar, { CreateOptions } from 'tar'; import { paths } from '../paths'; import { run } from '../run'; -import { packageVersions } from '../version'; import { ParallelOption } from '../parallel'; +import { + dependencies as cliDependencies, + devDependencies as cliDevDependencies, +} from '../../../package.json'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ - ...Object.keys(packageVersions), - '@backstage/cli-common', - '@backstage/config-loader', + ...Object.keys(cliDependencies), + ...Object.keys(cliDevDependencies), ]; type LernaPackage = { diff --git a/packages/cli/src/lib/urls.test.ts b/packages/cli/src/lib/urls.test.ts new file mode 100644 index 0000000000..c2a67fb849 --- /dev/null +++ b/packages/cli/src/lib/urls.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isValidUrl } from './urls'; + +describe('isValidUrl', () => { + it('should return true for url', () => { + const validUrl = isValidUrl('http://some.valid.url'); + expect(validUrl).toBe(true); + }); + + it('should return false for absolute path', () => { + const validUrl = isValidUrl('/some/absolute/path'); + expect(validUrl).toBe(false); + }); + + it('should return false for relative path', () => { + const validUrl = isValidUrl('../some/relative/path'); + expect(validUrl).toBe(false); + }); +}); diff --git a/packages/cli/src/lib/urls.ts b/packages/cli/src/lib/urls.ts new file mode 100644 index 0000000000..848cea25d9 --- /dev/null +++ b/packages/cli/src/lib/urls.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. + */ + +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } +} diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 43b9e83fe1..5cca7dcb07 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -36,7 +36,7 @@ "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", "supertest": "{{versionQuery 'supertest' '4.0.2'}}", - "msw": "{{versionQuery 'msw' '0.29.0'}}" + "msw": "{{versionQuery 'msw' '0.35.0'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index ef56d55343..41376821ba 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -44,7 +44,7 @@ "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '13.1.8'}}", "@types/jest": "{{versionQuery '@types/jest' '26.0.7'}}", "@types/node": "{{versionQuery '@types/node' '14.14.32'}}", - "msw": "{{versionQuery 'msw' '0.29.0'}}", + "msw": "{{versionQuery 'msw' '0.35.0'}}", "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}" }, "files": [ diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 0044b67a37..ccbebcac40 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -4,12 +4,15 @@ import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from "@backstage/test-utils"; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index fa1e289d8e..95533c899d 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -3,12 +3,12 @@ import { render } from '@testing-library/react'; import { ExampleFetchComponent } from './ExampleFetchComponent'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; describe('ExampleFetchComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 7aabaef583..1043d9718f 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/codemods +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.5 + - @backstage/core-components@0.7.2 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.19 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 77bdd181be..17e89c86f0 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.19", + "version": "0.1.21", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "backstage-codemods": "bin/backstage-codemods" }, "dependencies": { - "@backstage/cli-common": "0.1.4", + "@backstage/cli-common": "0.1.5", "@backstage/core-app-api": "*", "@backstage/core-components": "*", "@backstage/core-plugin-api": "*", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 0b4ef9a5f3..0f1bc27996 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/config-loader +## 0.7.2 + +### Patch Changes + +- 0611f3b3e2: Reading app config from a remote server +- 26c5659c97: Bump msw to the same version as the rest + +## 0.7.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- ea21f7f567: bump `typescript-json-schema` from 0.50.1 to 0.51.0 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + ## 0.7.0 ### Minor Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 19b5ee9e27..6f568df942 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -23,6 +23,17 @@ export type ConfigSchemaProcessingOptions = { withFilteredKeys?: boolean; }; +// Warning: (ae-missing-release-tag) "ConfigTarget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ConfigTarget = + | { + path: string; + } + | { + url: string; + }; + // @public export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; @@ -33,12 +44,26 @@ export function loadConfig(options: LoadConfigOptions): Promise; export type LoadConfigOptions = { configRoot: string; configPaths: string[]; + configTargets: ConfigTarget[]; env?: string; experimentalEnvFunc?: (name: string) => Promise; - watch?: { - onChange: (configs: AppConfig[]) => void; - stopSignal?: Promise; - }; + remote?: LoadConfigOptionsRemote; + watch?: LoadConfigOptionsWatch; +}; + +// Warning: (ae-missing-release-tag) "LoadConfigOptionsRemote" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type LoadConfigOptionsRemote = { + reloadIntervalSeconds: number; +}; + +// Warning: (ae-missing-release-tag) "LoadConfigOptionsWatch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type LoadConfigOptionsWatch = { + onChange: (configs: AppConfig[]) => void; + stopSignal?: Promise; }; // @public @@ -71,4 +96,8 @@ export type TransformFunc = ( visibility: ConfigVisibility; }, ) => T | undefined; + +// Warnings were encountered during analysis: +// +// src/loader.d.ts:33:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/config-loader" does not have an export "configTargets" ``` diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index a731225e2b..2e25d91856 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.7.0", + "version": "0.7.2", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", - "@backstage/config": "^0.1.9", - "@backstage/errors": "^0.1.3", + "@backstage/cli-common": "^0.1.5", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", @@ -41,9 +41,10 @@ "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", - "typescript-json-schema": "^0.50.1", + "typescript-json-schema": "^0.51.0", "yaml": "^1.9.2", - "yup": "^0.32.9" + "yup": "^0.32.9", + "node-fetch": "2.6.5" }, "devDependencies": { "@types/jest": "^26.0.7", @@ -51,7 +52,8 @@ "@types/mock-fs": "^4.10.0", "@types/node": "^14.14.32", "@types/yup": "^0.29.13", - "mock-fs": "^5.1.0" + "mock-fs": "^5.1.0", + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index 888c68ef70..97f0d301a3 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -29,4 +29,9 @@ export type { TransformFunc, } from './lib'; export { loadConfig } from './loader'; -export type { LoadConfigOptions } from './loader'; +export type { + ConfigTarget, + LoadConfigOptions, + LoadConfigOptionsWatch, + LoadConfigOptionsRemote, +} from './loader'; diff --git a/packages/config-loader/src/lib/index.ts b/packages/config-loader/src/lib/index.ts index 32a0191cae..ca88b771ba 100644 --- a/packages/config-loader/src/lib/index.ts +++ b/packages/config-loader/src/lib/index.ts @@ -17,3 +17,4 @@ export { readEnvConfig } from './env'; export * from './transform'; export * from './schema'; +export { isValidUrl } from './urls'; diff --git a/packages/config-loader/src/lib/urls.test.ts b/packages/config-loader/src/lib/urls.test.ts new file mode 100644 index 0000000000..c2a67fb849 --- /dev/null +++ b/packages/config-loader/src/lib/urls.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isValidUrl } from './urls'; + +describe('isValidUrl', () => { + it('should return true for url', () => { + const validUrl = isValidUrl('http://some.valid.url'); + expect(validUrl).toBe(true); + }); + + it('should return false for absolute path', () => { + const validUrl = isValidUrl('/some/absolute/path'); + expect(validUrl).toBe(false); + }); + + it('should return false for relative path', () => { + const validUrl = isValidUrl('../some/relative/path'); + expect(validUrl).toBe(false); + }); +}); diff --git a/packages/config-loader/src/lib/urls.ts b/packages/config-loader/src/lib/urls.ts new file mode 100644 index 0000000000..848cea25d9 --- /dev/null +++ b/packages/config-loader/src/lib/urls.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. + */ + +export function isValidUrl(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch { + return false; + } +} diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 3f51e76936..804254afa0 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -18,8 +18,43 @@ import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import mockFs from 'mock-fs'; import fs from 'fs-extra'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; describe('loadConfig', () => { + const server = setupServer(); + const initialLoaderHandler = rest.get( + `https://some.domain.io/app-config.yaml`, + (_req, res, ctx) => { + return res( + ctx.body( + `app: + title: Remote Example App + sessionKey: 'abc123' + escaped: \$\${Escaped} + `, + ), + ); + }, + ); + + const reloadHandler = rest.get( + `https://some.domain.io/app-config.yaml`, + (_req, res, ctx) => { + return res( + ctx.body( + `app: + title: NEW ReMOTe ExaMPLe App + sessionKey: 'abc123' + escaped: \$\${Escaped} + `, + ), + ); + }, + ); + + beforeAll(() => server.listen()); + beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; @@ -32,6 +67,13 @@ describe('loadConfig', () => { $file: secrets/session-key.txt escaped: \$\${Escaped} `, + '/root/app-config2.yaml': ` + app: + title: Example App 2 + sessionKey: + $file: secrets/session-key.txt + escaped: \$\${Escaped} + `, '/root/app-config.development.yaml': ` app: sessionKey: development-key @@ -67,13 +109,17 @@ describe('loadConfig', () => { afterEach(() => { mockFs.restore(); + server.resetHandlers(); }); + afterAll(() => server.close()); + it('load config from default path', async () => { await expect( loadConfig({ configRoot: '/root', configPaths: [], + configTargets: [], env: 'production', }), ).resolves.toEqual([ @@ -90,11 +136,73 @@ describe('loadConfig', () => { ]); }); - it('loads config with secrets', async () => { + it('load config from remote path', async () => { + server.use(initialLoaderHandler); + + const configUrl = 'https://some.domain.io/app-config.yaml'; + + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + configTargets: [{ url: configUrl }], + env: 'production', + remote: { + reloadIntervalSeconds: 30, + }, + }), + ).resolves.toEqual([ + { + context: configUrl, + data: { + app: { + title: 'Remote Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + }); + + it('loads config with secrets from two different files', async () => { + await expect( + loadConfig({ + configRoot: '/root', + configPaths: ['/root/app-config2.yaml'], + configTargets: [{ path: '/root/app-config.yaml' }], + env: 'production', + }), + ).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + { + context: 'app-config2.yaml', + data: { + app: { + title: 'Example App 2', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + }); + + it('loads config with secrets from single file', async () => { await expect( loadConfig({ configRoot: '/root', configPaths: ['/root/app-config.yaml'], + configTargets: [{ path: '/root/app-config.yaml' }], env: 'production', }), ).resolves.toEqual([ @@ -115,9 +223,10 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [ - '/root/app-config.yaml', - '/root/app-config.development.yaml', + configPaths: [], + configTargets: [ + { path: '/root/app-config.yaml' }, + { path: '/root/app-config.development.yaml' }, ], env: 'development', }), @@ -155,7 +264,8 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: ['/root/app-config.substitute.yaml'], + configPaths: [], + configTargets: [{ path: '/root/app-config.substitute.yaml' }], env: 'development', }), ).resolves.toEqual([ @@ -181,6 +291,7 @@ describe('loadConfig', () => { loadConfig({ configRoot: '/root', configPaths: [], + configTargets: [], watch: { onChange: onChange.resolve, stopSignal: stopSignal.promise, @@ -218,12 +329,64 @@ describe('loadConfig', () => { stopSignal.resolve(); }); + it('watches remote config urls', async () => { + server.use(initialLoaderHandler); + + const onChange = defer(); + const stopSignal = defer(); + + const configUrl = 'https://some.domain.io/app-config.yaml'; + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + configTargets: [{ url: configUrl }], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + remote: { + reloadIntervalSeconds: 1, + }, + }), + ).resolves.toEqual([ + { + context: configUrl, + data: { + app: { + title: 'Remote Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + server.use(reloadHandler); + + await expect(onChange.promise).resolves.toEqual([ + { + context: configUrl, + data: { + app: { + title: 'NEW ReMOTe ExaMPLe App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + it('stops watching config files', async () => { const stopSignal = defer(); await loadConfig({ configRoot: '/root', configPaths: [], + configTargets: [], watch: { onChange: () => { expect('not').toBe('called'); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 6803d61786..4a5660f46c 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -17,15 +17,38 @@ import fs from 'fs-extra'; import yaml from 'yaml'; import chokidar from 'chokidar'; -import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; +import { basename, dirname, isAbsolute, resolve as resolvePath } from 'path'; import { AppConfig } from '@backstage/config'; import { ForwardedError } from '@backstage/errors'; import { applyConfigTransforms, - readEnvConfig, createIncludeTransform, createSubstitutionTransform, + isValidUrl, + readEnvConfig, } from './lib'; +import fetch from 'node-fetch'; + +export type ConfigTarget = { path: string } | { url: string }; + +export type LoadConfigOptionsWatch = { + /** + * A listener that is called when a config file is changed. + */ + onChange: (configs: AppConfig[]) => void; + + /** + * An optional signal that stops the watcher once the promise resolves. + */ + stopSignal?: Promise; +}; + +export type LoadConfigOptionsRemote = { + /** + * An optional remote config reloading period, in seconds + */ + reloadIntervalSeconds: number; +}; /** * Options that control the loading of configuration files in the backend. @@ -36,9 +59,14 @@ export type LoadConfigOptions = { // The root directory of the config loading context. Used to find default configs. configRoot: string; - // Absolute paths to load config files from. Configs from earlier paths have lower priority. + /** Absolute paths to load config files from. Configs from earlier paths have lower priority. + * @deprecated Use {@link configTargets} instead. + */ configPaths: string[]; + // Paths to load config files from. Configs from earlier paths have lower priority. + configTargets: ConfigTarget[]; + /** @deprecated This option has been removed */ env?: string; @@ -49,20 +77,15 @@ export type LoadConfigOptions = { */ experimentalEnvFunc?: (name: string) => Promise; + /** + * An optional remote config + */ + remote?: LoadConfigOptionsRemote; + /** * An optional configuration that enables watching of config files. */ - watch?: { - /** - * A listener that is called when a config file is changed. - */ - onChange: (configs: AppConfig[]) => void; - - /** - * An optional signal that stops the watcher once the promise resolves. - */ - stopSignal?: Promise; - }; + watch?: LoadConfigOptionsWatch; }; /** @@ -73,12 +96,32 @@ export type LoadConfigOptions = { export async function loadConfig( options: LoadConfigOptions, ): Promise { - const { configRoot, experimentalEnvFunc: envFunc, watch } = options; - const configPaths = options.configPaths.slice(); + const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options; + + const configPaths: string[] = options.configTargets + .slice() + .filter((e): e is { path: string } => e.hasOwnProperty('path')) + .map(configTarget => configTarget.path); + + // Append deprecated configPaths to the absolute config paths received via configTargets. + options.configPaths.forEach(cp => { + if (!configPaths.includes(cp)) { + configPaths.push(cp); + } + }); + + const configUrls: string[] = options.configTargets + .slice() + .filter((e): e is { url: string } => e.hasOwnProperty('url')) + .map(configTarget => configTarget.url); + + if (remote === undefined && configUrls.length > 0) { + throw new Error(`Remote config detected but this feature is turned off`); + } // If no paths are provided, we default to reading // `app-config.yaml` and, if it exists, `app-config.local.yaml` - if (configPaths.length === 0) { + if (configPaths.length === 0 && configUrls.length === 0) { configPaths.push(resolvePath(configRoot, 'app-config.yaml')); const localConfig = resolvePath(configRoot, 'app-config.local.yaml'); @@ -114,22 +157,67 @@ export async function loadConfig( return configs; }; - let fileConfigs; + const loadRemoteConfigFiles = async () => { + const configs: AppConfig[] = []; + + const readConfigFromUrl = async (url: string) => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Could not read config file at ${url}`); + } + + return await response.text(); + }; + + for (let i = 0; i < configUrls.length; i++) { + const configUrl = configUrls[i]; + if (!isValidUrl(configUrl)) { + throw new Error(`Config load path is not valid: '${configUrl}'`); + } + + const remoteConfigContent = await readConfigFromUrl(configUrl); + if (!remoteConfigContent) { + throw new Error(`Config is not valid`); + } + const configYaml = yaml.parse(remoteConfigContent); + const substitutionTransform = createSubstitutionTransform(env); + const data = await applyConfigTransforms(configRoot, configYaml, [ + substitutionTransform, + ]); + + configs.push({ data, context: configUrl }); + } + + return configs; + }; + + let fileConfigs: AppConfig[]; try { fileConfigs = await loadConfigFiles(); } catch (error) { throw new ForwardedError('Failed to read static configuration file', error); } + let remoteConfigs: AppConfig[] = []; + if (remote) { + try { + remoteConfigs = await loadRemoteConfigFiles(); + } catch (error) { + throw new ForwardedError( + `Failed to read remote configuration file`, + error, + ); + } + } + const envConfigs = await readEnvConfig(process.env); - // Set up config file watching if requested by the caller - if (watch) { - let currentSerializedConfig = JSON.stringify(fileConfigs); - + const watchConfigFile = (watchProp: LoadConfigOptionsWatch) => { const watcher = chokidar.watch(configPaths, { usePolling: process.env.NODE_ENV === 'test', }); + + let currentSerializedConfig = JSON.stringify(fileConfigs); watcher.on('change', async () => { try { const newConfigs = await loadConfigFiles(); @@ -140,18 +228,69 @@ export async function loadConfig( } currentSerializedConfig = newSerializedConfig; - watch.onChange([...newConfigs, ...envConfigs]); + watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]); } catch (error) { console.error(`Failed to reload configuration files, ${error}`); } }); - if (watch.stopSignal) { - watch.stopSignal.then(() => { + if (watchProp.stopSignal) { + watchProp.stopSignal.then(() => { watcher.close(); }); } + }; + + const watchRemoteConfig = ( + watchProp: LoadConfigOptionsWatch, + remoteProp: LoadConfigOptionsRemote, + ) => { + const hasConfigChanged = async ( + oldRemoteConfigs: AppConfig[], + newRemoteConfigs: AppConfig[], + ) => { + return ( + JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs) + ); + }; + + let handle: NodeJS.Timeout | undefined; + try { + handle = setInterval(async () => { + console.info(`Checking for config update`); + const newRemoteConfigs = await loadRemoteConfigFiles(); + if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) { + remoteConfigs = newRemoteConfigs; + console.info(`Remote config change, reloading config ...`); + watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]); + console.info(`Remote config reloaded`); + } + }, remoteProp.reloadIntervalSeconds * 1000); + } catch (error) { + console.error(`Failed to reload configuration files, ${error}`); + } + + if (watchProp.stopSignal) { + watchProp.stopSignal.then(() => { + if (handle !== undefined) { + console.info(`Stopping remote config watch`); + clearInterval(handle); + handle = undefined; + } + }); + } + }; + + // Set up config file watching if requested by the caller + if (watch) { + watchConfigFile(watch); } - return [...fileConfigs, ...envConfigs]; + if (watch && remote) { + watchRemoteConfig(watch, remote); + } + + return remote + ? [...remoteConfigs, ...fileConfigs, ...envConfigs] + : [...fileConfigs, ...envConfigs]; } diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 9735c330c1..de62de5466 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/config +## 0.1.11 + +### Patch Changes + +- 10d267a1b7: Minor exports cleanup +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils + ## 0.1.10 ### Patch Changes diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 205db6ef2b..efa6b85559 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -46,39 +46,22 @@ export class ConfigReader implements Config { fallback?: ConfigReader | undefined, prefix?: string, ); - // (undocumented) static fromConfigs(configs: AppConfig[]): ConfigReader; - // (undocumented) get(key?: string): T; - // (undocumented) getBoolean(key: string): boolean; - // (undocumented) getConfig(key: string): ConfigReader; - // (undocumented) getConfigArray(key: string): ConfigReader[]; - // (undocumented) getNumber(key: string): number; - // (undocumented) getOptional(key?: string): T | undefined; - // (undocumented) getOptionalBoolean(key: string): boolean | undefined; - // (undocumented) getOptionalConfig(key: string): ConfigReader | undefined; - // (undocumented) getOptionalConfigArray(key: string): ConfigReader[] | undefined; - // (undocumented) getOptionalNumber(key: string): number | undefined; - // (undocumented) getOptionalString(key: string): string | undefined; - // (undocumented) getOptionalStringArray(key: string): string[] | undefined; - // (undocumented) getString(key: string): string; - // (undocumented) getStringArray(key: string): string[]; - // (undocumented) has(key: string): boolean; - // (undocumented) keys(): string[]; } diff --git a/packages/config/package.json b/packages/config/package.json index 719ab22772..d162b73cae 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.10", + "version": "0.1.11", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index bc8a860c85..8c0c4140a5 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -72,6 +72,9 @@ export class ConfigReader implements Config { private filteredKeys?: string[]; private notifiedFilteredKeys = new Set(); + /** + * Instantiates the config reader from a list of application config objects. + */ static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { return new ConfigReader(undefined); @@ -96,6 +99,7 @@ export class ConfigReader implements Config { private readonly prefix: string = '', ) {} + /** {@inheritdoc Config.has} */ has(key: string): boolean { const value = this.readValue(key); if (value !== undefined) { @@ -104,12 +108,14 @@ export class ConfigReader implements Config { return this.fallback?.has(key) ?? false; } + /** {@inheritdoc Config.keys} */ keys(): string[] { const localKeys = this.data ? Object.keys(this.data) : []; const fallbackKeys = this.fallback?.keys() ?? []; return [...new Set([...localKeys, ...fallbackKeys])]; } + /** {@inheritdoc Config.get} */ get(key?: string): T { const value = this.getOptional(key); if (value === undefined) { @@ -118,6 +124,7 @@ export class ConfigReader implements Config { return value as T; } + /** {@inheritdoc Config.getOptional} */ getOptional(key?: string): T | undefined { const value = this.readValue(key); const fallbackValue = this.fallback?.getOptional(key); @@ -154,6 +161,7 @@ export class ConfigReader implements Config { ).value as T; } + /** {@inheritdoc Config.getConfig} */ getConfig(key: string): ConfigReader { const value = this.getOptionalConfig(key); if (value === undefined) { @@ -162,6 +170,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalConfig} */ getOptionalConfig(key: string): ConfigReader | undefined { const value = this.readValue(key); const fallbackConfig = this.fallback?.getOptionalConfig(key); @@ -177,6 +186,7 @@ export class ConfigReader implements Config { return fallbackConfig; } + /** {@inheritdoc Config.getConfigArray} */ getConfigArray(key: string): ConfigReader[] { const value = this.getOptionalConfigArray(key); if (value === undefined) { @@ -185,6 +195,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalConfigArray} */ getOptionalConfigArray(key: string): ConfigReader[] | undefined { const configs = this.readConfigValue(key, values => { if (!Array.isArray(values)) { @@ -220,6 +231,7 @@ export class ConfigReader implements Config { return configs.map((obj, index) => this.copy(obj, `${key}[${index}]`)); } + /** {@inheritdoc Config.getNumber} */ getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { @@ -228,6 +240,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalNumber} */ getOptionalNumber(key: string): number | undefined { const value = this.readConfigValue( key, @@ -247,6 +260,7 @@ export class ConfigReader implements Config { return number; } + /** {@inheritdoc Config.getBoolean} */ getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { @@ -255,6 +269,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalBoolean} */ getOptionalBoolean(key: string): boolean | undefined { return this.readConfigValue( key, @@ -262,6 +277,7 @@ export class ConfigReader implements Config { ); } + /** {@inheritdoc Config.getString} */ getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { @@ -270,6 +286,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalString} */ getOptionalString(key: string): string | undefined { return this.readConfigValue( key, @@ -278,6 +295,7 @@ export class ConfigReader implements Config { ); } + /** {@inheritdoc Config.getStringArray} */ getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { @@ -286,6 +304,7 @@ export class ConfigReader implements Config { return value; } + /** {@inheritdoc Config.getOptionalStringArray} */ getOptionalStringArray(key: string): string[] | undefined { return this.readConfigValue(key, values => { if (!Array.isArray(values)) { diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index e01e89f04c..b9e60b7dab 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/core-app-api +## 0.1.20 + +### Patch Changes + +- 78c512ce8f: I have added default icons for the catalog, scaffolder, techdocs, and search. +- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + +## 0.1.19 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils +- 6b615e92c8: Api cleanup, adding `@public` where necessary and tweaking some comments +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.18 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 09d3313c84..d98b5ef5f8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -28,6 +28,7 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { ConfigReader } from '@backstage/config'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Error as Error_2 } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; @@ -44,6 +45,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { OptionalAppOptions } from '@backstage/app-defaults'; import { PendingAuthRequest } from '@backstage/core-plugin-api'; import { PluginOutput } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; @@ -149,7 +151,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; @@ -163,15 +165,41 @@ export type AppContext = { getComponents(): AppComponents; }; +// @public +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + // @public export type AppOptions = { apis?: Iterable; - icons?: Partial & { + defaultApis?: Iterable; + icons: AppIcons & { [key in string]: IconComponent; }; plugins?: BackstagePluginWithAnyOutput[]; - components?: Partial; - themes?: AppTheme[]; + components: AppComponents; + themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; }; @@ -285,10 +313,11 @@ export type BootErrorPageProps = { export { ConfigReader }; -// Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts -// +// @public @deprecated +export function createApp(options?: OptionalAppOptions): BackstageApp; + // @public -export function createApp(options?: AppOptions): PrivateAppImpl; +export function createSpecializedApp(options: AppOptions): BackstageApp; // @public export const defaultConfigLoader: AppConfigLoader; @@ -298,11 +327,7 @@ export class ErrorAlerter implements ErrorApi { constructor(alertApi: AlertApi, errorApi: ErrorApi); // (undocumented) error$(): Observable<{ - error: { - name: string; - message: string; - stack?: string | undefined; - }; + error: Error_2; context?: ErrorContext | undefined; }>; // (undocumented) @@ -611,5 +636,4 @@ export class WebStorage implements StorageApi { // Warnings were encountered during analysis: // // src/apis/system/ApiProvider.d.ts:15:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts -// src/app/types.d.ts:152:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index aee8eb9f02..fc6c103109 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.18", + "version": "0.1.20", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/app-defaults": "^0.1.0", + "@backstage/core-components": "^0.7.3", + "@backstage/config": "^0.1.11", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -46,8 +47,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -56,7 +57,7 @@ "@types/node": "^14.14.32", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts index d0f137f0be..76cf01d20a 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts @@ -15,7 +15,6 @@ */ import MockOAuthApi from './MockOAuthApi'; -import PowerIcon from '@material-ui/icons/Power'; describe('MockOAuthApi', () => { it('should trigger all requests', async () => { @@ -24,13 +23,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn().mockImplementation(() => authResult); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn().mockResolvedValue('other'); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); @@ -67,13 +66,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn(); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn(); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts index d8faf90229..3110197ad9 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { OAuthRequestManager } from './OAuthRequestManager'; describe('OAuthRequestManager', () => { @@ -27,7 +26,7 @@ describe('OAuthRequestManager', () => { const requester = manager.createAuthRequester({ provider: { title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, onAuthRequest: async () => 'hello', }); 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 227d70e494..caad40ddbf 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import AtlassianIcon from '@material-ui/icons/AcUnit'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'atlassian', title: 'Atlassian', - icon: AtlassianIcon, + icon: () => null, }; /** 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 69462942e4..0a158ccb9e 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import Auth0Icon from '@material-ui/icons/AcUnit'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'auth0', title: 'Auth0', - icon: Auth0Icon, + icon: () => null, }; /** 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 c9db93906a..e488580c4d 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import BitbucketIcon from '@material-ui/icons/FormatBold'; import { BackstageIdentity, bitbucketAuthApiRef, @@ -37,7 +36,7 @@ export type BitbucketAuthResponse = { const DEFAULT_PROVIDER = { id: 'bitbucket', title: 'Bitbucket', - icon: BitbucketIcon, + icon: () => null, }; /** 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 01e7e77b8e..885e80da7c 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import GithubIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { @@ -48,7 +47,7 @@ export type GithubAuthResponse = { const DEFAULT_PROVIDER = { id: 'github', title: 'GitHub', - icon: GithubIcon, + icon: () => null, }; /** 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 af581d0018..f46f2ef72e 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import GitlabIcon from '@material-ui/icons/AcUnit'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'gitlab', title: 'GitLab', - icon: GitlabIcon, + icon: () => null, }; /** 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 916c52382e..a6f37b250f 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import GoogleIcon from '@material-ui/icons/AcUnit'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'google', title: 'Google', - icon: GoogleIcon, + icon: () => null, }; const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; 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 09bd7f67a2..148be66873 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import MicrosoftIcon from '@material-ui/icons/AcUnit'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'microsoft', title: 'Microsoft', - icon: MicrosoftIcon, + icon: () => null, }; /** 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 6c0759d967..45937a68f8 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import OAuth2Icon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -51,7 +50,7 @@ export type OAuth2Response = { const DEFAULT_PROVIDER = { id: 'oauth2', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }; /** 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 294fb496af..465a124051 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import OktaIcon from '@material-ui/icons/AcUnit'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'okta', title: 'Okta', - icon: OktaIcon, + icon: () => null, }; const OKTA_OIDC_SCOPES: Set = new Set([ 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 f49ea4cde8..5f933b9c6e 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import OneLoginIcon from '@material-ui/icons/AcUnit'; import { oneloginAuthApiRef, OAuthRequestApi, @@ -33,7 +32,7 @@ type CreateOptions = { const DEFAULT_PROVIDER = { id: 'onelogin', title: 'onelogin', - icon: OneLoginIcon, + icon: () => null, }; const OIDC_SCOPES: Set = new Set([ 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 f2668dedd2..63985979de 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 @@ -14,7 +14,6 @@ * limitations under the License. */ -import SamlIcon from '@material-ui/icons/AcUnit'; import { DirectAuthConnector } from '../../../../lib/AuthConnector'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { @@ -42,7 +41,7 @@ export type SamlAuthResponse = { const DEFAULT_PROVIDER = { id: 'saml', title: 'SAML', - icon: SamlIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx similarity index 87% rename from packages/core-app-api/src/app/App.test.tsx rename to packages/core-app-api/src/app/AppManager.test.tsx index ab287a3ece..07401660e8 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -20,11 +20,9 @@ import { renderWithEffects, withLogCollector, } from '@backstage/test-utils'; -import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -import { defaultAppIcons } from './icons'; import { configApiRef, createApiFactory, @@ -37,8 +35,8 @@ import { createRoutableExtension, analyticsApiRef, } from '@backstage/core-plugin-api'; -import { generateBoundRoutes, PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; +import { generateBoundRoutes, AppManager } from './AppManager'; +import { AppComponents, AppIcons } from './types'; describe('generateBoundRoutes', () => { it('runs happy path', () => { @@ -128,7 +126,7 @@ describe('Integration Test', () => { const HiddenComponent = plugin2.provide( createRoutableExtension({ name: 'HiddenComponent', - component: () => Promise.resolve((_: { path?: string }) =>
), + component: () => Promise.resolve(() =>
), mountPoint: plugin2RouteRef, }), ); @@ -137,7 +135,7 @@ describe('Integration Test', () => { createRoutableExtension({ name: 'ExposedComponent', component: () => - Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { + Promise.resolve(() => { const link1 = useRouteRef(plugin1RouteRef); const link2 = useRouteRef(plugin2RouteRef); const subLink1 = useRouteRef(subRouteRef1); @@ -169,6 +167,7 @@ describe('Integration Test', () => { const NavigateComponent = plugin1.provide( createRoutableExtension({ + name: 'NavigateComponent', component: () => Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { return ; @@ -177,17 +176,19 @@ describe('Integration Test', () => { }), ); - const components = { + const components: AppComponents = { NotFoundErrorPage: () => null, BootErrorPage: () => null, Progress: () => null, Router: BrowserRouter, ErrorBoundaryFallback: () => null, - ThemeProvider: AppThemeProvider, + ThemeProvider: ({ children }) => <>{children}, }; + const icons = {} as AppIcons; + it('runs happy paths', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -195,12 +196,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -218,8 +220,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -241,7 +243,7 @@ describe('Integration Test', () => { }); it('runs happy paths without optional routes', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -249,12 +251,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -270,8 +273,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -298,7 +301,7 @@ describe('Integration Test', () => { }), ]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -306,10 +309,10 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], - icons: defaultAppIcons, + icons, plugins: [ createPlugin({ id: 'test', @@ -317,6 +320,7 @@ describe('Integration Test', () => { }), ], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -332,8 +336,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -348,7 +352,7 @@ describe('Integration Test', () => { it('should track route changes via analytics api', async () => { const mockAnalyticsApi = new MockAnalyticsApi(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -356,12 +360,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -378,7 +383,7 @@ describe('Integration Test', () => { } /> - } /> + } /> , @@ -408,7 +413,7 @@ describe('Integration Test', () => { }); it('should throw some error when the route has duplicate params', () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [], defaultApis: [], themes: [ @@ -416,12 +421,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -438,9 +444,9 @@ describe('Integration Test', () => { - - - + }> + } /> + , diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/AppManager.tsx similarity index 94% rename from packages/core-app-api/src/app/App.tsx rename to packages/core-app-api/src/app/AppManager.tsx index 8cb3f167fb..950f57bb7d 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -77,6 +77,8 @@ import { SignInPageProps, SignInResult, } from './types'; +import { AppThemeProvider } from './AppThemeProvider'; +import { defaultConfigLoader } from './defaultConfigLoader'; export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { const result = new Map(); @@ -121,17 +123,6 @@ function getBasePath(configApi: Config) { return pathname; } -type FullAppOptions = { - apis: Iterable; - icons: NonNullable; - plugins: BackstagePlugin[]; - components: AppComponents; - themes: AppTheme[]; - configLoader?: AppConfigLoader; - defaultApis: Iterable; - bindRoutes?: AppOptions['bindRoutes']; -}; - function useConfigLoader( configLoader: AppConfigLoader | undefined, components: AppComponents, @@ -151,7 +142,7 @@ function useConfigLoader( noConfigNode = ; } - const { ThemeProvider } = components; + const { ThemeProvider = AppThemeProvider } = components; // Before the config is loaded we can't use a router, so exit early if (noConfigNode) { @@ -170,7 +161,7 @@ function useConfigLoader( } class AppContextImpl implements AppContext { - constructor(private readonly app: PrivateAppImpl) {} + constructor(private readonly app: AppManager) {} getPlugins(): BackstagePlugin[] { return this.app.getPlugins(); @@ -185,7 +176,7 @@ class AppContextImpl implements AppContext { } } -export class PrivateAppImpl implements BackstageApp { +export class AppManager implements BackstageApp { private apiHolder?: ApiHolder; private configApi?: ConfigApi; @@ -201,14 +192,16 @@ export class PrivateAppImpl implements BackstageApp { private readonly identityApi = new AppIdentity(); private readonly apiFactoryRegistry: ApiFactoryRegistry; - constructor(options: FullAppOptions) { - this.apis = options.apis; + constructor(options: AppOptions) { + this.apis = options.apis ?? []; this.icons = options.icons; - this.plugins = new Set(options.plugins); + this.plugins = new Set( + (options.plugins as BackstagePlugin[]) ?? [], + ); this.components = options.components; - this.themes = options.themes; - this.configLoader = options.configLoader; - this.defaultApis = options.defaultApis; + this.themes = options.themes as AppTheme[]; + this.configLoader = options.configLoader ?? defaultConfigLoader; + this.defaultApis = options.defaultApis ?? []; this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); } @@ -307,7 +300,7 @@ export class PrivateAppImpl implements BackstageApp { return loadedConfig.node; } - const { ThemeProvider } = this.components; + const { ThemeProvider = AppThemeProvider } = this.components; return ( diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index 4e3a8487a6..2e145025d5 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -90,6 +90,17 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) { throw new Error('App has no themes'); } + if (appTheme.Provider) { + return ; + } + + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + + 'and should be migrated to use a Provider instead. ' + + 'See https://backstage.io/docs/api/deprecations#app-theme for more info.', + ); + return ( {children} diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 65aa15b50c..e11076099b 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -14,171 +14,25 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { Button } from '@material-ui/core'; -import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components'; -import { darkTheme, lightTheme } from '@backstage/theme'; -import DarkIcon from '@material-ui/icons/Brightness2'; -import LightIcon from '@material-ui/icons/WbSunny'; -import React, { PropsWithChildren } from 'react'; import { - BrowserRouter, - MemoryRouter, - useInRouterContext, -} from 'react-router-dom'; -import { PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; -import { defaultApis } from './defaultApis'; -import { defaultAppIcons } from './icons'; -import { - AppConfigLoader, - AppOptions, - BootErrorPageProps, - ErrorBoundaryFallbackProps, -} from './types'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; - -/** - * The default config loader, which expects that config is available at compile-time - * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as - * returned by the config loader. - * - * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, - * which can be rewritten at runtime to contain an additional JSON config object. - * If runtime config is present, it will be placed first in the config array, overriding - * other config values. - * - * @public - */ -export const defaultConfigLoader: AppConfigLoader = async ( - // This string may be replaced at runtime to provide additional config. - // It should be replaced by a JSON-serialized config object. - // It's a param so we can test it, but at runtime this will always fall back to default. - runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); - } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; - - // Avoiding this string also being replaced at runtime - if ( - runtimeConfigJson !== - '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') - ) { - try { - const data = JSON.parse(runtimeConfigJson) as JsonObject; - if (Array.isArray(data)) { - configs.push(...data); - } else { - configs.push({ data, context: 'env' }); - } - } catch (error) { - throw new Error(`Failed to load runtime configuration, ${error}`); - } - } - - const windowAppConfig = (window as any).__APP_CONFIG__; - if (windowAppConfig) { - configs.push({ - context: 'window', - data: windowAppConfig, - }); - } - return configs; -}; - -export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) { - if (useInRouterContext()) { - return <>{children}; - } - return {children}; -} + createApp as createDefaultApp, + OptionalAppOptions, +} from '@backstage/app-defaults'; /** * Creates a new Backstage App. * + * @deprecated Use {@link @backstage/app-defaults#createApp} from `@backstage/app-defaults` instead + * @param options - A set of options for creating the app * @public */ -export function createApp(options?: AppOptions) { - const DefaultNotFoundPage = () => ( - +export function createApp(options?: OptionalAppOptions) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp function from @backstage/core-app-api will soon be removed, ' + + 'migrate to importing createApp from the @backstage/app-defaults package instead. ' + + 'If you do not wish to use a standard app configuration but instead supply all options yourself ' + + ' you can use createSpecializedApp from @backstage/core-app-api instead.', ); - const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { - let message = ''; - if (step === 'load-config') { - message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; - } else if (step === 'load-chunk') { - message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; - } - // TODO: figure out a nicer way to handle routing on the error page, when it can be done. - return ( - - - - ); - }; - const DefaultErrorBoundaryFallback = ({ - error, - resetError, - plugin, - }: ErrorBoundaryFallbackProps) => { - return ( - - - - ); - }; - - const apis = options?.apis ?? []; - const icons = { ...defaultAppIcons, ...options?.icons }; - const plugins = options?.plugins ?? []; - const components = { - NotFoundErrorPage: DefaultNotFoundPage, - BootErrorPage: DefaultBootErrorPage, - Progress: Progress, - Router: BrowserRouter, - ErrorBoundaryFallback: DefaultErrorBoundaryFallback, - ThemeProvider: AppThemeProvider, - ...options?.components, - }; - const themes = options?.themes ?? [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - icon: , - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - theme: darkTheme, - icon: , - }, - ]; - const configLoader = options?.configLoader ?? defaultConfigLoader; - - return new PrivateAppImpl({ - apis, - icons, - plugins: plugins as BackstagePlugin[], - components, - themes, - configLoader, - defaultApis, - bindRoutes: options?.bindRoutes, - }); + return createDefaultApp(options); } diff --git a/packages/core-app-api/src/app/createSpecializedApp.tsx b/packages/core-app-api/src/app/createSpecializedApp.tsx new file mode 100644 index 0000000000..9bda223722 --- /dev/null +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -0,0 +1,29 @@ +/* + * 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 { AppManager } from './AppManager'; +import { AppOptions, BackstageApp } from './types'; + +/** + * Creates a new Backstage App where the full set of options are required. + * + * @public + * @param options - A set of options for creating the app + * @returns + */ +export function createSpecializedApp(options: AppOptions): BackstageApp { + return new AppManager(options); +} diff --git a/packages/core-app-api/src/app/createApp.test.tsx b/packages/core-app-api/src/app/defaultConfigLoader.test.ts similarity index 66% rename from packages/core-app-api/src/app/createApp.test.tsx rename to packages/core-app-api/src/app/defaultConfigLoader.test.ts index 0c3f62b14d..aebba92c43 100644 --- a/packages/core-app-api/src/app/createApp.test.tsx +++ b/packages/core-app-api/src/app/defaultConfigLoader.test.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { render, screen } from '@testing-library/react'; -import { renderWithEffects } from '@backstage/test-utils'; -import React, { PropsWithChildren } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { - defaultConfigLoader, - OptionallyWrapInRouter, - createApp, -} from './createApp'; +import { defaultConfigLoader } from './defaultConfigLoader'; (process as any).env = { NODE_ENV: 'test' }; const anyEnv = process.env as any; @@ -100,44 +92,3 @@ describe('defaultConfigLoader', () => { ]); }); }); - -describe('OptionallyWrapInRouter', () => { - it('should wrap with router if not yet inside a router', async () => { - const { getByText } = render( - Test, - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); - - it('should not wrap with router if already inside a router', async () => { - const { getByText } = render( - - Test - , - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); -}); - -describe('Optional ThemeProvider', () => { - it('should render app with user-provided ThemeProvider', async () => { - const components = { - NotFoundErrorPage: () => null, - BootErrorPage: () => null, - Progress: () => null, - Router: MemoryRouter, - ErrorBoundaryFallback: () => null, - ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( -
{children}
- ), - }; - - const App = createApp({ components }).getProvider(); - - await renderWithEffects(); - - expect(screen.getByRole('main')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-app-api/src/app/defaultConfigLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts new file mode 100644 index 0000000000..00aee2d367 --- /dev/null +++ b/packages/core-app-api/src/app/defaultConfigLoader.ts @@ -0,0 +1,73 @@ +/* + * 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 { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { AppConfigLoader } from './types'; + +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + * + * @public + */ +export const defaultConfigLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = appConfig.slice() as unknown as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if ( + runtimeConfigJson !== + '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') + ) { + try { + const data = JSON.parse(runtimeConfigJson) as JsonObject; + if (Array.isArray(data)) { + configs.push(...data); + } else { + configs.push({ data, context: 'env' }); + } + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + const windowAppConfig = (window as any).__APP_CONFIG__; + if (windowAppConfig) { + configs.push({ + context: 'window', + data: windowAppConfig, + }); + } + return configs; +}; diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index ab82774cad..5ea5405632 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -14,5 +14,7 @@ * limitations under the License. */ -export { createApp, defaultConfigLoader } from './createApp'; +export { createApp } from './createApp'; +export { createSpecializedApp } from './createSpecializedApp'; +export { defaultConfigLoader } from './defaultConfigLoader'; export * from './types'; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 407018111b..f2d1976f87 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,7 +27,6 @@ import { PluginOutput, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; -import { AppIcons } from './icons'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -97,7 +96,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; /** * An optional sign-in page that will be rendered instead of the AppRouter at startup. @@ -111,6 +110,36 @@ export type AppComponents = { SignInPage?: ComponentType; }; +/** + * A set of well-known icons that should be available within an app. + * + * @public + */ +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + /** * A function that loads in the App config that will be accessible via the ConfigApi. * @@ -196,14 +225,23 @@ export type BackstagePluginWithAnyOutput = Omit< export type AppOptions = { /** * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. + * add new ones, or override factories provided by default or by plugins. */ apis?: Iterable; + /** + * A collection of ApiFactories to register in the application as default APIs. + * Theses APIs can not be overridden by plugin factories, but can be overridden + * by plugin APIs provided through the + * A collection of ApiFactories to register in the application to either + * add new ones, or override factories provided by default or by plugins. + */ + defaultApis?: Iterable; + /** * Supply icons to override the default ones. */ - icons?: Partial & { [key in string]: IconComponent }; + icons: AppIcons & { [key in string]: IconComponent }; /** * A list of all plugins to include in the app. @@ -213,7 +251,7 @@ export type AppOptions = { /** * Supply components to the app to override the default ones. */ - components?: Partial; + components: AppComponents; /** * Themes provided as a part of the app. By default two themes are included, one @@ -226,18 +264,26 @@ export type AppOptions = { * id: 'light', * title: 'Light Theme', * variant: 'light', - * theme: lightTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }, { * id: 'dark', * title: 'Dark Theme', * variant: 'dark', - * theme: darkTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }] * ``` */ - themes?: AppTheme[]; + themes: (Partial & Omit)[]; /** * A function that loads in App configuration that will be accessible via diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 28125f6fa8..baf89a59d8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; import { UrlPatternDiscovery } from '../../apis'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; @@ -29,7 +28,7 @@ const defaultOptions = { provider: { id: 'my-provider', title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, oauthRequestApi: new MockOAuthApi(), sessionTransform: ({ expiresInSeconds, ...res }: any) => ({ @@ -41,7 +40,7 @@ const defaultOptions = { describe('DefaultAuthConnector', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); afterEach(() => { jest.resetAllMocks(); diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 375f23dfda..1e62df337d 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/core-components +## 0.7.3 + +### Patch Changes + +- c11a37710a: Added a warning variant to `DismissableBanner` component. If you are using a + custom theme, you will need to add the optional `palette.banner.warning` color, + otherwise this variant will fall back to the `palette.banner.error` color. +- 5826c17b7d: Allow for `cellStyle` property on `TableColumn` to be a function as well as `React.CSSProperties` as per the Material UI Table component +- e0861b92ff: Add new way to override color selection to progress bar/gauge components. + + `Gauge`, `LinearGauge` and `GaugeCard` all accept a `getColor` prop, + which is a function of the type: + + ```ts + export type GaugePropsGetColor = (args: { + palette: Palette; + value: number; + inverse?: boolean; + max?: number; + }) => string; + ``` + + Return a standard CSS color string (e.g. "red", "#f02020"), and the gauge will + be set to that color. + + If the prop is omitted, the default implementation is unchanged from previous + versions. + +- 021986e8a3: fixed route resolving (issue #7741) when user cannot select a tab in any of the tabbed pages (like the Catalog page) if it shares the same initial letters as a preceding tab. (i.e. where tab with a path of /ci is followed by a path of /ci-2, user cannot select /ci-2 as /ci will always be selected first). +- a39a2105ef: Add Theme Overrides for Sidebar +- Updated dependencies + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + +## 0.7.2 + +### Patch Changes + +- afd5c82ce1: Updates the d3 dependencies +- c6c2942daa: Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-plugin-api@0.1.12 + ## 0.7.1 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index cc884c9e8f..0f1e3510ae 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -7,6 +7,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 { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; @@ -100,7 +101,7 @@ export type BottomLinkProps = { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Breadcrumbs(props: Props_21): JSX.Element; +export function Breadcrumbs(props: Props_20): JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; @@ -157,7 +158,7 @@ export interface CodeSnippetProps { // Warning: (ae-missing-release-tag) "Content" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Content(props: PropsWithChildren): JSX.Element; +export function Content(props: PropsWithChildren): JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // @@ -368,10 +369,8 @@ export function FeatureCalloutCircular( // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export function Gauge(props: Props_11): JSX.Element; +export function Gauge(props: GaugeProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // @@ -384,6 +383,27 @@ export type GaugeCardClassKey = 'root'; // @public (undocumented) export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; +// @public (undocumented) +export type GaugeProps = { + value: number; + fractional?: boolean; + inverse?: boolean; + unit?: string; + max?: number; + getColor?: GaugePropsGetColor; +}; + +// @public (undocumented) +export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string; + +// @public (undocumented) +export type GaugePropsGetColorOptions = { + palette: BackstagePalette; + value: number; + inverse?: boolean; + max?: number; +}; + // @public (undocumented) export function GitHubIcon(props: IconComponentProps): JSX.Element; @@ -393,7 +413,7 @@ export function GroupIcon(props: IconComponentProps): JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Header(props: PropsWithChildren): JSX.Element; +export function Header(props: PropsWithChildren): JSX.Element; // @public (undocumented) export type HeaderClassKey = @@ -484,7 +504,7 @@ export type IconLinkVerticalProps = { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function InfoCard(props: Props_16): JSX.Element; +export function InfoCard(props: Props_15): JSX.Element; // @public (undocumented) export type InfoCardClassKey = @@ -565,7 +585,7 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // Warning: (ae-missing-release-tag) "LinearGauge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function LinearGauge(props: Props_12): JSX.Element | null; +export function LinearGauge(props: Props_11): JSX.Element | null; // Warning: (ae-missing-release-tag) "LinkType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -666,7 +686,7 @@ export type OverflowTooltipClassKey = 'container'; // Warning: (ae-missing-release-tag) "Page" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Page(props: PropsWithChildren): JSX.Element; +export function Page(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "PageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -771,7 +791,7 @@ export type SelectInputBaseClassKey = 'root' | 'input'; // Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Sidebar(props: PropsWithChildren): JSX.Element; +export function Sidebar(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1961,7 +1981,7 @@ export const SidebarSpacer: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SignInPage(props: Props_19): JSX.Element; +export function SignInPage(props: Props_18): JSX.Element; // Warning: (ae-missing-release-tag) "SignInPageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2050,7 +2070,7 @@ export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StructuredMetadataTable(props: Props_13): JSX.Element; +export function StructuredMetadataTable(props: Props_12): JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2132,7 +2152,7 @@ export type TabBarClassKey = 'indicator' | 'flexContainer' | 'root'; // Warning: (ae-missing-release-tag) "TabbedCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TabbedCard(props: PropsWithChildren): JSX.Element; +export function TabbedCard(props: PropsWithChildren): JSX.Element; // Warning: (ae-missing-release-tag) "TabbedCardClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 865f5b6df9..ffbde0072a 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.1", + "version": "0.7.3", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/config": "^0.1.11", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/theme": "^0.2.13", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,9 +42,9 @@ "@types/react-text-truncate": "^0.14.0", "classnames": "^2.2.6", "clsx": "^1.1.0", - "d3-selection": "^2.0.0", - "d3-shape": "^2.0.0", - "d3-zoom": "^2.0.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", @@ -67,17 +67,17 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.18", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/core-app-api": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@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/classnames": "^2.2.9", - "@types/d3-selection": "^2.0.0", + "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", - "@types/d3-zoom": "^2.0.0", + "@types/d3-zoom": "^3.0.1", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx index a64514be61..e0e9a89c9f 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.stories.tsx @@ -105,3 +105,14 @@ export const Fixed = () => (
); +export const Warning = () => ( +
+ + + +
+); diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 1213111c9d..92b449f705 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -79,12 +79,16 @@ const useStyles = makeStyles( error: { backgroundColor: theme.palette.banner.error, }, + warning: { + backgroundColor: + theme.palette.banner.warning ?? theme.palette.banner.error, + }, }), { name: 'BackstageDismissableBanner' }, ); type Props = { - variant: 'info' | 'error'; + variant: 'info' | 'error' | 'warning'; message: ReactNode; id: string; fixed?: boolean; diff --git a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx index 3481c48cef..401f502697 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.stories.tsx @@ -53,3 +53,9 @@ export const AbsoluteProgress = () => (
); + +export const StaticColor = () => ( +
+ '#f0f'} value={0.5} /> +
+); diff --git a/packages/core-components/src/components/ProgressBars/Gauge.test.tsx b/packages/core-components/src/components/ProgressBars/Gauge.test.tsx index 066e1233a0..2d9e852e6c 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.test.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.test.tsx @@ -56,16 +56,26 @@ describe('', () => { }; it('colors the progress correctly', () => { - expect(getProgressColor(palette, 'Not a Number' as any)).toBe('#ddd'); - expect(getProgressColor(palette, 10)).toBe(error); - expect(getProgressColor(palette, 50)).toBe(warning); - expect(getProgressColor(palette, 90)).toBe(ok); + expect(getProgressColor({ palette, value: 'Not a Number' as any })).toBe( + '#ddd', + ); + expect(getProgressColor({ palette, value: 10 })).toBe(error); + expect(getProgressColor({ palette, value: 50 })).toBe(warning); + expect(getProgressColor({ palette, value: 90 })).toBe(ok); }); it('colors the inverse progress correctly', () => { - expect(getProgressColor(palette, 'Not a Number' as any)).toBe('#ddd'); - expect(getProgressColor(palette, 10, true)).toBe(ok); - expect(getProgressColor(palette, 50, true)).toBe(warning); - expect(getProgressColor(palette, 90, true)).toBe(error); + expect( + getProgressColor({ + palette, + value: 'Not a Number' as any, + inverse: true, + }), + ).toBe('#ddd'); + expect(getProgressColor({ palette, value: 10, inverse: true })).toBe(ok); + expect(getProgressColor({ palette, value: 50, inverse: true })).toBe( + warning, + ); + expect(getProgressColor({ palette, value: 90, inverse: true })).toBe(error); }); }); diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 83e8d05b16..00e7b3257b 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -15,7 +15,7 @@ */ import { makeStyles, useTheme } from '@material-ui/core/styles'; -import { BackstageTheme } from '@backstage/theme'; +import { BackstagePalette, BackstageTheme } from '@backstage/theme'; import { Circle } from 'rc-progress'; import React from 'react'; @@ -46,32 +46,45 @@ const useStyles = makeStyles( { name: 'BackstageGauge' }, ); -type Props = { +/** @public */ +export type GaugeProps = { value: number; fractional?: boolean; inverse?: boolean; unit?: string; max?: number; + getColor?: GaugePropsGetColor; }; -const defaultProps = { +/** @public */ +export type GaugePropsGetColorOptions = { + palette: BackstagePalette; + value: number; + inverse?: boolean; + max?: number; +}; + +/** @public */ +export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string; + +const defaultGaugeProps = { fractional: true, inverse: false, unit: '%', max: 100, }; -export function getProgressColor( - palette: BackstageTheme['palette'], - value: number, - inverse?: boolean, - max?: number, -) { +export const getProgressColor: GaugePropsGetColor = ({ + palette, + value, + inverse, + max, +}) => { if (isNaN(value)) { return '#ddd'; } - const actualMax = max ? max : defaultProps.max; + const actualMax = max ? max : defaultGaugeProps.max; const actualValue = inverse ? actualMax - value : value; if (actualValue < actualMax / 3) { @@ -81,14 +94,15 @@ export function getProgressColor( } return palette.status.ok; -} +}; /** @public */ -export function Gauge(props: Props) { +export function Gauge(props: GaugeProps) { + const { getColor = getProgressColor } = props; const classes = useStyles(props); - const theme = useTheme(); + const { palette } = useTheme(); const { value, fractional, inverse, unit, max } = { - ...defaultProps, + ...defaultGaugeProps, ...props, }; @@ -102,7 +116,7 @@ export function Gauge(props: Props) { percent={asPercentage} strokeWidth={12} trailWidth={12} - strokeColor={getProgressColor(theme.palette, asActual, inverse, max)} + strokeColor={getColor({ palette, value: asActual, inverse, max })} className={classes.circle} />
diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index ad3323f8d3..64a6cc1b16 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -97,3 +97,24 @@ export const LinkInFooter = () => ( ); + +export const StaticColor = () => ( + + + '#f00'} title="Red" progress={0.5} /> + + + '#0f0'} title="Green" progress={0.5} /> + + + '#00f'} title="Blue" progress={0.5} /> + + + palette.status.error} + title="palette.status.error" + progress={0.5} + /> + + +); diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index fe17de30cd..0fa51b3919 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; -import { Gauge } from './Gauge'; +import { Gauge, GaugePropsGetColor } from './Gauge'; type Props = { title: string; @@ -28,6 +28,7 @@ type Props = { progress: number; inverse?: boolean; deepLink?: BottomLinkProps; + getColor?: GaugePropsGetColor; }; /** @public */ @@ -46,7 +47,14 @@ const useStyles = makeStyles( /** @public */ export function GaugeCard(props: Props) { const classes = useStyles(props); - const { title, subheader, progress, inverse, deepLink, variant } = props; + const { title, subheader, progress, inverse, deepLink, variant, getColor } = + props; + + const gaugeProps = { + inverse, + getColor, + value: progress, + }; return (
@@ -56,7 +64,7 @@ export function GaugeCard(props: Props) { deepLink={deepLink} variant={variant} > - +
); diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx index ee2ed2eae6..5d36ea9178 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.stories.tsx @@ -41,3 +41,9 @@ export const LowProgress = () => (
); + +export const StaticColor = () => ( +
+ '#f0f'} value={0.5} /> +
+); diff --git a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx index afc93fd337..f1e4fb3334 100644 --- a/packages/core-components/src/components/ProgressBars/LinearGauge.tsx +++ b/packages/core-components/src/components/ProgressBars/LinearGauge.tsx @@ -20,18 +20,19 @@ import Tooltip from '@material-ui/core/Tooltip'; // @ts-ignore import { Line } from 'rc-progress'; import { BackstageTheme } from '@backstage/theme'; -import { getProgressColor } from './Gauge'; +import { getProgressColor, GaugePropsGetColor } from './Gauge'; type Props = { /** * Progress value between 0.0 - 1.0. */ value: number; + getColor?: GaugePropsGetColor; }; export function LinearGauge(props: Props) { - const { value } = props; - const theme = useTheme(); + const { value, getColor = getProgressColor } = props; + const { palette } = useTheme(); if (isNaN(value)) { return null; } @@ -39,7 +40,12 @@ export function LinearGauge(props: Props) { if (percent > 100) { percent = 100; } - const strokeColor = getProgressColor(theme.palette, percent, false, 100); + const strokeColor = getColor({ + palette, + value: percent, + inverse: false, + max: 100, + }); return ( diff --git a/packages/core-components/src/components/ProgressBars/index.ts b/packages/core-components/src/components/ProgressBars/index.ts index 3d821eaac2..5e588ea696 100644 --- a/packages/core-components/src/components/ProgressBars/index.ts +++ b/packages/core-components/src/components/ProgressBars/index.ts @@ -17,5 +17,10 @@ export { GaugeCard } from './GaugeCard'; export type { GaugeCardClassKey } from './GaugeCard'; export { Gauge } from './Gauge'; +export type { + GaugeProps, + GaugePropsGetColor, + GaugePropsGetColorOptions, +} from './Gauge'; export type { GaugeClassKey } from './Gauge'; export { LinearGauge } from './LinearGauge'; diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx index 9b33304a95..bbd48c127c 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -31,6 +31,12 @@ const testRoute2 = { children:
tabbed-test-content-2
, }; +const testRoute3 = { + title: 'tabbed-test-title-3', + path: '/some-other-path-similar', + children:
tabbed-test-content-3
, +}; + describe('RoutedTabs', () => { it('renders simplest case', async () => { const rendered = await renderInTestApp( @@ -46,7 +52,7 @@ describe('RoutedTabs', () => { } + element={} /> , ); @@ -61,6 +67,13 @@ describe('RoutedTabs', () => { expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + + const thirdTab = rendered.queryAllByRole('tab')[2]; + act(() => { + fireEvent.click(thirdTab); + }); + expect(rendered.getByText('tabbed-test-title-3')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-3')).toBeInTheDocument(); }); describe('correctly delegates nested links', () => { diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 2a65c8ffc8..5debafd3f8 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -33,9 +33,15 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { element: children, })); - const element = useRoutes(routes) ?? subRoutes[0].children; + // TODO: remove once react-router updated + const sortedRoutes = routes.sort((a, b) => + // remove "/*" symbols from path end before comparing + b.path.replace(/\/\*$/, '').localeCompare(a.path.replace(/\/\*$/, '')), + ); - const [matchedRoute] = matchRoutes(routes, `/${params['*']}`) ?? []; + const element = useRoutes(sortedRoutes) ?? subRoutes[0].children; + + const [matchedRoute] = matchRoutes(sortedRoutes, `/${params['*']}`) ?? []; const foundIndex = matchedRoute ? subRoutes.findIndex(t => `${t.path}/*` === matchedRoute.route.path) : 0; diff --git a/packages/core-components/src/components/Table/Table.stories.tsx b/packages/core-components/src/components/Table/Table.stories.tsx index 7785764b22..b057d0a12c 100644 --- a/packages/core-components/src/components/Table/Table.stories.tsx +++ b/packages/core-components/src/components/Table/Table.stories.tsx @@ -326,3 +326,49 @@ export const FilterTable = () => { ); }; + +export const StyledTable = () => { + const classes = useStyles(); + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + cellStyle: (_, rowData: any & { tableData: { id: number } }) => { + return rowData.tableData.id % 2 === 0 + ? { + color: '#6CD75F', + } + : { + color: '#DC3D5A', + }; + }, + }, + { + title: 'Column 2', + field: 'col2', + cellStyle: { color: '#2FA5DC' }, + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ + + ); +}; diff --git a/packages/core-components/src/components/Table/Table.test.tsx b/packages/core-components/src/components/Table/Table.test.tsx index fb231fde31..12431dea59 100644 --- a/packages/core-components/src/components/Table/Table.test.tsx +++ b/packages/core-components/src/components/Table/Table.test.tsx @@ -18,17 +18,18 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { Table } from './Table'; +const column1 = { + title: 'Column 1', + field: 'col1', +}; + +const column2 = { + title: 'Column 2', + field: 'col2', +}; + const minProps = { - columns: [ - { - title: 'Column 1', - field: 'col1', - }, - { - title: 'Column 2', - field: 'col2', - }, - ], + columns: [column1, column2], data: [ { col1: 'first value, first row', @@ -47,6 +48,100 @@ describe('
', () => { expect(rendered.getByText('second value, second row')).toBeInTheDocument(); }); + describe('with style rows', () => { + describe('with CSS Properties object', () => { + const styledColumn2 = { + ...column2, + cellStyle: { + color: 'blue', + }, + }; + + it('renders non-highlighted correctly', async () => { + const columns = [column1, styledColumn2]; + + const rendered = await renderInTestApp( +
, + ); + expect(rendered.getByText('second value, first row')).toHaveStyle({ + color: 'blue', + }); + }); + + it('renders highlighted column correctly', async () => { + const columns = [ + column1, + { + ...styledColumn2, + highlight: true, + }, + ]; + + const rendered = await renderInTestApp( +
, + ); + expect(rendered.getByText('second value, first row')).toHaveStyle({ + color: 'blue', + 'font-weight': 700, + }); + }); + }); + + describe('with CSS Properties function', () => { + const styledColumn2 = { + ...column2, + cellStyle: ( + _data: any, + rowData: any & { tableData: { id: number } }, + ) => { + return rowData.tableData.id % 2 === 0 + ? { + color: 'green', + } + : { + color: 'red', + }; + }, + }; + + it('renders non-highlighted columns correctly', async () => { + const columns = [column1, styledColumn2]; + + const rendered = await renderInTestApp( +
, + ); + expect(rendered.getByText('second value, first row')).toHaveStyle({ + color: 'green', + }); + expect(rendered.getByText('second value, second row')).toHaveStyle({ + color: 'red', + }); + }); + + it('renders highlighted columns correctly', async () => { + const columns = [ + column1, + { + ...styledColumn2, + highlight: true, + }, + ]; + + const rendered = await renderInTestApp( +
, + ); + expect(rendered.getByText('second value, first row')).toHaveStyle({ + color: 'green', + 'font-weight': 700, + }); + expect(rendered.getByText('second value, second row')).toHaveStyle({ + color: 'red', + 'font-weight': 700, + }); + }); + }); + }); + it('renders with subtitle', async () => { const rendered = await renderInTestApp(
, diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index f433e7cdc5..e90ed04871 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -169,12 +169,26 @@ function convertColumns( ): TableColumn[] { return columns.map(column => { const headerStyle: React.CSSProperties = {}; - const cellStyle: React.CSSProperties = - typeof column.cellStyle === 'object' ? column.cellStyle : {}; + + let cellStyle = column.cellStyle || {}; if (column.highlight) { headerStyle.color = theme.palette.textContrast; - cellStyle.fontWeight = theme.typography.fontWeightBold; + + if (typeof cellStyle === 'object') { + (cellStyle as React.CSSProperties).fontWeight = + theme.typography.fontWeightBold; + } else { + const cellStyleFn = cellStyle as ( + data: any, + rowData: T, + column?: Column, + ) => React.CSSProperties; + cellStyle = (data, rowData, rowColumn) => { + const style = cellStyleFn(data, rowData, rowColumn); + return { ...style, fontWeight: theme.typography.fontWeightBold }; + }; + } } return { diff --git a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx index b1bd398d15..58785c5c90 100644 --- a/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx +++ b/packages/core-components/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx @@ -15,13 +15,10 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import { - wrapInTestApp, - Keyboard, - renderInTestApp, -} from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; import { HeaderActionMenu } from './HeaderActionMenu'; +import userEvent from '@testing-library/user-event'; describe('', () => { it('renders without any items and without exploding', async () => { @@ -89,16 +86,13 @@ describe('', () => { }); it('should close when hitting escape', async () => { - const rendered = render( - wrapInTestApp( - , - ), + const rendered = await renderInTestApp( + , ); - expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); fireEvent.click(rendered.getByTestId('header-action-menu')); expect(rendered.container.getAttribute('aria-hidden')).toBe('true'); - await Keyboard.type(rendered, ''); + userEvent.type(rendered.getByTestId('header-action-menu'), '{esc}'); expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); }); }); diff --git a/packages/core-components/src/layout/Page/Page.stories.tsx b/packages/core-components/src/layout/Page/Page.stories.tsx index 7386d198a3..12ddd51d04 100644 --- a/packages/core-components/src/layout/Page/Page.stories.tsx +++ b/packages/core-components/src/layout/Page/Page.stories.tsx @@ -20,8 +20,7 @@ import Grid from '@material-ui/core/Grid'; import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import React, { useState } from 'react'; -import { MemoryRouter } from 'react-router'; -import { createApp } from '@backstage/core-app-api'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Content, ContentHeader, @@ -197,53 +196,44 @@ const ExampleContentHeader = ({ selectedTab }: { selectedTab?: number }) => ( ); -const app = createApp({ configLoader: async () => [] }); -const AppProvider = app.getProvider(); - export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); - return ( - - -
- - - setSelectedTab(index)} - tabs={tabs.map(({ label }, index) => ({ - id: index.toString(), - label, - }))} - /> - - - - - -
-
-
- ); + return wrapInTestApp(() => ( +
+ + + setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + + + + +
+ )); }; export const PluginWithTable = () => { - return ( - -
- - - - -
- - - - - ); + return wrapInTestApp(() => ( +
+ + + + +
+ + + + )); }; diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 883e0395cd..ed089ad6a1 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -350,21 +350,30 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) { ); } -export const SidebarSpace = styled('div')({ - flex: 1, -}); +export const SidebarSpace = styled('div')( + { + flex: 1, + }, + { name: 'BackstageSidebarSpace' }, +); -export const SidebarSpacer = styled('div')({ - height: 8, -}); +export const SidebarSpacer = styled('div')( + { + height: 8, + }, + { name: 'BackstageSidebarSpacer' }, +); -export const SidebarDivider = styled('hr')({ - height: 1, - width: '100%', - background: '#383838', - border: 'none', - margin: '12px 0px', -}); +export const SidebarDivider = styled('hr')( + { + height: 1, + width: '100%', + background: '#383838', + border: 'none', + margin: '12px 0px', + }, + { name: 'BackstageSidebarDivider' }, +); const styledScrollbar = (theme: Theme): CreateCSSProperties => ({ overflowY: 'auto', diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 21ce590e1c..d92922ddb4 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/core-plugin-api +## 0.1.13 + +### Patch Changes + +- 4a336fd292: Deprecate use of extensions without name. Adds a warning to the developer console to prompt integrators to provide names for extensions. +- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api +- e059aea7b9: Deprecate unused ApiRef types +- Updated dependencies + - @backstage/theme@0.2.13 + +## 0.1.12 + +### Patch Changes + +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + ## 0.1.11 ### Patch Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 88afc0e8e6..2765b02bca 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -5,52 +5,43 @@ ```ts /// +import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api'; 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 { 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'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { Subscription as Subscription_2 } from '@backstage/types'; import { SvgIconProps } from '@material-ui/core'; -// Warning: (ae-missing-release-tag) "AlertApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AlertApi = { post(alert: AlertMessage): void; alert$(): Observable_2; }; -// Warning: (ae-missing-release-tag) "alertApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const alertApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "AlertMessage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AlertMessage = { message: string; severity?: 'success' | 'info' | 'warning' | 'error'; }; -// Warning: (ae-missing-release-tag) "AnalyticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnalyticsApi = { captureEvent(event: AnalyticsEvent): void; }; -// Warning: (ae-missing-release-tag) "analyticsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const analyticsApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "AnalyticsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const AnalyticsContext: ({ attributes, @@ -60,14 +51,10 @@ export const AnalyticsContext: ({ children: ReactNode; }) => JSX.Element; -// Warning: (ae-missing-release-tag) "AnalyticsContextValue" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnalyticsContextValue = CommonAnalyticsContext & AnyAnalyticsContext; -// Warning: (ae-missing-release-tag) "AnalyticsEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnalyticsEvent = { action: string; @@ -77,15 +64,11 @@ export type AnalyticsEvent = { context: AnalyticsContextValue; }; -// Warning: (ae-missing-release-tag) "AnalyticsEventAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnalyticsEventAttributes = { [attribute in string]: string | boolean | number; }; -// Warning: (ae-missing-release-tag) "AnalyticsTracker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnalyticsTracker = { captureEvent: ( @@ -98,16 +81,12 @@ export type AnalyticsTracker = { ) => void; }; -// Warning: (ae-missing-release-tag) "AnyAnalyticsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AnyAnalyticsContext = { [param in string]: string | boolean | number | undefined; }; -// Warning: (ae-missing-release-tag) "AnyApiFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnyApiFactory = ApiFactory< unknown, unknown, @@ -116,14 +95,27 @@ export type AnyApiFactory = ApiFactory< } >; -// Warning: (ae-missing-release-tag) "AnyApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnyApiRef = ApiRef; -// Warning: (ae-missing-release-tag) "ApiFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type AnyExternalRoutes = { + [name: string]: ExternalRouteRef; +}; + +// @public +export type AnyParams = + | { + [param in string]: string; + } + | undefined; + +// @public +export type AnyRoutes = { + [name: string]: RouteRef | SubRouteRef; +}; + +// @public export type ApiFactory< Api, Impl extends Api, @@ -136,24 +128,24 @@ export type ApiFactory< factory(deps: Deps): Impl; }; -// Warning: (ae-missing-release-tag) "ApiHolder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ApiHolder = { get(api: ApiRef): T | undefined; }; -// Warning: (ae-missing-release-tag) "ApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ApiRef = { id: string; T: T; }; -// Warning: (ae-missing-release-tag) "ApiRefsToTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ApiRefConfig = { + id: string; + description?: string; +}; + +// @public @deprecated export type ApiRefsToTypes< T extends { [key in string]: ApiRef; @@ -162,34 +154,27 @@ export type ApiRefsToTypes< [key in keyof T]: ApiRefType; }; -// Warning: (ae-missing-release-tag) "ApiRefType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated export type ApiRefType = T extends ApiRef ? U : never; -// Warning: (ae-missing-release-tag) "AppComponents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; -// Warning: (ae-missing-release-tag) "AppContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppContext = { - getPlugins(): BackstagePlugin[]; - getSystemIcon(key: string): IconComponent | undefined; + getPlugins(): BackstagePlugin_2[]; + getSystemIcon(key: string): IconComponent_2 | undefined; getComponents(): AppComponents; }; -// Warning: (ae-missing-release-tag) "AppTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AppTheme = { id: string; @@ -197,10 +182,9 @@ export type AppTheme = { variant: 'light' | 'dark'; theme: BackstageTheme; icon?: React.ReactElement; + Provider?(props: { children: ReactNode }): JSX.Element | null; }; -// Warning: (ae-missing-release-tag) "AppThemeApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AppThemeApi = { getInstalledThemes(): AppTheme[]; @@ -209,77 +193,56 @@ export type AppThemeApi = { setActiveThemeId(themeId?: string): void; }; -// Warning: (ae-missing-release-tag) "appThemeApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const appThemeApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "atlassianAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "attachComponentData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function attachComponentData

( component: ComponentType

, type: string, data: unknown, ): void; -// Warning: (ae-missing-release-tag) "auth0AuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "AuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AuthProvider = { title: string; icon: IconComponent; }; -// Warning: (ae-missing-release-tag) "AuthRequester" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AuthRequester = ( scopes: Set, ) => Promise; -// Warning: (ae-missing-release-tag) "AuthRequesterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AuthRequesterOptions = { provider: AuthProvider; onAuthRequest(scopes: Set): Promise; }; -// Warning: (ae-missing-release-tag) "AuthRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AuthRequestOptions = { optional?: boolean; instantPopup?: boolean; }; -// 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) +// @public export type BackstageIdentity = { id: string; idToken: string; token: string; }; -// Warning: (tsdoc-undefined-tag) The TSDoc tag "@IdentityApi" is not defined in this configuration -// Warning: (ae-missing-release-tag) "BackstageIdentityApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type BackstageIdentityApi = { getBackstageIdentity( @@ -287,11 +250,7 @@ export type BackstageIdentityApi = { ): Promise; }; -// Warning: (ae-forgotten-export) The symbol "AnyRoutes" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "AnyExternalRoutes" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "BackstagePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BackstagePlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, @@ -304,23 +263,17 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; -// Warning: (ae-missing-release-tag) "bitbucketAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "BootErrorPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; error: Error; }; -// Warning: (ae-missing-release-tag) "CommonAnalyticsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type CommonAnalyticsContext = { pluginId: string; @@ -328,19 +281,21 @@ export type CommonAnalyticsContext = { extension: string; }; -// Warning: (ae-missing-release-tag) "ConfigApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type ComponentLoader = + | { + lazy: () => Promise; + } + | { + sync: T; + }; + // @public export type ConfigApi = Config; -// Warning: (ae-missing-release-tag) "configApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const configApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "createApiFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "createApiFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function createApiFactory< Api, @@ -350,29 +305,21 @@ export function createApiFactory< }, >(factory: ApiFactory): ApiFactory; -// @public (undocumented) +// @public export function createApiFactory( api: ApiRef, instance: Impl, ): ApiFactory; -// Warning: (ae-forgotten-export) The symbol "ApiRefConfig" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createApiRef(config: ApiRefConfig): ApiRef; -// Warning: (ae-missing-release-tag) "createComponentExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createComponentExtension< T extends (props: any) => JSX.Element | null, >(options: { component: ComponentLoader; name?: string }): Extension; -// Warning: (ae-forgotten-export) The symbol "OptionalParams" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createExternalRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createExternalRouteRef< Params extends { [param in ParamKey]: string; @@ -385,9 +332,7 @@ export function createExternalRouteRef< optional?: Optional; }): ExternalRouteRef, Optional>; -// Warning: (ae-missing-release-tag) "createPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createPlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, @@ -395,9 +340,7 @@ export function createPlugin< config: PluginConfig, ): BackstagePlugin; -// Warning: (ae-missing-release-tag) "createReactExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createReactExtension< T extends (props: any) => JSX.Element | null, >(options: { @@ -406,9 +349,7 @@ export function createReactExtension< name?: string; }): Extension; -// Warning: (ae-missing-release-tag) "createRoutableExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createRoutableExtension< T extends (props: any) => JSX.Element | null, >(options: { @@ -417,9 +358,7 @@ export function createRoutableExtension< name?: string; }): Extension; -// Warning: (ae-missing-release-tag) "createRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createRouteRef< Params extends { [param in ParamKey]: string; @@ -433,12 +372,7 @@ export function createRouteRef< title?: string; }): RouteRef>; -// Warning: (ae-forgotten-export) The symbol "AnyParams" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "MakeSubRouteRef" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "PathParams" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createSubRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createSubRouteRef< Path extends string, ParentParams extends AnyParams = never, @@ -448,20 +382,14 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// Warning: (ae-missing-release-tag) "DiscoveryApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; -// Warning: (ae-missing-release-tag) "discoveryApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const discoveryApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "ElementCollection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface ElementCollection { findComponentData(query: { key: string }): T[]; @@ -476,8 +404,14 @@ export interface ElementCollection { }): ElementCollection; } -// Warning: (ae-missing-release-tag) "ErrorApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +type Error_2 = { + name: string; + message: string; + stack?: string; +}; +export { Error_2 as Error }; + // @public export type ErrorApi = { post(error: Error_2, context?: ErrorContext): void; @@ -487,37 +421,27 @@ export type ErrorApi = { }>; }; -// Warning: (ae-missing-release-tag) "errorApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const errorApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "ErrorBoundaryFallbackProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; + plugin?: BackstagePlugin_2; error: Error; resetError: () => void; }; -// Warning: (ae-missing-release-tag) "ErrorContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ErrorContext = { hidden?: boolean; }; -// Warning: (ae-missing-release-tag) "Extension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type Extension = { expose(plugin: BackstagePlugin): T; }; -// Warning: (ae-missing-release-tag) "ExternalRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ExternalRouteRef< Params extends AnyParams = any, Optional extends boolean = any, @@ -527,25 +451,19 @@ export type ExternalRouteRef< optional?: Optional; }; -// Warning: (ae-missing-release-tag) "FeatureFlag" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type FeatureFlag = { name: string; pluginId: string; }; -// Warning: (ae-missing-release-tag) "FeatureFlagOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type FeatureFlagOutput = { type: 'feature-flag'; name: string; }; -// Warning: (ae-missing-release-tag) "FeatureFlagsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface FeatureFlagsApi { getRegisteredFlags(): FeatureFlag[]; isActive(name: string): boolean; @@ -553,60 +471,42 @@ export interface FeatureFlagsApi { save(options: FeatureFlagsSaveOptions): void; } -// Warning: (ae-missing-release-tag) "featureFlagsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const featureFlagsApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "FeatureFlagsHooks" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type FeatureFlagsHooks = { register(name: string): void; }; -// Warning: (ae-missing-release-tag) "FeatureFlagsSaveOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type FeatureFlagsSaveOptions = { states: Record; merge?: boolean; }; -// Warning: (ae-missing-release-tag) "FeatureFlagState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export enum FeatureFlagState { - // (undocumented) Active = 1, - // (undocumented) None = 0, } -// Warning: (ae-missing-release-tag) "getComponentData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function getComponentData( node: ReactNode, type: string, ): T | undefined; -// Warning: (ae-missing-release-tag) "githubAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "gitlabAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "googleAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const googleAuthApiRef: ApiRef< OAuthApi & @@ -616,15 +516,11 @@ export const googleAuthApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "IconComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type IconComponent = ComponentType<{ fontSize?: 'default' | 'small' | 'large'; }>; -// Warning: (ae-missing-release-tag) "IdentityApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type IdentityApi = { getUserId(): string; @@ -633,13 +529,27 @@ export type IdentityApi = { signOut(): Promise; }; -// Warning: (ae-missing-release-tag) "identityApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const identityApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "microsoftAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type MakeSubRouteRef< + Params extends { + [param in string]: string; + }, + ParentParams extends AnyParams, +> = keyof Params & keyof ParentParams extends never + ? SubRouteRef>> + : never; + +// @public +export type MergeParams< + P1 extends { + [param in string]: string; + }, + P2 extends AnyParams, +> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); + // @public export const microsoftAuthApiRef: ApiRef< OAuthApi & @@ -649,8 +559,6 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "oauth2ApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const oauth2ApiRef: ApiRef< OAuthApi & @@ -660,8 +568,6 @@ export const oauth2ApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "OAuthApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type OAuthApi = { getAccessToken( @@ -670,8 +576,6 @@ export type OAuthApi = { ): Promise; }; -// Warning: (ae-missing-release-tag) "OAuthRequestApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type OAuthRequestApi = { createAuthRequester( @@ -680,13 +584,9 @@ export type OAuthRequestApi = { authRequest$(): Observable_2; }; -// Warning: (ae-missing-release-tag) "oauthRequestApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const oauthRequestApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "OAuthScope" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type OAuthScope = string | string[]; @@ -696,8 +596,6 @@ export type Observable = Observable_2; // @public @deprecated export type Observer = Observer_2; -// Warning: (ae-missing-release-tag) "oidcAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const oidcAuthApiRef: ApiRef< OAuthApi & @@ -707,8 +605,6 @@ export const oidcAuthApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "oktaAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -718,9 +614,10 @@ export const oktaAuthApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "oneloginAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type OldIconComponent = ComponentType; + +// @public export const oneloginAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -729,15 +626,39 @@ export const oneloginAuthApiRef: ApiRef< SessionApi >; -// Warning: (ae-missing-release-tag) "OpenIdConnectApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type OpenIdConnectApi = { getIdToken(options?: AuthRequestOptions): Promise; }; -// Warning: (ae-missing-release-tag) "PendingAuthRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type OptionalParams< + Params extends { + [param in string]: string; + }, +> = Params[keyof Params] extends never ? undefined : Params; + +// @public +export type ParamKeys = keyof Params extends never + ? [] + : (keyof Params)[]; + +// @public +export type ParamNames = + S extends `${infer Part}/${infer Rest}` + ? ParamPart | ParamNames + : ParamPart; + +// @public +export type ParamPart = S extends `:${infer Param}` + ? Param + : never; + +// @public +export type PathParams = { + [name in ParamNames]: string; +}; + // @public export type PendingAuthRequest = { provider: AuthProvider; @@ -745,9 +666,7 @@ export type PendingAuthRequest = { trigger(): Promise; }; -// Warning: (ae-missing-release-tag) "PluginConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PluginConfig< Routes extends AnyRoutes, ExternalRoutes extends AnyExternalRoutes, @@ -759,20 +678,14 @@ export type PluginConfig< externalRoutes?: ExternalRoutes; }; -// Warning: (ae-missing-release-tag) "PluginHooks" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PluginHooks = { featureFlags: FeatureFlagsHooks; }; -// Warning: (ae-missing-release-tag) "PluginOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PluginOutput = FeatureFlagOutput; -// 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 type ProfileInfo = { email?: string; @@ -780,28 +693,25 @@ export type ProfileInfo = { picture?: string; }; -// Warning: (ae-missing-release-tag) "ProfileInfoApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ProfileInfoApi = { getProfile(options?: AuthRequestOptions): Promise; }; -// Warning: (ae-missing-release-tag) "RouteOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type RouteFunc = ( + ...[params]: Params extends undefined ? readonly [] : readonly [Params] +) => string; + +// @public export type RouteOptions = { exact?: boolean; }; -// Warning: (ae-missing-release-tag) "RoutePath" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type RoutePath = string; -// Warning: (ae-missing-release-tag) "RouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type RouteRef = { $$routeRefType: 'absolute'; params: ParamKeys; @@ -810,15 +720,11 @@ export type RouteRef = { title?: string; }; -// Warning: (ae-missing-release-tag) "samlAuthApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const samlAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// Warning: (ae-missing-release-tag) "SessionApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SessionApi = { signIn(): Promise; @@ -826,73 +732,44 @@ export type SessionApi = { sessionState$(): Observable_2; }; -// Warning: (ae-missing-release-tag) "SessionState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export enum SessionState { - // (undocumented) SignedIn = 'SignedIn', - // (undocumented) SignedOut = 'SignedOut', } -// Warning: (ae-missing-release-tag) "SignInPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SignInPageProps = { onResult(result: SignInResult): void; }; -// Warning: (ae-missing-release-tag) "SignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SignInResult = { userId: string; - profile: ProfileInfo; + profile: ProfileInfo_2; getIdToken?: () => Promise; signOut?: () => Promise; }; -// Warning: (ae-missing-release-tag) "StorageApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface StorageApi { - // 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}' forBucket(name: string): StorageApi; - // 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-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-undefined-tag) The TSDoc tag "@return" is not defined in this configuration get(key: string): T | undefined; - // 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}' observe$(key: string): Observable_2>; - // 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}' remove(key: string): 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}' set(key: string, data: any): Promise; } -// Warning: (ae-missing-release-tag) "storageApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const storageApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "StorageValueChange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type StorageValueChange = { key: string; newValue?: T; }; -// Warning: (ae-missing-release-tag) "SubRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type SubRouteRef = { $$routeRefType: 'sub'; parent: RouteRef; @@ -903,35 +780,23 @@ export type SubRouteRef = { // @public @deprecated export type Subscription = Subscription_2; -// Warning: (ae-missing-release-tag) "TypesToApiRefs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type TypesToApiRefs = { [key in keyof T]: ApiRef; }; -// Warning: (ae-missing-release-tag) "useAnalytics" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function useAnalytics(): AnalyticsTracker; -// Warning: (ae-missing-release-tag) "useApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function useApi(apiRef: ApiRef): T; -// Warning: (ae-missing-release-tag) "useApiHolder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function useApiHolder(): ApiHolder; -// Warning: (ae-missing-release-tag) "useApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const useApp: () => AppContext; -// Warning: (ae-missing-release-tag) "useElementFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function useElementFilter( node: ReactNode, @@ -939,51 +804,26 @@ export function useElementFilter( dependencies?: any[], ): T; -// Warning: (ae-missing-release-tag) "UserFlags" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type UserFlags = {}; - -// Warning: (ae-forgotten-export) The symbol "RouteFunc" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "useRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "useRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function useRouteRef( routeRef: ExternalRouteRef, ): Optional extends true ? RouteFunc | undefined : RouteFunc; -// @public (undocumented) +// @public export function useRouteRef( routeRef: RouteRef | SubRouteRef, ): RouteFunc; -// Warning: (ae-missing-release-tag) "useRouteRefParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params; -// Warning: (ae-missing-release-tag) "withApis" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function withApis(apis: TypesToApiRefs):

( WrappedComponent: React_2.ComponentType

, ) => { (props: React_2.PropsWithChildren>): JSX.Element; displayName: string; }; - -// Warnings were encountered during analysis: -// -// src/apis/definitions/ErrorApi.d.ts:37:5 - (ae-forgotten-export) The symbol "Error" needs to be exported by the entry point index.d.ts -// src/apis/definitions/auth.d.ts:29:8 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration -// src/apis/definitions/auth.d.ts:38:8 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration -// src/apis/definitions/auth.d.ts:96:68 - (tsdoc-undefined-tag) The TSDoc tag "@AuthRequestOptions" is not defined in this configuration -// src/apis/definitions/auth.d.ts:110:16 - (tsdoc-undefined-tag) The TSDoc tag "@IdentityApi" is not defined in this configuration -// src/apis/definitions/auth.d.ts:113:68 - (tsdoc-undefined-tag) The TSDoc tag "@AuthRequestOptions" is not defined in this configuration -// src/extensions/extensions.d.ts:15:5 - (ae-forgotten-export) The symbol "ComponentLoader" needs to be exported by the entry point index.d.ts -// src/routing/RouteRef.d.ts:35:5 - (ae-forgotten-export) The symbol "OldIconComponent" needs to be exported by the entry point index.d.ts -// src/routing/types.d.ts:30:5 - (ae-forgotten-export) The symbol "ParamKeys" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index c59668a432..6679e89645 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.1.11", + "version": "0.1.13", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.9", - "@backstage/theme": "^0.2.9", + "@backstage/config": "^0.1.11", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -55,7 +55,7 @@ "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx index adbd9be6f3..bb2141d47f 100644 --- a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx @@ -27,7 +27,8 @@ const AnalyticsReactContext = /** * A "private" (to this package) hook that enables context inheritance and a * way to read Analytics Context values at event capture-time. - * @private + * + * @internal */ export const useAnalyticsContext = (): AnalyticsContextValue => { const theContext = useContext(AnalyticsReactContext); @@ -54,8 +55,12 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { * Provides components in the child react tree an Analytics Context, ensuring * all analytics events captured within the context have relevant attributes. * + * @remarks + * * Analytics contexts are additive, meaning the context ultimately emitted with * an event is the combination of all contexts in the parent tree. + * + * @public */ export const AnalyticsContext = ({ attributes, @@ -84,6 +89,7 @@ export const AnalyticsContext = ({ * * @param Component - Component to be wrapped with analytics context attributes * @param values - Analytics context key/value pairs. + * @internal */ export function withAnalyticsContext

( Component: React.ComponentType

, diff --git a/packages/core-plugin-api/src/analytics/types.ts b/packages/core-plugin-api/src/analytics/types.ts index ea6c3b030b..d001c38d3e 100644 --- a/packages/core-plugin-api/src/analytics/types.ts +++ b/packages/core-plugin-api/src/analytics/types.ts @@ -16,6 +16,8 @@ /** * Common analytics context attributes. + * + * @public */ export type CommonAnalyticsContext = { /** @@ -35,7 +37,9 @@ export type CommonAnalyticsContext = { }; /** - * Allow arbitrary scalar values as context attributes too. + * Allows arbitrary scalar values as context attributes too. + * + * @public */ export type AnyAnalyticsContext = { [param in string]: string | boolean | number | undefined; @@ -43,6 +47,8 @@ export type AnyAnalyticsContext = { /** * Analytics context envelope. + * + * @public */ export type AnalyticsContextValue = CommonAnalyticsContext & AnyAnalyticsContext; diff --git a/packages/core-plugin-api/src/analytics/useAnalytics.tsx b/packages/core-plugin-api/src/analytics/useAnalytics.tsx index 02d1ecc3c3..5a047e5a63 100644 --- a/packages/core-plugin-api/src/analytics/useAnalytics.tsx +++ b/packages/core-plugin-api/src/analytics/useAnalytics.tsx @@ -33,7 +33,9 @@ function useAnalyticsApi(): AnalyticsApi { } /** - * Get a pre-configured analytics tracker. + * Gets a pre-configured analytics tracker. + * + * @public */ export function useAnalytics(): AnalyticsTracker { const trackerRef = useRef(null); diff --git a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts index 7110c1d9f8..0c84f7c975 100644 --- a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts @@ -17,6 +17,11 @@ import { createApiRef, ApiRef } from '../system'; import { Observable } from '@backstage/types'; +/** + * Message handled by the {@link AlertApi}. + * + * @public + */ export type AlertMessage = { message: string; // Severity will default to success since that is what material ui defaults the value to. @@ -25,8 +30,9 @@ export type AlertMessage = { /** * The alert API is used to report alerts to the app, and display them to the user. + * + * @public */ - export type AlertApi = { /** * Post an alert for handling by the application. @@ -39,6 +45,11 @@ export type AlertApi = { alert$(): Observable; }; +/** + * The {@link ApiRef} of {@link AlertApi}. + * + * @public + */ export const alertApiRef: ApiRef = createApiRef({ id: 'core.alert', }); diff --git a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts index 43e35607a3..ef190d6c21 100644 --- a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -20,6 +20,8 @@ import { AnalyticsContextValue } from '../../analytics/types'; /** * Represents an event worth tracking in an analytics system that could inform * how users of a Backstage instance are using its features. + * + * @public */ export type AnalyticsEvent = { /** @@ -76,6 +78,8 @@ export type AnalyticsEvent = { /** * A structure allowing other arbitrary metadata to be provided by analytics * event emitters. + * + * @public */ export type AnalyticsEventAttributes = { [attribute in string]: string | boolean | number; @@ -84,6 +88,8 @@ export type AnalyticsEventAttributes = { /** * Represents a tracker with methods that can be called to track events in a * configured analytics service. + * + * @public */ export type AnalyticsTracker = { captureEvent: ( @@ -99,9 +105,13 @@ export type AnalyticsTracker = { /** * The Analytics API is used to track user behavior in a Backstage instance. * + * @remarks + * * To instrument your App or Plugin, retrieve an analytics tracker using the * useAnalytics() hook. This will return a pre-configured AnalyticsTracker * with relevant methods for instrumentation. + * + * @public */ export type AnalyticsApi = { /** @@ -111,6 +121,11 @@ export type AnalyticsApi = { captureEvent(event: AnalyticsEvent): void; }; +/** + * The {@link ApiRef} of {@link AnalyticsApi}. + * + * @public + */ export const analyticsApiRef: ApiRef = createApiRef({ id: 'core.analytics', }); diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index 5ac16f83fd..8053c449a1 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,12 +14,15 @@ * limitations under the License. */ +import { ReactNode } from 'react'; import { ApiRef, createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '@backstage/types'; /** * Describes a theme provided by the app. + * + * @public */ export type AppTheme = { /** @@ -39,6 +42,7 @@ export type AppTheme = { /** * The specialized MaterialUI theme instance. + * @deprecated use Provider instead, see https://backstage.io/docs/api/deprecations#app-theme */ theme: BackstageTheme; @@ -46,11 +50,15 @@ export type AppTheme = { * An Icon for the theme mode setting. */ icon?: React.ReactElement; + + Provider?(props: { children: ReactNode }): JSX.Element | null; }; /** * The AppThemeApi gives access to the current app theme, and allows switching * to other options that have been registered as a part of the App. + * + * @public */ export type AppThemeApi = { /** @@ -76,6 +84,11 @@ export type AppThemeApi = { setActiveThemeId(themeId?: string): void; }; +/** + * The {@link ApiRef} of {@link AppThemeApi}. + * + * @public + */ export const appThemeApiRef: ApiRef = createApiRef({ id: 'core.apptheme', }); diff --git a/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts b/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts index 08b9f91f57..d3bada9e46 100644 --- a/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ConfigApi.ts @@ -19,9 +19,16 @@ import { Config } from '@backstage/config'; /** * The Config API is used to provide a mechanism to access the * runtime configuration of the system. + * + * @public */ export type ConfigApi = Config; +/** + * The {@link ApiRef} of {@link ConfigApi}. + * + * @public + */ export const configApiRef: ApiRef = createApiRef({ id: 'core.config', }); diff --git a/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts index 7082cda9dc..d23fe3db6c 100644 --- a/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -19,6 +19,8 @@ import { ApiRef, createApiRef } from '../system'; * The discovery API is used to provide a mechanism for plugins to * discover the endpoint to use to talk to their backend counterpart. * + * @remarks + * * The purpose of the discovery API is to allow for many different deployment * setups and routing methods through a central configuration, instead * of letting each individual plugin manage that configuration. @@ -26,6 +28,8 @@ import { ApiRef, createApiRef } from '../system'; * Implementations of the discovery API can be a simple as a URL pattern * using the pluginId, but could also have overrides for individual plugins, * or query a separate discovery service. + * + * @public */ export type DiscoveryApi = { /** @@ -41,6 +45,11 @@ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; +/** + * The {@link ApiRef} of {@link DiscoveryApi}. + * + * @public + */ export const discoveryApiRef: ApiRef = createApiRef({ id: 'core.discovery', }); diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 69c331bc06..820660ac52 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -20,8 +20,10 @@ import { Observable } from '@backstage/types'; /** * Mirrors the JavaScript Error class, for the purpose of * providing documentation and optional fields. + * + * @public */ -type Error = { +export type Error = { name: string; message: string; stack?: string; @@ -29,6 +31,8 @@ type Error = { /** * Provides additional information about an error that was posted to the application. + * + * @public */ export type ErrorContext = { // If set to true, this error should not be displayed to the user. Defaults to false. @@ -38,6 +42,8 @@ export type ErrorContext = { /** * The error API is used to report errors to the app, and display them to the user. * + * @remarks + * * Plugins can use this API as a method of displaying errors to the user, but also * to report errors for collection by error reporting services. * @@ -49,6 +55,8 @@ export type ErrorContext = { * if it would be useful to collect or log it for debugging purposes, but with * the hidden flag set. For example, an error arising from form field validation * should probably not be reported, while a failed REST call would be useful to report. + * + * @public */ export type ErrorApi = { /** @@ -62,6 +70,11 @@ export type ErrorApi = { error$(): Observable<{ error: Error; context?: ErrorContext }>; }; +/** + * The {@link ApiRef} of {@link ErrorApi}. + * + * @public + */ export const errorApiRef: ApiRef = createApiRef({ id: 'core.error', }); diff --git a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index 07797bf3f1..ac43bbebe8 100644 --- a/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -17,29 +17,35 @@ import { ApiRef, createApiRef } from '../system'; /** - * The feature flags API is used to toggle functionality to users across plugins and Backstage. + * Fetaure flag descriptor. * - * Plugins can use this API to register feature flags that they have available - * for users to enable/disable, and this API will centralize the current user's - * state of which feature flags they would like to enable. - * - * This is ideal for Backstage plugins, as well as your own App, to trial incomplete - * or unstable upcoming features. Although there will be a common interface for users - * to enable and disable feature flags, this API acts as another way to enable/disable. + * @public */ - export type FeatureFlag = { name: string; pluginId: string; }; +/** + * Enum representing the state of a feature flag (inactive/active). + * + * @public + */ export enum FeatureFlagState { + /** + * Feature flag inactive (disabled). + */ None = 0, + /** + * Feature flag active (enabled). + */ Active = 1, } /** * Options to use when saving feature flags. + * + * @public */ export type FeatureFlagsSaveOptions = { /** @@ -55,8 +61,21 @@ export type FeatureFlagsSaveOptions = { merge?: boolean; }; -export type UserFlags = {}; - +/** + * The feature flags API is used to toggle functionality to users across plugins and Backstage. + * + * @remarks + * + * Plugins can use this API to register feature flags that they have available + * for users to enable/disable, and this API will centralize the current user's + * state of which feature flags they would like to enable. + * + * This is ideal for Backstage plugins, as well as your own App, to trial incomplete + * or unstable upcoming features. Although there will be a common interface for users + * to enable and disable feature flags, this API acts as another way to enable/disable. + * + * @public + */ export interface FeatureFlagsApi { /** * Registers a new feature flag. Once a feature flag has been registered it @@ -80,6 +99,11 @@ export interface FeatureFlagsApi { save(options: FeatureFlagsSaveOptions): void; } +/** + * The {@link ApiRef} of {@link FeatureFlagsApi}. + * + * @public + */ export const featureFlagsApiRef: ApiRef = createApiRef({ id: 'core.featureflags', }); diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 9d7131fed6..9f68a8b2cc 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -18,6 +18,8 @@ import { ProfileInfo } from './auth'; /** * The Identity API used to identify and get information about the signed in user. + * + * @public */ export type IdentityApi = { /** @@ -49,6 +51,11 @@ export type IdentityApi = { signOut(): Promise; }; +/** + * The {@link ApiRef} of {@link IdentityApi}. + * + * @public + */ export const identityApiRef: ApiRef = createApiRef({ id: 'core.identity', }); diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 14609f5b3a..03e129ef6a 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -21,8 +21,12 @@ import { ApiRef, createApiRef } from '../system'; /** * Information about the auth provider that we're requesting a login towards. * + * @remarks + * * This should be shown to the user so that they can be informed about what login is being requested * before a popup is shown. + * + * @public */ export type AuthProvider = { /** @@ -39,6 +43,8 @@ export type AuthProvider = { /** * Describes how to handle auth requests. Both how to show them to the user, and what to do when * the user accesses the auth request. + * + * @public */ export type AuthRequesterOptions = { /** @@ -56,12 +62,16 @@ export type AuthRequesterOptions = { /** * Function used to trigger new auth requests for a set of scopes. * + * @remarks + * * The returned promise will resolve to the same value returned by the onAuthRequest in the - * AuthRequesterOptions. Or rejected, if the request is rejected. + * {@link AuthRequesterOptions}. Or rejected, if the request is rejected. * * This function can be called multiple times before the promise resolves. All calls * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the * union of all requested scopes. + * + * @public */ export type AuthRequester = ( scopes: Set, @@ -71,8 +81,12 @@ export type AuthRequester = ( * An pending auth request for a single auth provider. The request will remain in this pending * state until either reject() or trigger() is called. * + * @remarks + * * Any new requests for the same provider are merged into the existing pending request, meaning * there will only ever be a single pending request for a given provider. + * + * @public */ export type PendingAuthRequest = { /** @@ -95,6 +109,8 @@ export type PendingAuthRequest = { /** * Provides helpers for implemented OAuth login flows within Backstage. + * + * @public */ export type OAuthRequestApi = { /** @@ -127,6 +143,11 @@ export type OAuthRequestApi = { authRequest$(): Observable; }; +/** + * The {@link ApiRef} of {@link OAuthRequestApi}. + * + * @public + */ export const oauthRequestApiRef: ApiRef = createApiRef({ id: 'core.oauthrequest', }); diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index f9a52a0bf4..73a19a1ceb 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -17,48 +17,60 @@ import { ApiRef, createApiRef } from '../system'; import { Observable } from '@backstage/types'; +/** + * Describes a value change event. + * + * @public + */ export type StorageValueChange = { key: string; newValue?: T; }; +/** + * Provides key-value persistence API. + * + * @public + */ export interface StorageApi { /** * Create a bucket to store data in. - * @param {String} name Namespace for the storage to be stored under, + * @param name - Namespace for the storage to be stored under, * will inherit previous namespaces too */ forBucket(name: string): StorageApi; /** * Get the current value for persistent data, use observe$ to be notified of updates. - * - * @param {String} key Unique key associated with the data. - * @return {Object} data The data that should is stored. + * @param key - Unique key associated with the data. */ get(key: string): T | undefined; /** * Remove persistent data. - * - * @param {String} key Unique key associated with the data. + * @param key - Unique key associated with the data. */ remove(key: string): Promise; /** * Save persistent data, and emit messages to anyone that is using observe$ for this key - * - * @param {String} key Unique key associated with the data. + * @param key - Unique key associated with the data. + * @param data - The data to be stored under the key. */ set(key: string, data: any): Promise; /** * Observe changes on a particular key in the bucket - * @param {String} key Unique key associated with the data + * @param key - Unique key associated with the data */ observe$(key: string): Observable>; } +/** + * The {@link ApiRef} of {@link StorageApi}. + * + * @public + */ export const storageApiRef: ApiRef = createApiRef({ id: 'core.storage', }); diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 41c918fa6c..8fe532f3a6 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -32,11 +32,20 @@ import { Observable } from '@backstage/types'; * An array of scopes, or a scope string formatted according to the * auth provider, which is typically a space separated list. * + * @remarks + * * See the documentation for each auth provider for the list of scopes * supported by each provider. + * + * @public */ export type OAuthScope = string | string[]; +/** + * Configuration of an authentication request. + * + * @public + */ export type AuthRequestOptions = { /** * If this is set to true, the user will not be prompted to log in, @@ -45,7 +54,7 @@ export type AuthRequestOptions = { * This can be used to perform a check whether the user is logged in, or if you don't * want to force a user to be logged in, but provide functionality if they already are. * - * @default false + * @defaultValue false */ optional?: boolean; @@ -55,7 +64,7 @@ export type AuthRequestOptions = { * * The method must be called synchronously from a user action for this to work in all browsers. * - * @default false + * @defaultValue false */ instantPopup?: boolean; }; @@ -63,6 +72,8 @@ export type AuthRequestOptions = { /** * This API provides access to OAuth 2 credentials. It lets you request access tokens, * which can be used to act on behalf of the user when talking to APIs. + * + * @public */ export type OAuthApi = { /** @@ -95,6 +106,8 @@ export type OAuthApi = { /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. + * + * @public */ export type OpenIdConnectApi = { /** @@ -113,13 +126,15 @@ export type OpenIdConnectApi = { /** * This API provides access to profile information of the user from an auth provider. + * + * @public */ export type ProfileInfoApi = { /** * Get profile information for the user as supplied by this auth provider. * * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. */ getProfile(options?: AuthRequestOptions): Promise; }; @@ -127,23 +142,32 @@ export type ProfileInfoApi = { /** * This API provides access to the user's identity within Backstage. * + * @remarks + * * An auth provider that implements this interface can be used to sign-in to backstage. It is * not intended to be used directly from a plugin, but instead serves as a connection between - * this authentication method and the app's @IdentityApi + * this authentication method and the app's {@link IdentityApi} + * + * @public */ export type BackstageIdentityApi = { /** * Get the user's identity within Backstage. This should normally not be called directly, - * use the @IdentityApi instead. + * use the {@link IdentityApi} instead. * * If the optional flag is not set, a session is guaranteed to be returned, while if - * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + * the optional flag is set, the session may be undefined. See {@link AuthRequestOptions} for more details. */ getBackstageIdentity( options?: AuthRequestOptions, ): Promise; }; +/** + * A (user id, token) pair. + * + * @public + */ export type BackstageIdentity = { /** * The backstage user ID. @@ -163,6 +187,8 @@ export type BackstageIdentity = { /** * Profile information of the user. + * + * @public */ export type ProfileInfo = { /** @@ -183,14 +209,24 @@ export type ProfileInfo = { /** * Session state values passed to subscribers of the SessionApi. + * + * @public */ export enum SessionState { + /** + * User signed in. + */ SignedIn = 'SignedIn', + /** + * User not signed in. + */ SignedOut = 'SignedOut', } /** * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. + * + * @public */ export type SessionApi = { /** @@ -212,10 +248,14 @@ export type SessionApi = { /** * Provides authentication towards Google APIs and identities. * - * See https://developers.google.com/identity/protocols/googlescopes for a full list of supported scopes. + * @remarks + * + * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. * * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, * email and expiration information. Do not rely on any other fields, as they might not be present. + * + * @public */ export const googleAuthApiRef: ApiRef< OAuthApi & @@ -230,8 +270,12 @@ export const googleAuthApiRef: ApiRef< /** * Provides authentication towards GitHub APIs. * - * See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ + * @remarks + * + * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} * for a full list of supported scopes. + * + * @public */ export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -242,8 +286,12 @@ export const githubAuthApiRef: ApiRef< /** * Provides authentication towards Okta APIs. * - * See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/ + * @remarks + * + * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} * for a full list of supported scopes. + * + * @public */ export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -258,8 +306,12 @@ export const oktaAuthApiRef: ApiRef< /** * Provides authentication towards GitLab APIs. * - * See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token + * @remarks + * + * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} * for a full list of supported scopes. + * + * @public */ export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -270,8 +322,12 @@ export const gitlabAuthApiRef: ApiRef< /** * Provides authentication towards Auth0 APIs. * - * See https://auth0.com/docs/scopes/current/oidc-scopes + * @remarks + * + * See {@link https://auth0.com/docs/scopes/current/oidc-scopes} * for a full list of supported scopes. + * + * @public */ export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -282,9 +338,13 @@ export const auth0AuthApiRef: ApiRef< /** * Provides authentication towards Microsoft APIs and identities. * + * @remarks + * * For more info and a full list of supported scopes, see: - * - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent - * - https://docs.microsoft.com/en-us/graph/permissions-reference + * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} + * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} + * + * @public */ export const microsoftAuthApiRef: ApiRef< OAuthApi & @@ -298,6 +358,8 @@ export const microsoftAuthApiRef: ApiRef< /** * Provides authentication for custom identity providers. + * + * @public */ export const oauth2ApiRef: ApiRef< OAuthApi & @@ -311,6 +373,8 @@ export const oauth2ApiRef: ApiRef< /** * Provides authentication for custom OpenID Connect identity providers. + * + * @public */ export const oidcAuthApiRef: ApiRef< OAuthApi & @@ -323,7 +387,9 @@ export const oidcAuthApiRef: ApiRef< }); /** - * Provides authentication for saml based identity providers + * Provides authentication for SAML-based identity providers. + * + * @public */ export const samlAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -331,6 +397,11 @@ export const samlAuthApiRef: ApiRef< id: 'core.auth.saml', }); +/** + * Provides authentication towards OneLogin APIs. + * + * @public + */ export const oneloginAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -344,8 +415,12 @@ export const oneloginAuthApiRef: ApiRef< /** * Provides authentication towards Bitbucket APIs. * - * See https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/ + * @remarks + * + * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} * for a full list of supported scopes. + * + * @public */ export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -356,8 +431,12 @@ export const bitbucketAuthApiRef: ApiRef< /** * Provides authentication towards Atlassian APIs. * - * See https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/ + * @remarks + * + * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} * for a full list of supported scopes. + * + * @public */ export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi diff --git a/packages/core-plugin-api/src/apis/system/ApiRef.ts b/packages/core-plugin-api/src/apis/system/ApiRef.ts index 37678ed079..7e13a1963f 100644 --- a/packages/core-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/core-plugin-api/src/apis/system/ApiRef.ts @@ -16,6 +16,11 @@ import type { ApiRef } from './types'; +/** + * API reference configuration - holds an ID of the referenced API. + * + * @public + */ export type ApiRefConfig = { id: string; /** @@ -57,6 +62,13 @@ class ApiRefImpl implements ApiRef { } } +/** + * Creates a reference to an API. + * + * @param config - The descriptor of the API to reference. + * @returns An API reference. + * @public + */ export function createApiRef(config: ApiRefConfig): ApiRef { return new ApiRefImpl(config); } diff --git a/packages/core-plugin-api/src/apis/system/helpers.ts b/packages/core-plugin-api/src/apis/system/helpers.ts index 547d583482..eaffed3003 100644 --- a/packages/core-plugin-api/src/apis/system/helpers.ts +++ b/packages/core-plugin-api/src/apis/system/helpers.ts @@ -17,19 +17,44 @@ import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; /** - * Used to infer types for a standalone ApiFactory that isn't immediately passed + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed * to another function. + * + * @remarks + * * This function doesn't actually do anything, it's only used to infer types. + * + * @public */ export function createApiFactory< Api, Impl extends Api, Deps extends { [name in string]: unknown }, >(factory: ApiFactory): ApiFactory; +/** + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed + * to another function. + * + * @param api - Ref of the API that will be produced by the factory. + * @param instance - Implementation of the API to use. + * @public + */ export function createApiFactory( api: ApiRef, instance: Impl, ): ApiFactory; +/** + * Used to infer types for a standalone {@link ApiFactory} that isn't immediately passed + * to another function. + * + * @remarks + * + * Creates factory from {@link ApiRef} or returns the factory itself if provided. + * + * @param factory - Existing factory or {@link ApiRef}. + * @param instance - The instance to be returned by the factory. + * @public + */ export function createApiFactory< Api, Impl extends Api, diff --git a/packages/core-plugin-api/src/apis/system/index.ts b/packages/core-plugin-api/src/apis/system/index.ts index ceada1ebd7..964750474e 100644 --- a/packages/core-plugin-api/src/apis/system/index.ts +++ b/packages/core-plugin-api/src/apis/system/index.ts @@ -16,5 +16,6 @@ export { useApi, useApiHolder, withApis } from './useApi'; export { createApiRef } from './ApiRef'; +export type { ApiRefConfig } from './ApiRef'; export * from './types'; export * from './helpers'; diff --git a/packages/core-plugin-api/src/apis/system/types.ts b/packages/core-plugin-api/src/apis/system/types.ts index a97d29fdaa..a449e83450 100644 --- a/packages/core-plugin-api/src/apis/system/types.ts +++ b/packages/core-plugin-api/src/apis/system/types.ts @@ -14,25 +14,64 @@ * limitations under the License. */ +/** + * API reference. + * + * @public + */ export type ApiRef = { id: string; T: T; }; +/** + * Catch-all {@link ApiRef} type. + * + * @public + */ export type AnyApiRef = ApiRef; +/** + * Transforms ApiRef type into its inner API type. + * + * @public + * @deprecated unused type. + */ export type ApiRefType = T extends ApiRef ? U : never; +/** + * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. + * Reverse type transform of {@link ApiRefsToTypes}. + * + * @public + */ export type TypesToApiRefs = { [key in keyof T]: ApiRef }; +/** + * Unwraps type with {@link ApiRef} properties into a type holding their respective API types. + * Reverse type transform of {@link TypesToApiRefs}. + * + * @public + * @deprecated unused type. + */ export type ApiRefsToTypes }> = { [key in keyof T]: ApiRefType; }; +/** + * Provides lookup of APIs through their {@link ApiRef}s. + * + * @public + */ export type ApiHolder = { get(api: ApiRef): T | undefined; }; +/** + * Describes type returning API implementations. + * + * @public + */ export type ApiFactory< Api, Impl extends Api, @@ -43,6 +82,11 @@ export type ApiFactory< factory(deps: Deps): Impl; }; +/** + * Catch-all {@link ApiFactory} type. + * + * @public + */ export type AnyApiFactory = ApiFactory< unknown, unknown, diff --git a/packages/core-plugin-api/src/apis/system/useApi.tsx b/packages/core-plugin-api/src/apis/system/useApi.tsx index ef598a4c0e..465a39cff4 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.tsx @@ -18,6 +18,11 @@ import React, { PropsWithChildren } from 'react'; import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { useVersionedContext } from '@backstage/version-bridge'; +/** + * React hook for retrieving {@link ApiHolder}, an API catalog. + * + * @public + */ export function useApiHolder(): ApiHolder { const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); if (!versionedHolder) { @@ -31,6 +36,12 @@ export function useApiHolder(): ApiHolder { return apiHolder; } +/** + * React hook for retrieving APIs. + * + * @param apiRef - Reference of the API to use. + * @public + */ export function useApi(apiRef: ApiRef): T { const apiHolder = useApiHolder(); @@ -41,6 +52,12 @@ export function useApi(apiRef: ApiRef): T { return api; } +/** + * Wrapper for giving component an API context. + * + * @param apis - APIs for the context. + * @public + */ export function withApis(apis: TypesToApiRefs) { return function withApisWrapper

( WrappedComponent: React.ComponentType

, diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index 8b07d699ee..df78728a21 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -14,80 +14,15 @@ * limitations under the License. */ -import { ComponentType } from 'react'; -import { ProfileInfo } from '../apis/definitions'; -import { IconComponent } from '../icons'; -import { BackstagePlugin } from '../plugin/types'; - -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; - error: Error; - resetError: () => void; -}; - -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - ErrorBoundaryFallback: ComponentType; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -export type AppContext = { - /** - * Get a list of all plugins that are installed in the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; -}; +// This is a bit of a hack that we use to avoid having to redeclare these types +// within this package or have an explicit dependency on core-app-api. +// These types end up being inlined and duplicated into this package at build time. +// eslint-disable-next-line no-restricted-imports +export type { + BootErrorPageProps, + SignInResult, + SignInPageProps, + ErrorBoundaryFallbackProps, + AppComponents, + AppContext, +} from '../../../core-app-api/src/app/types'; diff --git a/packages/core-plugin-api/src/app/useApp.tsx b/packages/core-plugin-api/src/app/useApp.tsx index 05d2e1c5c0..f2175a0969 100644 --- a/packages/core-plugin-api/src/app/useApp.tsx +++ b/packages/core-plugin-api/src/app/useApp.tsx @@ -17,6 +17,11 @@ import { useVersionedContext } from '@backstage/version-bridge'; import { AppContext as AppContextV1 } from './types'; +/** + * React hook providing {@link AppContext}. + * + * @public + */ export const useApp = (): AppContextV1 => { const versionedContext = useVersionedContext<{ 1: AppContextV1 }>('app-context'); diff --git a/packages/core-plugin-api/src/deprecatedTypes.ts b/packages/core-plugin-api/src/deprecatedTypes.ts index 4b24614e17..5596ff9f46 100644 --- a/packages/core-plugin-api/src/deprecatedTypes.ts +++ b/packages/core-plugin-api/src/deprecatedTypes.ts @@ -39,8 +39,9 @@ export type Subscription = CoreSubscription; /** * Observable sequence of values and errors, see TC39. * - * https://github.com/tc39/proposal-observable + * @remarks * + * {@link https://github.com/tc39/proposal-observable} * This is used as a common return type for observable values and can be created * using many different observable implementations, such as zen-observable or RxJS 5. * diff --git a/packages/core-plugin-api/src/extensions/componentData.tsx b/packages/core-plugin-api/src/extensions/componentData.tsx index 97a0b74e5b..1f7ef7f06a 100644 --- a/packages/core-plugin-api/src/extensions/componentData.tsx +++ b/packages/core-plugin-api/src/extensions/componentData.tsx @@ -41,6 +41,18 @@ type MaybeComponentNode = ReactNode & { type?: ComponentWithData; }; +/** + * Stores data related to a component in a global store. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#component-data}. + * + * @param component - The component to attach the data to. + * @param type - The key under which the data will be stored. + * @param data - Arbitrary value. + * @public + */ export function attachComponentData

( component: ComponentType

, type: string, @@ -70,6 +82,18 @@ export function attachComponentData

( container.map.set(type, data); } +/** + * Retrieves data attached to a component. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#component-data}. + * + * @param node - React component to look up. + * @param type - Key of the data to retrieve. + * @returns Data stored using {@link attachComponentData}. + * @public + */ export function getComponentData( node: ReactNode, type: string, diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index 29c696c71f..dcc92d9d47 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -41,6 +41,7 @@ describe('extensions', () => { const Component = () =>

; const extension = createReactExtension({ + name: 'Extension', component: { sync: Component, }, @@ -67,6 +68,7 @@ describe('extensions', () => { }); const extension2 = createRoutableExtension({ + name: 'Extension2', component: () => Promise.resolve(Component), mountPoint: routeRef, }); diff --git a/packages/core-plugin-api/src/extensions/extensions.tsx b/packages/core-plugin-api/src/extensions/extensions.tsx index 2eb5275c66..ca5ada8478 100644 --- a/packages/core-plugin-api/src/extensions/extensions.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.tsx @@ -22,7 +22,12 @@ import { attachComponentData } from './componentData'; import { Extension, BackstagePlugin } from '../plugin/types'; import { PluginErrorBoundary } from './PluginErrorBoundary'; -type ComponentLoader = +/** + * Lazy or synchronous retrieving of extension components. + * + * @public + */ +export type ComponentLoader = | { lazy: () => Promise; } @@ -30,9 +35,19 @@ type ComponentLoader = sync: T; }; -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. +/** + * Extension for components that can have its own URL route (top-level pages, tabs etc.). + * + * @remarks + * + * We do not use ComponentType as the return type, since it doesn't let us convey the children prop. + * ComponentType inserts children as an optional prop whether the inner component accepts it or not, + * making it impossible to make the usage of children type safe. + * + * See {@link https://backstage.io/docs/plugins/composability#extensions}. + * + * @public + */ export function createRoutableExtension< T extends (props: any) => JSX.Element | null, >(options: { @@ -40,7 +55,7 @@ export function createRoutableExtension< mountPoint: RouteRef; name?: string; }): Extension { - const { component, mountPoint } = options; + const { component, mountPoint, name } = options; return createReactExtension({ component: { lazy: () => @@ -70,7 +85,7 @@ export function createRoutableExtension< }; const componentName = - options.name || + name || (InnerComponent as { displayName?: string }).displayName || InnerComponent.name || 'LazyComponent'; @@ -93,13 +108,23 @@ export function createRoutableExtension< data: { 'core.mountPoint': mountPoint, }, - name: options.name, + name, }); } -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. +/** + * Plain React component extension. + * + * @remarks + * + * We do not use ComponentType as the return type, since it doesn't let us convey the children prop. + * ComponentType inserts children as an optional prop whether the inner component accepts it or not, + * making it impossible to make the usage of children type safe. + * + * See {@link https://backstage.io/docs/plugins/composability#extensions}. + * + * @public + */ export function createComponentExtension< T extends (props: any) => JSX.Element | null, >(options: { component: ComponentLoader; name?: string }): Extension { @@ -107,9 +132,19 @@ export function createComponentExtension< return createReactExtension({ component, name }); } -// We do not use ComponentType as the return type, since it doesn't let us convey the children prop. -// ComponentType inserts children as an optional prop whether the inner component accepts it or not, -// making it impossible to make the usage of children type safe. +/** + * Used by {@link createComponentExtension} and {@link createRoutableExtension}. + * + * @remarks + * + * We do not use ComponentType as the return type, since it doesn't let us convey the children prop. + * ComponentType inserts children as an optional prop whether the inner component accepts it or not, + * making it impossible to make the usage of children type safe. + * + * See {@link https://backstage.io/docs/plugins/composability#extensions}. + * + * @public + */ export function createReactExtension< T extends (props: any) => JSX.Element | null, >(options: { @@ -117,7 +152,14 @@ export function createReactExtension< data?: Record; name?: string; }): Extension { - const { data = {} } = options; + const { data = {}, name } = options; + if (!name) { + // eslint-disable-next-line no-console + console.warn( + 'Declaring extensions without name is DEPRECATED. ' + + 'Make sure that all usages of createReactExtension, createComponentExtension and createRoutableExtension provide a name.', + ); + } let Component: T; if ('lazy' in options.component) { @@ -129,7 +171,7 @@ export function createReactExtension< Component = options.component.sync; } const componentName = - options.name || + name || (Component as { displayName?: string }).displayName || Component.name || 'Component'; @@ -151,7 +193,7 @@ export function createReactExtension< diff --git a/packages/core-plugin-api/src/extensions/index.ts b/packages/core-plugin-api/src/extensions/index.ts index 39db17311a..35803bdeee 100644 --- a/packages/core-plugin-api/src/extensions/index.ts +++ b/packages/core-plugin-api/src/extensions/index.ts @@ -20,5 +20,6 @@ export { createRoutableExtension, createComponentExtension, } from './extensions'; +export type { ComponentLoader } from './extensions'; export { useElementFilter } from './useElementFilter'; export type { ElementCollection } from './useElementFilter'; diff --git a/packages/core-plugin-api/src/extensions/useElementFilter.tsx b/packages/core-plugin-api/src/extensions/useElementFilter.tsx index 7efdd949b2..fa04e7c2c6 100644 --- a/packages/core-plugin-api/src/extensions/useElementFilter.tsx +++ b/packages/core-plugin-api/src/extensions/useElementFilter.tsx @@ -82,17 +82,23 @@ function selectChildren( * A querying interface tailored to traversing a set of selected React elements * and extracting data. * + * @remarks + * * Methods prefixed with `selectBy` are used to narrow the set of selected elements. * * Methods prefixed with `find` return concrete data using a deep traversal of the set. * * Methods prefixed with `get` return concrete data using a shallow traversal of the set. + * + * @public */ export interface ElementCollection { /** * Narrows the set of selected components by doing a deep traversal and * only including those that have defined component data for the given `key`. * + * @remarks + * * Whether an element in the tree has component data set for the given key * is determined by whether `getComponentData` returns undefined. * @@ -104,6 +110,8 @@ export interface ElementCollection { * If `withStrictError` is set, the resulting selection must be a full match, meaning * there may be no elements that were excluded in the selection. If the selection * is not a clean match, an error will be throw with `withStrictError` as the message. + * + * @param query - Filtering query. */ selectByComponentData(query: { key: string; @@ -113,6 +121,8 @@ export interface ElementCollection { /** * Finds all elements using the same criteria as `selectByComponentData`, but * returns the actual component data of each of those elements instead. + * + * @param query - Lookup query. */ findComponentData(query: { key: string }): T[]; @@ -163,7 +173,11 @@ class Collection implements ElementCollection { /** * useElementFilter is a utility that helps you narrow down and retrieve data * from a React element tree, typically operating on the `children` property - * passed in to a component. A common use-case is to construct declarative APIs + * passed in to a component. + * + * @remarks + * + * A common use-case is to construct declarative APIs * where a React component defines its behavior based on its children, such as * the relationship between `Routes` and `Route` in `react-router`. * @@ -175,6 +189,8 @@ class Collection implements ElementCollection { * with added memoization based on the input `node`. If further memoization * dependencies are used in the filter function, they should be added to the * third `dependencies` argument, just like `useMemo`, `useEffect`, etc. + * + * @public */ export function useElementFilter( node: ReactNode, diff --git a/packages/core-plugin-api/src/icons/index.ts b/packages/core-plugin-api/src/icons/index.ts index 9c9e45e54c..de79dd08d3 100644 --- a/packages/core-plugin-api/src/icons/index.ts +++ b/packages/core-plugin-api/src/icons/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { IconComponent } from './types'; +export type { IconComponent, OldIconComponent } from './types'; diff --git a/packages/core-plugin-api/src/icons/types.ts b/packages/core-plugin-api/src/icons/types.ts index ccc1c337e7..5648261f3c 100644 --- a/packages/core-plugin-api/src/icons/types.ts +++ b/packages/core-plugin-api/src/icons/types.ts @@ -21,6 +21,8 @@ import { SvgIconProps } from '@material-ui/core'; * IconComponent is the common icon type used throughout Backstage when * working with and rendering generic icons, including the app system icons. * + * @remarks + * * The type is based on SvgIcon from MUI, but both do not what the plugin-api * package to have a dependency on MUI, nor do we want the props to be as broad * as the SvgIconProps interface. @@ -28,6 +30,8 @@ import { SvgIconProps } from '@material-ui/core'; * If you have the need to forward additional props from SvgIconProps, you can * open an issue or submit a PR to the main Backstage repo. When doing so please * also describe your use-case and reasoning of the addition. + * + * @public */ export type IconComponent = ComponentType<{ fontSize?: 'default' | 'small' | 'large'; @@ -37,5 +41,7 @@ export type IconComponent = ComponentType<{ * This exists for backwards compatibility with the old core package. * It's used in some parts of this package in order to smooth out the * migration, but it is not exported. + * + * @public */ export type OldIconComponent = ComponentType; diff --git a/packages/core-plugin-api/src/plugin/Plugin.tsx b/packages/core-plugin-api/src/plugin/Plugin.tsx index 53a53b76d0..52ca0e76e3 100644 --- a/packages/core-plugin-api/src/plugin/Plugin.tsx +++ b/packages/core-plugin-api/src/plugin/Plugin.tsx @@ -24,6 +24,9 @@ import { } from './types'; import { AnyApiFactory } from '../apis'; +/** + * @internal + */ export class PluginImpl< Routes extends AnyRoutes, ExternalRoutes extends AnyExternalRoutes, @@ -80,6 +83,12 @@ export class PluginImpl< } } +/** + * Creates Backstage Plugin from config. + * + * @param config - Plugin configuration. + * @public + */ export function createPlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, diff --git a/packages/core-plugin-api/src/plugin/index.ts b/packages/core-plugin-api/src/plugin/index.ts index 321ae6998a..0cb9a2aede 100644 --- a/packages/core-plugin-api/src/plugin/index.ts +++ b/packages/core-plugin-api/src/plugin/index.ts @@ -16,6 +16,8 @@ export { createPlugin } from './Plugin'; export type { + AnyExternalRoutes, + AnyRoutes, BackstagePlugin, Extension, FeatureFlagOutput, diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index b3e1043efd..192e771092 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -17,29 +17,72 @@ import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing'; import { AnyApiFactory } from '../apis/system'; +/** + * Route configuration. + * + * @public + */ export type RouteOptions = { // Whether the route path must match exactly, defaults to true. exact?: boolean; }; +/** + * Type alias for paths. + * + * @public + */ export type RoutePath = string; -// Replace with using RouteRefs +/** + * Replace with using {@link RouteRef}s. + * + * @public + */ export type FeatureFlagOutput = { type: 'feature-flag'; name: string; }; +/** + * {@link FeatureFlagOutput} type. + * + * @public + */ export type PluginOutput = FeatureFlagOutput; +/** + * Plugin extension type. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#extensions}. + * + * @public + */ export type Extension = { expose(plugin: BackstagePlugin): T; }; +/** + * Catch-all route type. + * + * @public + */ export type AnyRoutes = { [name: string]: RouteRef | SubRouteRef }; +/** + * Catch-all type for {@link ExternalRouteRef}s. + * + * @public + */ export type AnyExternalRoutes = { [name: string]: ExternalRouteRef }; +/** + * Plugin type. + * + * @public + */ export type BackstagePlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, @@ -52,6 +95,11 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; +/** + * Plugin descriptor type. + * + * @public + */ export type PluginConfig< Routes extends AnyRoutes, ExternalRoutes extends AnyExternalRoutes, @@ -63,10 +111,20 @@ export type PluginConfig< externalRoutes?: ExternalRoutes; }; +/** + * Holds hooks registered by the plugin. + * + * @public + */ export type PluginHooks = { featureFlags: FeatureFlagsHooks; }; +/** + * Interface for registering feature flags hooks. + * + * @public + */ export type FeatureFlagsHooks = { register(name: string): void; }; diff --git a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts index c1c17c86df..2ebb81dbfb 100644 --- a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts @@ -22,6 +22,9 @@ import { OptionalParams, } from './types'; +/** + * @internal + */ export class ExternalRouteRefImpl< Params extends AnyParams, Optional extends boolean, @@ -42,6 +45,16 @@ export class ExternalRouteRefImpl< } } +/** + * Creates a route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @param options - Description of the route reference to be created. + * @public + */ export function createExternalRouteRef< Params extends { [param in ParamKey]: string }, Optional extends boolean = false, diff --git a/packages/core-plugin-api/src/routing/RouteRef.ts b/packages/core-plugin-api/src/routing/RouteRef.ts index 78211e29c6..a926b775e6 100644 --- a/packages/core-plugin-api/src/routing/RouteRef.ts +++ b/packages/core-plugin-api/src/routing/RouteRef.ts @@ -24,6 +24,10 @@ import { import { OldIconComponent } from '../icons/types'; // TODO(Rugvip): Remove this in the next breaking release, it's exported but unused +/** + * @deprecated + * @internal + */ export type RouteRefConfig = { params?: ParamKeys; path?: string; @@ -31,6 +35,9 @@ export type RouteRefConfig = { title: string; }; +/** + * @internal + */ export class RouteRefImpl implements RouteRef { @@ -66,6 +73,12 @@ export class RouteRefImpl } } +/** + * Create a {@link RouteRef} from a route descriptor. + * + * @param config - Description of the route reference to be created. + * @public + */ export function createRouteRef< // Params is the type that we care about and the one to be embedded in the route ref. // For example, given the params ['name', 'kind'], Params will be {name: string, kind: string} diff --git a/packages/core-plugin-api/src/routing/SubRouteRef.ts b/packages/core-plugin-api/src/routing/SubRouteRef.ts index be30d56096..61d89fcbdb 100644 --- a/packages/core-plugin-api/src/routing/SubRouteRef.ts +++ b/packages/core-plugin-api/src/routing/SubRouteRef.ts @@ -26,6 +26,9 @@ import { // Should match the pattern in react-router const PARAM_PATTERN = /^\w+$/; +/** + * @internal + */ export class SubRouteRefImpl implements SubRouteRef { @@ -45,18 +48,34 @@ export class SubRouteRefImpl } } -// These utility types help us infer a Param object type from a string path -// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }` -type ParamPart = S extends `:${infer Param}` ? Param : never; -type ParamNames = S extends `${infer Part}/${infer Rest}` - ? ParamPart | ParamNames - : ParamPart; -type PathParams = { [name in ParamNames]: string }; +/** + * Used in {@link PathParams} type declaration. + * @public + */ +export type ParamPart = S extends `:${infer Param}` + ? Param + : never; /** - * Merges a param object type with with an optional params type into a params object + * Used in {@link PathParams} type declaration. + * @public */ -type MergeParams< +export type ParamNames = + S extends `${infer Part}/${infer Rest}` + ? ParamPart | ParamNames + : ParamPart; +/** + * This utility type helps us infer a Param object type from a string path + * For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }` + * @public + */ +export type PathParams = { [name in ParamNames]: string }; + +/** + * Merges a param object type with with an optional params type into a params object. + * @public + */ +export type MergeParams< P1 extends { [param in string]: string }, P2 extends AnyParams, > = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); @@ -64,14 +83,22 @@ type MergeParams< /** * Creates a SubRouteRef type given the desired parameters and parent route parameters. * The parameters types are merged together while ensuring that there is no overlap between the two. + * + * @public */ -type MakeSubRouteRef< +export type MakeSubRouteRef< Params extends { [param in string]: string }, ParentParams extends AnyParams, > = keyof Params & keyof ParentParams extends never ? SubRouteRef>> : never; +/** + * Create a {@link SubRouteRef} from a route descriptor. + * + * @param config - Description of the route reference to be created. + * @public + */ export function createSubRouteRef< Path extends string, ParentParams extends AnyParams = never, diff --git a/packages/core-plugin-api/src/routing/index.ts b/packages/core-plugin-api/src/routing/index.ts index 79158d2cdc..01d69cd4b0 100644 --- a/packages/core-plugin-api/src/routing/index.ts +++ b/packages/core-plugin-api/src/routing/index.ts @@ -14,9 +14,24 @@ * limitations under the License. */ -export type { RouteRef, SubRouteRef, ExternalRouteRef } from './types'; +export type { + AnyParams, + RouteRef, + SubRouteRef, + ExternalRouteRef, + OptionalParams, + ParamKeys, + RouteFunc, +} from './types'; export { createRouteRef } from './RouteRef'; export { createSubRouteRef } from './SubRouteRef'; +export type { + MakeSubRouteRef, + MergeParams, + ParamNames, + ParamPart, + PathParams, +} from './SubRouteRef'; export { createExternalRouteRef } from './ExternalRouteRef'; export { useRouteRef } from './useRouteRef'; export { useRouteRefParams } from './useRouteRefParams'; diff --git a/packages/core-plugin-api/src/routing/types.ts b/packages/core-plugin-api/src/routing/types.ts index 7e20d1ff67..bd419abb76 100644 --- a/packages/core-plugin-api/src/routing/types.ts +++ b/packages/core-plugin-api/src/routing/types.ts @@ -17,32 +17,70 @@ import { OldIconComponent } from '../icons/types'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; +/** + * Catch-all type for route params. + * + * @public + */ export type AnyParams = { [param in string]: string } | undefined; + +/** + * Type describing the key type of a route parameter mapping. + * + * @public + */ export type ParamKeys = keyof Params extends never ? [] : (keyof Params)[]; + +/** + * Optional route params. + * + * @public + */ export type OptionalParams = Params[keyof Params] extends never ? undefined : Params; -// The extra TS magic here is to require a single params argument if the RouteRef -// had at least one param defined, but require 0 arguments if there are no params defined. -// Without this we'd have to pass in empty object to all parameter-less RouteRefs -// just to make TypeScript happy, or we would have to make the argument optional in -// which case you might forget to pass it in when it is actually required. +/** + * TS magic for handling route parameters. + * + * @remarks + * + * The extra TS magic here is to require a single params argument if the RouteRef + * had at least one param defined, but require 0 arguments if there are no params defined. + * Without this we'd have to pass in empty object to all parameter-less RouteRefs + * just to make TypeScript happy, or we would have to make the argument optional in + * which case you might forget to pass it in when it is actually required. + * + * @public + */ export type RouteFunc = ( ...[params]: Params extends undefined ? readonly [] : readonly [Params] ) => string; -// This symbol is what we use at runtime to determine whether a given object -// is a type of RouteRef or not. It doesn't work well in TypeScript though since -// the `unique symbol` will refer to different values between package versions. -// For that reason we use the marker $$routeRefType to represent the symbol at -// compile-time instead of using the symbol directly. +/** + * This symbol is what we use at runtime to determine whether a given object + * is a type of RouteRef or not. It doesn't work well in TypeScript though since + * the `unique symbol` will refer to different values between package versions. + * For that reason we use the marker $$routeRefType to represent the symbol at + * compile-time instead of using the symbol directly. + * + * @internal + */ export const routeRefType: unique symbol = getOrCreateGlobalSingleton( 'route-ref-type', () => Symbol('route-ref-type'), ); +/** + * Absolute route reference. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @public + */ export type RouteRef = { $$routeRefType: 'absolute'; // See routeRefType above @@ -57,6 +95,15 @@ export type RouteRef = { title?: string; }; +/** + * Descriptor of a route relative to an absolute {@link RouteRef}. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @public + */ export type SubRouteRef = { $$routeRefType: 'sub'; // See routeRefType above @@ -67,6 +114,15 @@ export type SubRouteRef = { params: ParamKeys; }; +/** + * Route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system}. + * + * @public + */ export type ExternalRouteRef< Params extends AnyParams = any, Optional extends boolean = any, @@ -78,20 +134,35 @@ export type ExternalRouteRef< optional?: Optional; }; +/** + * @internal + */ export type AnyRouteRef = | RouteRef | SubRouteRef | ExternalRouteRef; // TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release -/** @deprecated */ +/** + * @deprecated + * @internal + */ export type ConcreteRoute = {}; -/** @deprecated */ +/** + * @deprecated + * @internal + */ export type AbsoluteRouteRef = RouteRef<{}>; -/** @deprecated */ +/** + * @deprecated + * @internal + */ export type MutableRouteRef = RouteRef<{}>; -// A duplicate of the react-router RouteObject, but with routeRef added +/** + * A duplicate of the react-router RouteObject, but with routeRef added + * @internal + */ export interface BackstageRouteObject { caseSensitive: boolean; children?: BackstageRouteObject[]; diff --git a/packages/core-plugin-api/src/routing/useRouteRef.tsx b/packages/core-plugin-api/src/routing/useRouteRef.tsx index ea05cc14fd..50cfb3bdfb 100644 --- a/packages/core-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/core-plugin-api/src/routing/useRouteRef.tsx @@ -25,6 +25,9 @@ import { SubRouteRef, } from './types'; +/** + * @internal + */ export interface RouteResolver { resolve( anyRouteRef: @@ -35,12 +38,47 @@ export interface RouteResolver { ): RouteFunc | undefined; } +/** + * React hook for constructing URLs to routes. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system} + * + * @param routeRef - The ref to route that should be converted to URL. + * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @public + */ export function useRouteRef( routeRef: ExternalRouteRef, ): Optional extends true ? RouteFunc | undefined : RouteFunc; + +/** + * React hook for constructing URLs to routes. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system} + * + * @param routeRef - The ref to route that should be converted to URL. + * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @public + */ export function useRouteRef( routeRef: RouteRef | SubRouteRef, ): RouteFunc; + +/** + * React hook for constructing URLs to routes. + * + * @remarks + * + * See {@link https://backstage.io/docs/plugins/composability#routing-system} + * + * @param routeRef - The ref to route that should be converted to URL. + * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @public + */ export function useRouteRef( routeRef: | RouteRef diff --git a/packages/core-plugin-api/src/routing/useRouteRefParams.ts b/packages/core-plugin-api/src/routing/useRouteRefParams.ts index a61df97cf5..e24576372b 100644 --- a/packages/core-plugin-api/src/routing/useRouteRefParams.ts +++ b/packages/core-plugin-api/src/routing/useRouteRefParams.ts @@ -17,6 +17,11 @@ import { useParams } from 'react-router-dom'; import { RouteRef, AnyParams, SubRouteRef } from './types'; +/** + * React hook for retrieving dynamic params from the current URL. + * @param _routeRef - Ref of the current route. + * @public + */ export function useRouteRefParams( _routeRef: RouteRef | SubRouteRef, ): Params { diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 070d558124..7e53d0cde5 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/create-app +## 0.4.3 + +### Patch Changes + +- 5dcea2586c: Integrated `SidebarSearchModal` component into default-app to use the `SearchModal`. + + The `SidebarSearchModal` component can also be used in other generated apps: + + ```diff + import { + - SidebarSearch, + + SidebarSearchModal + } from '@backstage/plugin-search'; + ... + + + + - + + + + ... + ``` + + If you only want to use the `SearchModal` you can import it from `'@backstage/plugin-search'`: + + ```js + import { SearchModal } from '@backstage/plugin-search'; + ``` + +- 5725f87e4c: Updated the app template to no longer include the `--no-private` flag for the `create-plugin` command. + + To apply this change to an existing application, remove the `--no-private` flag from the `create-plugin` command in the root `package.json`: + + ```diff + "prettier:check": "prettier --check .", + - "create-plugin": "backstage-cli create-plugin --scope internal --no-private", + + "create-plugin": "backstage-cli create-plugin --scope internal", + "remove-plugin": "backstage-cli remove-plugin" + ``` + +- 1921f70aa7: Removed the version pinning of the packages `graphql-language-service-interface` and `graphql-language-service-parser`. This should no longer be necessary. + + You can apply the same change in your repository by ensuring that the following does _NOT_ appear in your root `package.json`. + + ```json + "resolutions": { + "graphql-language-service-interface": "2.8.2", + "graphql-language-service-parser": "1.9.0" + }, + ``` + +## 0.4.2 + +## 0.4.1 + +### Patch Changes + +- 4f1c30c176: Support optional path argument when generating a project using `create-app` +- Updated dependencies + - @backstage/cli-common@0.1.5 + ## 0.4.0 ### Minor Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 6cf606ed49..2820d41ed6 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.0", + "version": "0.4.3", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.4", + "@backstage/cli-common": "^0.1.5", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "9.1.0", diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 562ebf0e22..d9719742fb 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -27,7 +27,7 @@ import { Task, templatingTask } from './lib/tasks'; const exec = promisify(execCb); -async function checkExists(rootDir: string, name: string) { +async function checkAppExists(rootDir: string, name: string) { await Task.forItem('checking', name, async () => { const destination = resolvePath(rootDir, name); @@ -40,6 +40,17 @@ async function checkExists(rootDir: string, name: string) { }); } +async function checkPathExists(path: string) { + await Task.forItem('checking', path, async () => { + try { + await fs.mkdirs(path); + } catch (error) { + // will fail if a file already exists at given `path` + throw new Error(`Failed to create app directory: ${error.message}`); + } + }); +} + async function createTemporaryAppFolder(tempDir: string) { await Task.forItem('creating', 'temporary directory', async () => { try { @@ -50,12 +61,6 @@ async function createTemporaryAppFolder(tempDir: string) { }); } -async function cleanUp(tempDir: string) { - await Task.forItem('remove', 'temporary directory', async () => { - await fs.remove(tempDir); - }); -} - async function buildApp(appDir: string) { const runCmd = async (cmd: string) => { await Task.forItem('executing', cmd, async () => { @@ -75,11 +80,17 @@ async function buildApp(appDir: string) { async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch(error => { - throw new Error( - `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, - ); - }); + await fs + .move(tempDir, destination) + .catch(error => { + throw new Error( + `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, + ); + }) + .finally(() => { + // remove temporary files on both success and failure + fs.removeSync(tempDir); + }); }); } @@ -117,23 +128,40 @@ export default async (cmd: Command): Promise => { const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); - const appDir = resolvePath(paths.targetDir, answers.name); + + // Use `--path` argument as applicaiton directory when specified, otherwise + // create a directory using `answers.name` + const appDir = cmd.path + ? resolvePath(paths.targetDir, cmd.path) + : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); try { - Task.section('Checking if the directory is available'); - await checkExists(paths.targetDir, answers.name); + if (cmd.path) { + // Template directly to specified path - Task.section('Creating a temporary app directory'); - await createTemporaryAppFolder(tempDir); + Task.section('Checking that supplied path exists'); + await checkPathExists(appDir); - Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); + Task.section('Preparing files'); + await templatingTask(templateDir, cmd.path, answers); + } else { + // Template to temporary location, and then move files - Task.section('Moving to final location'); - await moveApp(tempDir, appDir, answers.name); + Task.section('Checking if the directory is available'); + await checkAppExists(paths.targetDir, answers.name); + + Task.section('Creating a temporary app directory'); + await createTemporaryAppFolder(tempDir); + + Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, answers); + + Task.section('Moving to final location'); + await moveApp(tempDir, appDir, answers.name); + } if (!cmd.skipInstall) { Task.section('Building the app'); @@ -157,10 +185,7 @@ export default async (cmd: Command): Promise => { Task.error(String(error)); Task.log('It seems that something went wrong when creating the app ๐Ÿค”'); - Task.log('We are going to clean up, and then you can try again.'); - Task.section('Cleanup'); - await cleanUp(tempDir); Task.error('๐Ÿ”ฅ Failed to create app!'); Task.exit(1); } diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 98a3cee908..1f37be56d2 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -26,10 +26,14 @@ import { version } from '../package.json'; import createApp from './createApp'; const main = (argv: string[]) => { - program.name('backstage-create-app').version(version); - program - .description('Creates a new app in a new directory') + .name('backstage-create-app') + .version(version) + .description('Creates a new app in a new directory or specified path') + .option( + '--path [directory]', + 'Location to store the app defaulting to a new folder with the app name', + ) .option( '--skip-install', 'Skip the install and builds steps after creating the app', diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 97904cc15b..6c85652c64 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -30,6 +30,7 @@ Rollup will extract the value of the version field in each package at build time leaving any imports in place. */ +import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendCommon } from '../../../backend-common/package.json'; import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; @@ -68,6 +69,7 @@ import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-b import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { + '@backstage/app-defaults': appDefaults, '@backstage/backend-common': backendCommon, '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 5ed066a30b..bdae71d9e3 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -20,13 +20,9 @@ "lint": "lerna run lint --since origin/master --", "lint:all": "lerna run lint --", "prettier:check": "prettier --check .", - "create-plugin": "backstage-cli create-plugin --scope internal --no-private", + "create-plugin": "backstage-cli create-plugin --scope internal", "remove-plugin": "backstage-cli remove-plugin" }, - "resolutions": { - "graphql-language-service-interface": "2.8.2", - "graphql-language-service-parser": "1.9.0" - }, "workspaces": { "packages": [ "packages/*", 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 c443b45dc7..9e15608a01 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 @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^{{version '@backstage/app-defaults'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/core-app-api": "^{{version '@backstage/core-app-api'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 4cd83685a6..8a535835b4 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -26,7 +26,8 @@ import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; const app = createApp({ apis, 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 198e7ec2b1..ec59b0b116 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,7 @@ import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; import { NavLink } from 'react-router-dom'; import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; -import { SidebarSearch } from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; import { Sidebar, SidebarPage, @@ -74,7 +74,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - + {/* Global nav, not org-specific */} diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index e01390e169..974329e8d4 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/core-app-api": "^0.1.18", "@backstage/core-components": "^0.7.1", "@backstage/core-plugin-api": "^0.1.11", @@ -44,10 +45,12 @@ "@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" + "react-router-dom": "6.0.0-beta.0", + "zen-observable": "^0.8.15" }, "devDependencies": { "@backstage/cli": "^0.8.0", diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx new file mode 100644 index 0000000000..3907dc1007 --- /dev/null +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { AppThemeApi, appThemeApiRef } from '@backstage/core-plugin-api'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BackstageTheme } from '@backstage/theme'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import ObservableImpl from 'zen-observable'; +import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; + +describe('SidebarThemeSwitcher', () => { + let appThemeApi: jest.Mocked; + let apiRegistry: ApiRegistry; + + beforeEach(() => { + appThemeApi = { + activeThemeId$: jest.fn(), + getActiveThemeId: jest.fn(), + getInstalledThemes: jest.fn(), + setActiveThemeId: jest.fn(), + }; + + appThemeApi.activeThemeId$.mockReturnValue( + ObservableImpl.of('dark'), + ); + appThemeApi.getInstalledThemes.mockReturnValue([ + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + theme: {} as unknown as BackstageTheme, + }, + { + id: 'light', + title: 'Light Theme', + variant: 'light', + theme: {} as unknown as BackstageTheme, + }, + ]); + + apiRegistry = ApiRegistry.with(appThemeApiRef, appThemeApi); + }); + + it('should display current theme', async () => { + const { getByLabelText, getByRole, getByText } = await renderInTestApp( + + + , + ); + + const button = getByLabelText('Switch Theme'); + expect(button).toBeInTheDocument(); + + userEvent.click(button); + + expect(getByRole('listbox')).toBeInTheDocument(); + expect(getByText('Dark Theme')).toBeInTheDocument(); + expect( + getByText('Dark Theme').parentElement?.parentElement, + ).toHaveAttribute('aria-selected', 'true'); + }); + + it('should select different theme', async () => { + const { getByLabelText, getByRole, getByText } = await renderInTestApp( + + + , + ); + + const button = getByLabelText('Switch Theme'); + expect(button).toBeInTheDocument(); + + userEvent.click(button); + + expect(getByRole('listbox')).toBeInTheDocument(); + + userEvent.click(getByText('Light Theme')); + + expect(appThemeApi.setActiveThemeId).toHaveBeenCalledWith('light'); + }); +}); diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx new file mode 100644 index 0000000000..e13554bbf7 --- /dev/null +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.tsx @@ -0,0 +1,125 @@ +/* + * 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 { SidebarItem } from '@backstage/core-components'; +import { appThemeApiRef, useApi } from '@backstage/core-plugin-api'; +import { ListItemIcon, ListItemText, Menu, MenuItem } from '@material-ui/core'; +import AutoIcon from '@material-ui/icons/BrightnessAuto'; +import React, { cloneElement, useCallback, useState } from 'react'; +import { useObservable } from 'react-use'; + +type ThemeIconProps = { + active?: boolean; + icon: JSX.Element | undefined; +}; + +const ThemeIcon = ({ active, icon }: ThemeIconProps) => + icon ? ( + cloneElement(icon, { + color: active ? 'primary' : undefined, + }) + ) : ( + + ); + +export const SidebarThemeSwitcher = () => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + const themeIds = appThemeApi.getInstalledThemes(); + const activeTheme = themeIds.find(t => t.id === themeId); + + const [anchorEl, setAnchorEl] = useState(); + const open = Boolean(anchorEl); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleSelectTheme = (newThemeId: string | undefined) => { + if (themeIds.some(t => t.id === newThemeId)) { + appThemeApi.setActiveThemeId(newThemeId); + } else { + appThemeApi.setActiveThemeId(undefined); + } + + setAnchorEl(undefined); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const ActiveIcon = useCallback( + () => , + [activeTheme], + ); + + return ( + <> + + + + Choose a theme + handleSelectTheme(undefined)} + > + + + + Auto + + + {themeIds.map(theme => { + const active = theme.id === themeId; + return ( + handleSelectTheme(theme.id)} + > + + + + {theme.title} + + ); + })} + + + ); +}; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 755289a060..adea9e14f2 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -14,6 +14,30 @@ * limitations under the License. */ +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { + AlertDisplay, + OAuthRequestDialog, + Sidebar, + SidebarDivider, + SidebarItem, + SidebarPage, + SidebarSpace, + SidebarSpacer, +} from '@backstage/core-components'; +import { + AnyApiFactory, + ApiFactory, + AppTheme, + attachComponentData, + BackstagePlugin, + configApiRef, + createApiFactory, + createRouteRef, + IconComponent, + RouteRef, +} from '@backstage/core-plugin-api'; import { ScmIntegrationsApi, scmIntegrationsApiRef, @@ -24,30 +48,7 @@ import React, { ComponentType, ReactNode } from 'react'; import ReactDOM from 'react-dom'; import { hot } from 'react-hot-loader'; import { Route } from 'react-router'; - -import { - AlertDisplay, - OAuthRequestDialog, - Sidebar, - SidebarItem, - SidebarPage, - SidebarSpacer, -} from '@backstage/core-components'; - -import { - AnyApiFactory, - ApiFactory, - AppTheme, - attachComponentData, - configApiRef, - createApiFactory, - createRouteRef, - IconComponent, - RouteRef, - BackstagePlugin, -} from '@backstage/core-plugin-api'; - -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; const GatheringRoute: (props: { path: string; @@ -202,6 +203,9 @@ export class DevAppBuilder { {this.sidebarItems} + + + {this.routes} diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index e783fa1de6..6c941cda58 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -25,18 +25,19 @@ }, "devDependencies": { "@backstage/cli-common": "^0.1.1", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.4", "@types/fs-extra": "^9.0.1", "@types/node": "^14.14.32", + "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^6.1.0", "cross-fetch": "^3.0.6", "fs-extra": "9.1.0", "handlebars": "^4.7.3", "pgtools": "^0.3.0", + "puppeteer": "^10.4.0", "tree-kill": "^1.2.2", - "ts-node": "^10.0.0", - "zombie": "^6.1.4" + "ts-node": "^10.0.0" }, "nodemonConfig": { "watch": "./src", diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b0db715f25..e1d8473121 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -20,7 +20,8 @@ import fetch from 'cross-fetch'; import handlebars from 'handlebars'; import killTree from 'tree-kill'; import { resolve as resolvePath, join as joinPath } from 'path'; -import Browser from 'zombie'; +import puppeteer from 'puppeteer'; + import { spawnPiped, runPlain, @@ -350,17 +351,21 @@ async function testAppServe(pluginName: string, appDir: string) { GITHUB_TOKEN: 'abc', }, }); - Browser.localhost('localhost', 3000); let successful = false; + + let browser; try { for (let attempts = 1; ; attempts++) { try { - const browser = new Browser(); + browser = await puppeteer.launch(); + const page = await browser.newPage(); - await waitForPageWithText(browser, '/', 'My Company Catalog'); + await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' }); + + await waitForPageWithText(page, '/', 'My Company Catalog'); await waitForPageWithText( - browser, + page, `/${pluginName}`, `Welcome to ${pluginName}!`, ); @@ -372,13 +377,15 @@ async function testAppServe(pluginName: string, appDir: string) { if (attempts >= 20) { throw new Error(`App serve test failed, ${error}`); } - console.log(`App serve failed, trying again, ${error}`); + print(`App serve failed, trying again, ${error}`); await new Promise(resolve => setTimeout(resolve, 1000)); } } } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes - killTree(startApp.pid); + await new Promise((res, rej) => + killTree(startApp.pid, err => (err ? rej(err) : res())), + ); } try { @@ -438,9 +445,26 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { }); let successful = false; + const stdErrorHasErrors = (input: string) => { + const lines = input.split('\n').filter(Boolean); + return ( + lines.filter( + l => + !l.includes('Use of deprecated folder mapping') && + !l.includes('Update this package.json to use a subpath') && + !l.includes( + '(Use `node --trace-deprecation ...` to show where the warning was created)', + ), + ).length !== 0 + ); + }; + try { - await waitFor(() => stdout.includes('Listening on ') || stderr !== ''); - if (stderr !== '') { + await waitFor( + () => stdout.includes('Listening on ') || stdErrorHasErrors(stderr), + ); + if (stdErrorHasErrors(stderr)) { + print(`Expected stderr to be clean, got ${stderr}`); // Skipping the whole block throw new Error(stderr); } @@ -453,11 +477,14 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { print('Entities fetched successfully'); successful = true; } catch (error) { + print(''); throw new Error(`Backend failed to startup: ${error}`); } finally { print('Stopping the child process'); // Kill entire process group, otherwise we'll end up with hanging serve processes - killTree(child.pid); + await new Promise((res, rej) => + killTree(child.pid, err => (err ? rej(err) : res())), + ); } try { diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index 5193db08ec..350898234d 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -22,6 +22,7 @@ import { ChildProcess, } from 'child_process'; import { promisify } from 'util'; +import puppeteer from 'puppeteer'; const execFile = promisify(execFileCb); @@ -125,7 +126,7 @@ export async function waitForExit(child: ChildProcess) { } export async function waitForPageWithText( - browser: any, + page: puppeteer.Page, path: string, text: string, { intervalMs = 1000, maxFindAttempts = 50 } = {}, @@ -137,18 +138,25 @@ export async function waitForPageWithText( console.log(`Attempting to load page at ${path}, waiting ${waitTimeMs}`); await new Promise(resolve => setTimeout(resolve, waitTimeMs)); - await browser.visit(path); + await page.goto(`http://localhost:3000${path}`, { + waitUntil: 'networkidle0', + }); - await new Promise(resolve => setTimeout(resolve, waitTimeMs)); - - const escapedText = text.replace(/"|\\/g, '\\$&'); - browser.assert.evaluate( - `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, - true, - `expected to find text ${text}`, + const match = await page.evaluate( + textContent => + Array.from(document.querySelectorAll('*')).some( + el => el.textContent === textContent, + ), + text, ); + + if (!match) { + throw new Error(`Expected to find text ${text}`); + } + break; } catch (error) { + console.log(error); assertError(error); findAttempts++; diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 368fa26593..9ea2f47cbb 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/errors +## 0.1.4 + +### Patch Changes + +- a15d028517: More API fixes: mark things public, add docs, fix exports +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 8c30ae8902: Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` + ## 0.1.3 ### Patch Changes diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index a96132cee7..4984919e6a 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -17,7 +17,6 @@ export class ConflictError extends CustomErrorBase {} // @public export class CustomErrorBase extends Error { constructor(message?: string, cause?: Error | unknown); - // (undocumented) readonly cause?: Error | undefined; } diff --git a/packages/errors/package.json b/packages/errors/package.json index b1f214e5af..fe62bb3bba 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index 17149cd398..786e10ef23 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -33,6 +33,9 @@ import { isError } from './assertion'; * ``` */ export class CustomErrorBase extends Error { + /** + * An inner error that caused this error to be thrown, if any. + */ readonly cause?: Error | undefined; constructor(message?: string, cause?: Error | unknown) { diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index cddaf2e792..a2a0d0c368 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/integration-react +## 0.1.13 + +### Patch Changes + +- 36e2b548cb: Clean up the API exports +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.12 ### Patch Changes diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index b7feb6b6a9..2db29631a3 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -65,7 +65,6 @@ export class ScmAuth implements ScmAuthApi { host?: string; }, ): ScmAuth; - // (undocumented) getCredentials(options: ScmAuthTokenOptions): Promise; isUrlSupported(url: URL): boolean; static merge(...providers: ScmAuth[]): ScmAuthApi; @@ -79,7 +78,7 @@ export interface ScmAuthApi { // @public export const scmAuthApiRef: ApiRef; -// @public (undocumented) +// @public export interface ScmAuthTokenOptions extends AuthRequestOptions { additionalScope?: { repoWrite?: boolean; @@ -87,7 +86,7 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions { url: string; } -// @public (undocumented) +// @public export interface ScmAuthTokenResponse { headers: { [name: string]: string; @@ -95,25 +94,21 @@ export interface ScmAuthTokenResponse { token: string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ScmIntegrationIcon: ({ - type, -}: { - type?: string | undefined; -}) => JSX.Element; +// @public +export const ScmIntegrationIcon: ( + props: ScmIntegrationIconProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "ScmIntegrationsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type ScmIntegrationIconProps = { + type?: string; +}; + +// @public export class ScmIntegrationsApi { - // (undocumented) static fromConfig(config: Config): ScmIntegrationRegistry; } -// Warning: (ae-missing-release-tag) "scmIntegrationsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const scmIntegrationsApiRef: ApiRef; ``` diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index b0867c755b..b703203ab2 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.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.11", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.2", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/integration": "^0.6.9", + "@backstage/theme": "^0.2.12", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -34,15 +34,15 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.20", "@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.29.0", + "msw": "^0.35.0", "cross-fetch": "^3.0.6" }, "files": [ diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index c34c4da758..be3216962d 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -254,6 +254,9 @@ export class ScmAuth implements ScmAuthApi { return url.host === this.#host; } + /** + * Fetches credentials for the given resource. + */ async getCredentials( options: ScmAuthTokenOptions, ): Promise { diff --git a/packages/integration-react/src/api/ScmAuthApi.ts b/packages/integration-react/src/api/ScmAuthApi.ts index d8fc413620..44b007c4a2 100644 --- a/packages/integration-react/src/api/ScmAuthApi.ts +++ b/packages/integration-react/src/api/ScmAuthApi.ts @@ -20,7 +20,11 @@ import { AuthRequestOptions, } from '@backstage/core-plugin-api'; -/** @public */ +/** + * The options that control a {@link ScmAuthApi.getCredentials} call. + * + * @public + */ export interface ScmAuthTokenOptions extends AuthRequestOptions { /** * The URL of the SCM resource to be accessed. @@ -43,7 +47,11 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions { }; } -/** @public */ +/** + * The response from a {@link ScmAuthApi.getCredentials} call. + * + * @public + */ export interface ScmAuthTokenResponse { /** * An authorization token that can be used to authenticate requests. diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.ts b/packages/integration-react/src/api/ScmIntegrationsApi.ts index 6bf2ba8a6d..06f28a6c4d 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.ts @@ -21,12 +21,27 @@ import { } from '@backstage/integration'; import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; +/** + * Factory class for creating {@link @backstage/integration#ScmIntegrationRegistry} instances. + * + * @public + */ export class ScmIntegrationsApi { + /** + * Instantiates an {@link @backstage/integration#ScmIntegrationRegistry}. + * + * @param config - The root of the config hierarchy. + */ static fromConfig(config: Config): ScmIntegrationRegistry { return ScmIntegrations.fromConfig(config); } } +/** + * The API that holds all configured SCM integrations. + * + * @public + */ export const scmIntegrationsApiRef: ApiRef = createApiRef({ id: 'integration.scmintegrations', diff --git a/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx b/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx index 80567d5cc3..dd6ba18690 100644 --- a/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx +++ b/packages/integration-react/src/components/ScmIntegrationIcon/ScmIntegrationIcon.tsx @@ -17,7 +17,25 @@ import CodeIcon from '@material-ui/icons/Code'; import React from 'react'; import { useApp } from '@backstage/core-plugin-api'; -export const ScmIntegrationIcon = ({ type }: { type?: string }) => { +/** + * Props for {@link ScmIntegrationIcon}. + * + * @public + */ +export type ScmIntegrationIconProps = { + /** + * The integration type, e.g. "github". + */ + type?: string; +}; + +/** + * An icon that represents a certain SCM integration. + * + * @public + */ +export const ScmIntegrationIcon = (props: ScmIntegrationIconProps) => { + const { type } = props; const app = useApp(); const DefaultIcon = CodeIcon; const Icon = type ? app.getSystemIcon(type) ?? DefaultIcon : DefaultIcon; diff --git a/packages/integration-react/src/components/ScmIntegrationIcon/index.ts b/packages/integration-react/src/components/ScmIntegrationIcon/index.ts index 86e60e5b99..a487484d39 100644 --- a/packages/integration-react/src/components/ScmIntegrationIcon/index.ts +++ b/packages/integration-react/src/components/ScmIntegrationIcon/index.ts @@ -15,3 +15,4 @@ */ export { ScmIntegrationIcon } from './ScmIntegrationIcon'; +export type { ScmIntegrationIconProps } from './ScmIntegrationIcon'; diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 327b9e5fcc..b9c6601e6c 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration +## 0.6.9 + +### Patch Changes + +- a15d028517: More API fixes: mark things public, add docs, fix exports +- Updated dependencies + - @backstage/config@0.1.11 + ## 0.6.8 ### Patch Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 01f84383fe..4c2258779a 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -6,15 +6,11 @@ import { Config } from '@backstage/config'; import { RestEndpointMethodTypes } from '@octokit/rest'; -// Warning: (ae-missing-release-tag) "AwsS3Integration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AwsS3Integration implements ScmIntegration { constructor(integrationConfig: AwsS3IntegrationConfig); // (undocumented) get config(): AwsS3IntegrationConfig; - // Warning: (ae-forgotten-export) The symbol "ScmIntegrationsFactory" needs to be exported by the entry point index.d.ts - // // (undocumented) static factory: ScmIntegrationsFactory; // (undocumented) @@ -31,8 +27,6 @@ export class AwsS3Integration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "AwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AwsS3IntegrationConfig = { host: string; @@ -41,9 +35,7 @@ export type AwsS3IntegrationConfig = { roleArn?: string; }; -// Warning: (ae-missing-release-tag) "AzureIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class AzureIntegration implements ScmIntegration { constructor(integrationConfig: AzureIntegrationConfig); // (undocumented) @@ -64,17 +56,13 @@ export class AzureIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "AzureIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type AzureIntegrationConfig = { host: string; token?: string; }; -// Warning: (ae-missing-release-tag) "BitbucketIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class BitbucketIntegration implements ScmIntegration { constructor(integrationConfig: BitbucketIntegrationConfig); // (undocumented) @@ -95,8 +83,6 @@ export class BitbucketIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "BitbucketIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type BitbucketIntegrationConfig = { host: string; @@ -106,8 +92,6 @@ export type BitbucketIntegrationConfig = { appPassword?: string; }; -// Warning: (ae-missing-release-tag) "defaultScmResolveUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function defaultScmResolveUrl(options: { url: string; @@ -115,92 +99,44 @@ export function defaultScmResolveUrl(options: { lineNumber?: number; }): string; -// 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) "getAzureCommitsUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureCommitsUrl(url: string): string; -// 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) "getAzureDownloadUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureDownloadUrl(url: string): string; -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// 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) "getAzureFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureFileFetchUrl(url: string): string; -// 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) "getAzureRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// 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) "getBitbucketDefaultBranch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketDefaultBranch( url: string, config: BitbucketIntegrationConfig, ): 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-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketDownloadUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketDownloadUrl( url: string, config: BitbucketIntegrationConfig, ): 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-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getBitbucketFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketFileFetchUrl( url: string, config: BitbucketIntegrationConfig, ): string; -// 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) "getBitbucketRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// 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 "GithubCredentials" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "getGitHubFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitHubFileFetchUrl( url: string, @@ -208,36 +144,34 @@ export function getGitHubFileFetchUrl( credentials: GithubCredentials, ): string; -// 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) "getGitHubRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export function getGitHubRequestOptions( config: GitHubIntegrationConfig, credentials: GithubCredentials, ): RequestInit; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// 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) "getGitLabFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitLabFileFetchUrl( url: string, config: GitLabIntegrationConfig, ): 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) "getGitLabRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getGitLabRequestOptions( config: GitLabIntegrationConfig, ): RequestInit; -// Warning: (ae-missing-release-tag) "GithubAppCredentialsMux" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type GithubAppConfig = { + appId: number; + privateKey: string; + webhookSecret: string; + clientId: string; + clientSecret: string; + allowedInstallationOwners?: string[]; +}; + +// @public export class GithubAppCredentialsMux { constructor(config: GitHubIntegrationConfig); // (undocumented) @@ -248,33 +182,26 @@ export class GithubAppCredentialsMux { getAppToken(owner: string, repo?: string): Promise; } -// Warning: (ae-missing-release-tag) "GithubCredentialsProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type GithubCredentials = { + headers?: { + [name: string]: string; + }; + token?: string; + type: GithubCredentialType; +}; + +// @public export class GithubCredentialsProvider { // (undocumented) static create(config: GitHubIntegrationConfig): GithubCredentialsProvider; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" getCredentials(opts: { url: string }): Promise; } -// Warning: (ae-missing-release-tag) "GithubCredentialType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type GithubCredentialType = 'app' | 'token'; -// Warning: (ae-missing-release-tag) "GitHubIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GitHubIntegration implements ScmIntegration { constructor(integrationConfig: GitHubIntegrationConfig); // (undocumented) @@ -295,8 +222,6 @@ export class GitHubIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "GitHubIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GitHubIntegrationConfig = { host: string; @@ -306,9 +231,7 @@ export type GitHubIntegrationConfig = { apps?: GithubAppConfig[]; }; -// Warning: (ae-missing-release-tag) "GitLabIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class GitLabIntegration implements ScmIntegration { constructor(integrationConfig: GitLabIntegrationConfig); // (undocumented) @@ -329,8 +252,6 @@ export class GitLabIntegration implements ScmIntegration { get type(): string; } -// Warning: (ae-missing-release-tag) "GitLabIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GitLabIntegrationConfig = { host: string; @@ -339,118 +260,90 @@ export type GitLabIntegrationConfig = { baseUrl: string; }; -// Warning: (ae-missing-release-tag) "GoogleGcsIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GoogleGcsIntegrationConfig = { clientEmail?: string; privateKey?: string; }; -// 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) "readAwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export interface IntegrationsByType { + // (undocumented) + awsS3: ScmIntegrationsGroup; + // (undocumented) + azure: ScmIntegrationsGroup; + // (undocumented) + bitbucket: ScmIntegrationsGroup; + // (undocumented) + github: ScmIntegrationsGroup; + // (undocumented) + gitlab: ScmIntegrationsGroup; +} + // @public export function readAwsS3IntegrationConfig( config: Config, ): AwsS3IntegrationConfig; -// Warning: (ae-missing-release-tag) "readAwsS3IntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function readAwsS3IntegrationConfigs( configs: Config[], ): AwsS3IntegrationConfig[]; -// 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) "readAzureIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig; -// 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) "readAzureIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readAzureIntegrationConfigs( configs: Config[], ): AzureIntegrationConfig[]; -// 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) "readBitbucketIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readBitbucketIntegrationConfig( config: Config, ): BitbucketIntegrationConfig; -// 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) "readBitbucketIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readBitbucketIntegrationConfigs( configs: Config[], ): BitbucketIntegrationConfig[]; -// 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) "readGitHubIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitHubIntegrationConfig( config: Config, ): GitHubIntegrationConfig; -// 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) "readGitHubIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitHubIntegrationConfigs( configs: Config[], ): GitHubIntegrationConfig[]; -// 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) "readGitLabIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitLabIntegrationConfig( config: Config, ): GitLabIntegrationConfig; -// 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) "readGitLabIntegrationConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGitLabIntegrationConfigs( configs: Config[], ): GitLabIntegrationConfig[]; -// 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) "readGoogleGcsIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readGoogleGcsIntegrationConfig( config: Config, ): GoogleGcsIntegrationConfig; -// Warning: (ae-missing-release-tag) "replaceUrlType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function replaceGitHubUrlType( url: string, type: 'blob' | 'tree' | 'edit', ): string; -// Warning: (ae-missing-release-tag) "ScmIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface ScmIntegration { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen resolveEditUrl(url: string): string; - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters resolveUrl(options: { url: string; base: string; @@ -460,8 +353,6 @@ export interface ScmIntegration { type: string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface ScmIntegrationRegistry extends ScmIntegrationsGroup { @@ -475,11 +366,7 @@ export interface ScmIntegrationRegistry github: ScmIntegrationsGroup; // (undocumented) gitlab: ScmIntegrationsGroup; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen resolveEditUrl(url: string): string; - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters - // Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters resolveUrl(options: { url: string; base: string; @@ -487,11 +374,8 @@ export interface ScmIntegrationRegistry }): string; } -// Warning: (ae-missing-release-tag) "ScmIntegrations" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class ScmIntegrations implements ScmIntegrationRegistry { - // Warning: (ae-forgotten-export) The symbol "IntegrationsByType" needs to be exported by the entry point index.d.ts constructor(integrationsByType: IntegrationsByType); // (undocumented) get awsS3(): ScmIntegrationsGroup; @@ -521,20 +405,20 @@ export class ScmIntegrations implements ScmIntegrationRegistry { }): string; } -// Warning: (ae-missing-release-tag) "ScmIntegrationsGroup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type ScmIntegrationsFactory = (options: { + config: Config; +}) => ScmIntegrationsGroup; + // @public export interface ScmIntegrationsGroup { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen byHost(host: string): T | undefined; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen byUrl(url: string | URL): T | undefined; list(): T[]; } // Warnings were encountered during analysis: // -// src/github/config.d.ts:41:5 - (ae-forgotten-export) The symbol "GithubAppConfig" needs to be exported by the entry point index.d.ts -// src/gitlab/config.d.ts:27:68 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/gitlab/config.d.ts:27:63 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/gitlab/config.d.ts:29:68 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/gitlab/config.d.ts:29:63 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" ``` diff --git a/packages/integration/package.json b/packages/integration/package.json index 5dda924580..bd49ecc419 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.8", + "version": "0.6.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.10", + "@backstage/config": "^0.1.11", "cross-fetch": "^3.0.6", "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", @@ -39,12 +39,12 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/config-loader": "^0.7.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/config-loader": "^0.7.1", + "@backstage/test-utils": "^0.1.20", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 983ab7d24b..add9e4e016 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -24,14 +24,24 @@ import { defaultScmResolveUrl } from './helpers'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; import { ScmIntegrationRegistry } from './registry'; -type IntegrationsByType = { +/** + * The set of supported integrations. + * + * @public + */ +export interface IntegrationsByType { awsS3: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; bitbucket: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; -}; +} +/** + * Exposes the set of supported integrations. + * + * @public + */ export class ScmIntegrations implements ScmIntegrationRegistry { private readonly byType: IntegrationsByType; diff --git a/packages/integration/src/awsS3/AwsS3Integration.ts b/packages/integration/src/awsS3/AwsS3Integration.ts index e987b8f7a3..1507ff514c 100644 --- a/packages/integration/src/awsS3/AwsS3Integration.ts +++ b/packages/integration/src/awsS3/AwsS3Integration.ts @@ -18,6 +18,11 @@ import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AwsS3IntegrationConfig, readAwsS3IntegrationConfigs } from './config'; +/** + * Integrates with AWS S3 or compatible solutions. + * + * @public + */ export class AwsS3Integration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAwsS3IntegrationConfigs( @@ -42,6 +47,7 @@ export class AwsS3Integration implements ScmIntegration { } constructor(private readonly integrationConfig: AwsS3IntegrationConfig) {} + resolveUrl(options: { url: string; base: string; diff --git a/packages/integration/src/awsS3/config.ts b/packages/integration/src/awsS3/config.ts index 5f9cd46a22..6b5a23c85c 100644 --- a/packages/integration/src/awsS3/config.ts +++ b/packages/integration/src/awsS3/config.ts @@ -21,8 +21,9 @@ const AMAZON_AWS_HOST = 'amazonaws.com'; /** * The configuration parameters for a single AWS S3 provider. + * + * @public */ - export type AwsS3IntegrationConfig = { /** * The host of the target that this matches on, e.g. "amazonaws.com" @@ -50,7 +51,8 @@ export type AwsS3IntegrationConfig = { /** * Reads a single Aws S3 integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readAwsS3IntegrationConfig( @@ -70,6 +72,13 @@ export function readAwsS3IntegrationConfig( return { host, accessKeyId, secretAccessKey, roleArn }; } +/** + * Reads a set of AWS S3 integration configs, and inserts some defaults for + * public Amazon AWS if not specified. + * + * @param configs - The config objects of the integrations + * @public + */ export function readAwsS3IntegrationConfigs( configs: Config[], ): AwsS3IntegrationConfig[] { diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index ab1b2489ae..d036d88c21 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -19,6 +19,11 @@ import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureUrl } from './AzureUrl'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; +/** + * Microsoft Azure based integration. + * + * @public + */ export class AzureIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAzureIntegrationConfigs( diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 9755bbbe24..2d722d1589 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -21,6 +21,8 @@ const AZURE_HOST = 'dev.azure.com'; /** * The configuration parameters for a single Azure provider. + * + * @public */ export type AzureIntegrationConfig = { /** @@ -41,7 +43,8 @@ export type AzureIntegrationConfig = { /** * Reads a single Azure integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readAzureIntegrationConfig( config: Config, @@ -62,7 +65,8 @@ export function readAzureIntegrationConfig( * Reads a set of Azure integration configs, and inserts some defaults for * public Azure if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readAzureIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 30603b1f09..7461954267 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -21,11 +21,14 @@ import { AzureIntegrationConfig } from './config'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * - * Converts - * from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents - * to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} + * @remarks * - * @param url A URL pointing to a file + * Converts + * - from: `https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents` + * - to: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}` + * + * @param url - A URL pointing to a file + * @public */ export function getAzureFileFetchUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toFileUrl(); @@ -35,7 +38,8 @@ export function getAzureFileFetchUrl(url: string): string { * Given a URL pointing to a path on a provider, returns a URL that is suitable * for downloading the subtree. * - * @param url A URL pointing to a path + * @param url - A URL pointing to a path + * @public */ export function getAzureDownloadUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toArchiveUrl(); @@ -44,7 +48,8 @@ export function getAzureDownloadUrl(url: string): string { /** * Given a URL, return the API URL to fetch commits on the branch. * - * @param url A URL pointing to a repository or a sub-path + * @param url - A URL pointing to a repository or a sub-path + * @public */ export function getAzureCommitsUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toCommitsUrl(); @@ -53,7 +58,8 @@ export function getAzureCommitsUrl(url: string): string { /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getAzureRequestOptions( config: AzureIntegrationConfig, diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index 07c04cd41e..6c21d716b7 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -22,6 +22,11 @@ import { readBitbucketIntegrationConfigs, } from './config'; +/** + * A Bitbucket based integration. + * + * @public + */ export class BitbucketIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config, diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index e2758952e9..1cd911aad3 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -23,6 +23,8 @@ const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0'; /** * The configuration parameters for a single Bitbucket API provider. + * + * @public */ export type BitbucketIntegrationConfig = { /** @@ -66,7 +68,8 @@ export type BitbucketIntegrationConfig = { /** * Reads a single Bitbucket integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readBitbucketIntegrationConfig( config: Config, @@ -102,7 +105,8 @@ export function readBitbucketIntegrationConfig( * Reads a set of Bitbucket integration configs, and inserts some defaults for * public Bitbucket if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readBitbucketIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 0de1d5aaab..2b16be82d4 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { BitbucketIntegrationConfig } from './config'; import { getBitbucketDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucket core', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('getBitbucketRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index f8f8f56fa8..afcb9fcfdb 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -21,8 +21,9 @@ import { BitbucketIntegrationConfig } from './config'; /** * Given a URL pointing to a path on a provider, returns the default branch. * - * @param url A URL pointing to a path - * @param config The relevant provider config + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public */ export async function getBitbucketDefaultBranch( url: string, @@ -71,8 +72,9 @@ export async function getBitbucketDefaultBranch( * Given a URL pointing to a path on a provider, returns a URL that is suitable * for downloading the subtree. * - * @param url A URL pointing to a path - * @param config The relevant provider config + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public */ export async function getBitbucketDownloadUrl( url: string, @@ -108,12 +110,15 @@ export async function getBitbucketDownloadUrl( * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export function getBitbucketFileFetchUrl( url: string, @@ -148,7 +153,8 @@ export function getBitbucketFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index efc1de782e..ccceb34d88 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitHubIntegration, replaceUrlType } from './GitHubIntegration'; +import { GitHubIntegration, replaceGitHubUrlType } from './GitHubIntegration'; describe('GitHubIntegration', () => { it('has a working factory', () => { @@ -80,25 +80,28 @@ describe('GitHubIntegration', () => { }); }); -describe('replaceUrlType', () => { +describe('replaceGitHubUrlType', () => { it('should replace with expected type', () => { expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/backstage/backstage/blob/master/README.md', 'edit', ), ).toBe('https://github.com/backstage/backstage/edit/master/README.md'); expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/webmodules/blob/blob/master/test', 'tree', ), ).toBe('https://github.com/webmodules/blob/tree/master/test'); expect( - replaceUrlType('https://github.com/blob/blob/blob/master/test', 'tree'), + replaceGitHubUrlType( + 'https://github.com/blob/blob/blob/master/test', + 'tree', + ), ).toBe('https://github.com/blob/blob/tree/master/test'); expect( - replaceUrlType( + replaceGitHubUrlType( 'https://github.com/backstage/backstage/edit/tree/README.md', 'blob', ), diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index 3b53fe51e8..1db0b1fae5 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -21,6 +21,11 @@ import { readGitHubIntegrationConfigs, } from './config'; +/** + * A GitHub based integration. + * + * @public + */ export class GitHubIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitHubIntegrationConfigs( @@ -54,15 +59,22 @@ export class GitHubIntegration implements ScmIntegration { // GitHub uses blob URLs for files and tree urls for directory listings. But // there is a redirect from tree to blob for files, so we can always return // tree urls here. - return replaceUrlType(defaultScmResolveUrl(options), 'tree'); + return replaceGitHubUrlType(defaultScmResolveUrl(options), 'tree'); } resolveEditUrl(url: string): string { - return replaceUrlType(url, 'edit'); + return replaceGitHubUrlType(url, 'edit'); } } -export function replaceUrlType( +/** + * Takes a GitHub URL and replaces the type part (blob, tree etc). + * + * @param url - The original URL + * @param type - The desired type, e.g. "blob" + * @public + */ +export function replaceGitHubUrlType( url: string, type: 'blob' | 'tree' | 'edit', ): string { diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index 278a1ed028..ece692fecc 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -159,7 +159,11 @@ class GithubAppManager { } } -// GithubAppCredentialsMux corresponds to a Github installation which internally could hold several GitHub Apps. +/** + * Corresponds to a Github installation which internally could hold several GitHub Apps. + * + * @public + */ export class GithubAppCredentialsMux { private readonly apps: GithubAppManager[]; @@ -211,15 +215,32 @@ export class GithubAppCredentialsMux { } } +/** + * The type of credentials produced by the credential provider. + * + * @public + */ export type GithubCredentialType = 'app' | 'token'; +/** + * A set of credentials information for a GitHub integration. + * + * @public + */ export type GithubCredentials = { headers?: { [name: string]: string }; token?: string; type: GithubCredentialType; }; -// TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake +/** + * Handles the creation and caching of credentials for GitHub integrations. + * + * @public + * @remarks + * + * TODO: Possibly move this to a backend only package so that it's not used in the frontend by mistake + */ export class GithubCredentialsProvider { static create(config: GitHubIntegrationConfig): GithubCredentialsProvider { return new GithubCredentialsProvider( @@ -234,13 +255,24 @@ export class GithubCredentialsProvider { ) {} /** - * Returns GithubCredentials for requested url. - * Consecutive calls to this method with the same url will return cached credentials. + * Returns {@link GithubCredentials} for a given URL. + * + * @remarks + * + * Consecutive calls to this method with the same URL will return cached + * credentials. + * * The shortest lifetime for a token returned is 10 minutes. - * @param opts containing the organization or repository url - * @returns {Promise} of @type {GithubCredentials}. + * * @example - * const { token, headers } = await getCredentials({url: 'github.com/backstage/foobar'}) + * ```ts + * const { token, headers } = await getCredentials({ + * url: 'github.com/backstage/foobar' + * }) + * ``` + * + * @param opts - The organization or repository URL + * @returns A promise of {@link GithubCredentials}. */ async getCredentials(opts: { url: string }): Promise { const parsed = parseGitUrl(opts.url); diff --git a/packages/integration/src/github/config.ts b/packages/integration/src/github/config.ts index 49674681f1..93801e96fb 100644 --- a/packages/integration/src/github/config.ts +++ b/packages/integration/src/github/config.ts @@ -24,6 +24,8 @@ const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com'; /** * The configuration parameters for a single GitHub integration. + * + * @public */ export type GitHubIntegrationConfig = { /** @@ -70,7 +72,12 @@ export type GitHubIntegrationConfig = { /** * The configuration parameters for authenticating a GitHub Application. - * A Github Apps configuration can be generated using the `backstage-cli create-github-app` command. + * + * @remarks + * + * A GitHub Apps configuration can be generated using the `backstage-cli create-github-app` command. + * + * @public */ export type GithubAppConfig = { /** @@ -107,7 +114,8 @@ export type GithubAppConfig = { /** * Reads a single GitHub integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGitHubIntegrationConfig( config: Config, @@ -152,7 +160,8 @@ export function readGitHubIntegrationConfig( * Reads a set of GitHub integration configs, and inserts some defaults for * public GitHub if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readGitHubIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index 49695ec5ac..2c5e739a26 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -22,13 +22,16 @@ import { GithubCredentials } from './GithubCredentialsProvider'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://github.com/a/b/blob/branchname/path/to/c.yaml * to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname * or: https://raw.githubusercontent.com/a/b/branchname/c.yaml * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export function getGitHubFileFetchUrl( url: string, @@ -64,7 +67,8 @@ export function getGitHubFileFetchUrl( * Gets the request options necessary to make requests to a given provider. * * @deprecated This function is no longer used internally - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getGitHubRequestOptions( config: GitHubIntegrationConfig, diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 80d9ce2f26..9c13f13135 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -18,14 +18,14 @@ export { readGitHubIntegrationConfig, readGitHubIntegrationConfigs, } from './config'; -export type { GitHubIntegrationConfig } from './config'; +export type { GithubAppConfig, GitHubIntegrationConfig } from './config'; export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; export { GithubAppCredentialsMux, GithubCredentialsProvider, } from './GithubCredentialsProvider'; -export type { GithubCredentialType } from './GithubCredentialsProvider'; -export { - GitHubIntegration, - replaceUrlType as replaceGitHubUrlType, -} from './GitHubIntegration'; +export type { + GithubCredentials, + GithubCredentialType, +} from './GithubCredentialsProvider'; +export { GitHubIntegration, replaceGitHubUrlType } from './GitHubIntegration'; diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 70a7c11987..cb24829946 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -21,6 +21,11 @@ import { readGitLabIntegrationConfigs, } from './config'; +/** + * A GitLab based integration. + * + * @public + */ export class GitLabIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitLabIntegrationConfigs( diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 7471ba0078..f9c15c3219 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -23,6 +23,8 @@ const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; /** * The configuration parameters for a single GitLab integration. + * + * @public */ export type GitLabIntegrationConfig = { /** @@ -57,7 +59,8 @@ export type GitLabIntegrationConfig = { /** * Reads a single GitLab integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGitLabIntegrationConfig( config: Config, @@ -104,7 +107,8 @@ export function readGitLabIntegrationConfig( * Reads a set of GitLab integration configs, and inserts some defaults for * public GitLab if not specified. * - * @param configs All of the integration config objects + * @param configs - All of the integration config objects + * @public */ export function readGitLabIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index b30d44e2db..e6f4dbc910 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -21,6 +21,8 @@ import fetch from 'cross-fetch'; * Given a URL pointing to a file on a provider, returns a URL that is suitable * for fetching the contents of the data. * + * @remarks + * * Converts * from: https://gitlab.example.com/a/b/blob/master/c.yaml * to: https://gitlab.example.com/a/b/raw/master/c.yaml @@ -28,8 +30,9 @@ import fetch from 'cross-fetch'; * from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath * to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch * - * @param url A URL pointing to a file - * @param config The relevant provider config + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public */ export async function getGitLabFileFetchUrl( url: string, @@ -48,7 +51,8 @@ export async function getGitLabFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. * - * @param config The relevant provider config + * @param config - The relevant provider config + * @public */ export function getGitLabRequestOptions( config: GitLabIntegrationConfig, diff --git a/packages/integration/src/googleGcs/config.ts b/packages/integration/src/googleGcs/config.ts index 0945a0e467..3c03204e4c 100644 --- a/packages/integration/src/googleGcs/config.ts +++ b/packages/integration/src/googleGcs/config.ts @@ -18,6 +18,8 @@ import { Config } from '@backstage/config'; /** * The configuration parameters for a single Google Cloud Storage provider. + * + * @public */ export type GoogleGcsIntegrationConfig = { /** @@ -33,7 +35,8 @@ export type GoogleGcsIntegrationConfig = { /** * Reads a single Google GCS integration config. * - * @param config The config object of a single integration + * @param config - The config object of a single integration + * @public */ export function readGoogleGcsIntegrationConfig( config: Config, diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index 52d2cf6571..2e5552d55c 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -59,8 +59,10 @@ export function basicIntegrations( } /** - * Default implementation of ScmIntegration.resolveUrl, that only works with - * URL pathname based providers. + * Default implementation of {@link ScmIntegration} `resolveUrl`, that only + * works with URL pathname based providers. + * + * @public */ export function defaultScmResolveUrl(options: { url: string; diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 3d5f336e8d..2f33b76ab3 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -28,5 +28,10 @@ export * from './googleGcs'; export * from './awsS3'; export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; -export type { ScmIntegration, ScmIntegrationsGroup } from './types'; +export type { IntegrationsByType } from './ScmIntegrations'; +export type { + ScmIntegration, + ScmIntegrationsFactory, + ScmIntegrationsGroup, +} from './types'; export type { ScmIntegrationRegistry } from './registry'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 21ca472ccb..5864695e25 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -23,6 +23,8 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration'; /** * Holds all registered SCM integrations, of all types. + * + * @public */ export interface ScmIntegrationRegistry extends ScmIntegrationsGroup { @@ -43,14 +45,19 @@ export interface ScmIntegrationRegistry * within the file tree of a certain repo, an absolute path of `/b.yaml` does * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. - * - * @param options.url The (absolute or relative) URL or path to resolve - * @param options.base The base URL onto which this resolution happens - * @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { + /** + * The (absolute or relative) URL or path to resolve. + */ url: string; + /** + * The base URL onto which this resolution happens + */ base: string; + /** + * The line number in the target file to link to, starting with 1. Only applicable when linking to files. + */ lineNumber?: number; }): string; @@ -63,7 +70,7 @@ export interface ScmIntegrationRegistry * If this is not possible, the integration can fall back to a URL to view * the file in the web interface. * - * @param url The absolute URL to the file that should be edited. + * @param url - The absolute URL to the file that should be edited. */ resolveEditUrl(url: string): string; } diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index e66b20f8f8..cb4b78eb7c 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -18,6 +18,8 @@ import { Config } from '@backstage/config'; /** * Encapsulates a single SCM integration. + * + * @public */ export interface ScmIntegration { /** @@ -42,14 +44,19 @@ export interface ScmIntegration { * within the file tree of a certain repo, an absolute path of `/b.yaml` does * not resolve to `https://hostname/b.yaml` but rather to * `/b.yaml` inside the file tree of that same repo. - * - * @param options.url The (absolute or relative) URL or path to resolve - * @param options.base The base URL onto which this resolution happens - * @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files. */ resolveUrl(options: { + /** + * The (absolute or relative) URL or path to resolve + */ url: string; + /** + * The base URL onto which this resolution happens + */ base: string; + /** + * The line number in the target file to link to, starting with 1. Only applicable when linking to files. + */ lineNumber?: number; }): string; @@ -62,13 +69,15 @@ export interface ScmIntegration { * If this is not possible, the integration can fall back to a URL to view * the file in the web interface. * - * @param url The absolute URL to the file that should be edited. + * @param url - The absolute URL to the file that should be edited. */ resolveEditUrl(url: string): string; } /** * Encapsulates several integrations, that are all of the same type. + * + * @public */ export interface ScmIntegrationsGroup { /** @@ -79,18 +88,23 @@ export interface ScmIntegrationsGroup { /** * Fetches an integration of this type by URL. * - * @param url A URL that matches a registered integration of this type + * @param url - A URL that matches a registered integration of this type */ byUrl(url: string | URL): T | undefined; /** * Fetches an integration of this type by host name. * - * @param url A host name that matches a registered integration of this type + * @param host - A host name that matches a registered integration of this type */ byHost(host: string): T | undefined; } +/** + * A factory function that creates an integration group based on configuration. + * + * @public + */ export type ScmIntegrationsFactory = (options: { config: Config; }) => ScmIntegrationsGroup; diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 34f956586e..e9f45d522e 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/search-common +## 0.2.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` + ## 0.2.0 ### Minor Changes diff --git a/packages/search-common/package.json b/packages/search-common/package.json index fee0fe59dd..e913ef66ff 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,7 +39,7 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.1" }, "jest": { "roots": [ diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 5bdc6b3a2f..72ff42311d 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -1,23 +1,30 @@ const path = require('path'); const WebpackPluginFailBuildOnWarning = require('./webpack-plugin-fail-build-on-warning'); +/** + * This set of stories are the ones that we publish to backstage.io. + */ +const BACKSTAGE_CORE_STORIES = [ + 'packages/core-components', + 'plugins/org', + 'plugins/search', +]; + module.exports = ({ args }) => { // Calling storybook with no args causes our default list of stories to be used. // This set of stories are the ones that we publish to backstage.io // // If it's called with args, each arg should be the path to a package that we will // show the stories from, for example `yarn storybook plugins/catalog`. - let stories; - if (args.length === 0) { - stories = [ - '../../core-components/src/**/*.stories.tsx', - '../../../plugins/org/src/**/*.stories.tsx', - '../../../plugins/search/src/**/*.stories.tsx', - ]; - } else { - const rootDir = path.resolve(__dirname, '../../..'); - stories = args.map(arg => path.join(rootDir, arg, 'src/**/*.stories.tsx')); - } + + const rootPath = '../../..'; + const storiesSrcGlob = 'src/**/*.stories.tsx'; + + const getStoriesPath = package => + path.posix.join(rootPath, package, storiesSrcGlob); + + const packages = args.length === 0 ? BACKSTAGE_CORE_STORIES : args; + const stories = packages.map(getStoriesPath); return { stories, diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index fa32a0355e..87e9e12193 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/techdocs-common +## 0.10.6 + +### Patch Changes + +- a2d4389587: 1. Techdocs publisher constructors now use parameter objects when being + instantiated + + 2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for + creation to be aligned with other publishers; this does not impact + `LocalPublish` usage. + + ```diff + - const publisher = new LocalPublish(config, logger, discovery); + + const publisher = LocalPublish.fromConfig(config, logger, discovery); + ``` + +- 6129c89a47: Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5). +- f3c7eec64b: Updated to properly join URL segments under any OS for both AWS S3 and GCP +- Updated dependencies + - @backstage/backend-common@0.9.9 + +## 0.10.5 + +### Patch Changes + +- d207f6ee9e: Support optional bucketRootPath configuration parameter in S3 and GCS publishers +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + ## 0.10.4 ### Patch Changes diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index f5a00c25ea..61371525e5 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -256,7 +256,7 @@ export class TechdocsGenerator implements GeneratorBase { config: Config; scmIntegrations: ScmIntegrationRegistry; }); - static readonly defaultDockerImage = 'spotify/techdocs:v0.3.4'; + static readonly defaultDockerImage = 'spotify/techdocs:v0.3.5'; // (undocumented) static fromConfig( config: Config, diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index fc3a9376dc..25577df1dd 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.4", + "version": "0.10.6", "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.7", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.3", - "@backstage/search-common": "^0.2.0", - "@backstage/integration": "^0.6.7", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/search-common": "^0.2.1", + "@backstage/integration": "^0.6.9", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.4", "@types/express": "^4.17.6", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@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/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index e87278c2d4..728cab5a81 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -44,7 +44,7 @@ export class TechdocsGenerator implements GeneratorBase { * The default docker image (and version) used to generate content. Public * and static so that techdocs-common consumers can use the same version. */ - public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.4'; + public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.5'; private readonly logger: Logger; private readonly containerRunner: ContainerRunner; private readonly options: GeneratorConfig; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 9b74d89dda..1f7afa0855 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -334,15 +334,12 @@ describe('AwsS3Publish', () => { name: 'path', }; - const techDocsMetadaFilePath = path.posix.join( - ...Object.values(invalidEntityName), - 'techdocs_metadata.json', - ); - const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `TechDocs metadata fetch failed; caused by Error: The file ${techDocsMetadaFilePath} does not exist!`, + message: expect.stringMatching( + /TechDocs metadata fetch failed; caused by Error: The file .* does not exist/i, + ), }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 29eecb8d03..5ebc2aab77 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -57,6 +57,26 @@ const streamToBuffer = (stream: Readable): Promise => { }; export class AwsS3Publish implements PublisherBase { + private readonly storageClient: aws.S3; + private readonly bucketName: string; + private readonly legacyPathCasing: boolean; + private readonly logger: Logger; + private readonly bucketRootPath: string; + + constructor(options: { + storageClient: aws.S3; + bucketName: string; + legacyPathCasing: boolean; + logger: Logger; + bucketRootPath: string; + }) { + this.storageClient = options.storageClient; + this.bucketName = options.bucketName; + this.legacyPathCasing = options.legacyPathCasing; + this.logger = options.logger; + this.bucketRootPath = options.bucketRootPath; + } + static fromConfig(config: Config, logger: Logger): PublisherBase { let bucketName = ''; try { @@ -112,13 +132,13 @@ export class AwsS3Publish implements PublisherBase { 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; - return new AwsS3Publish( + return new AwsS3Publish({ storageClient, bucketName, + bucketRootPath, legacyPathCasing, logger, - bucketRootPath, - ); + }); } private static buildCredentials( @@ -152,20 +172,6 @@ export class AwsS3Publish implements PublisherBase { return explicitCredentials; } - constructor( - private readonly storageClient: aws.S3, - private readonly bucketName: string, - private readonly legacyPathCasing: boolean, - private readonly logger: Logger, - private readonly bucketRootPath: string, - ) { - this.storageClient = storageClient; - this.bucketName = bucketName; - this.legacyPathCasing = legacyPathCasing; - this.logger = logger; - this.bucketRootPath = bucketRootPath; - } - /** * Check if the defined bucket exists. Being able to connect means the configuration is good * and the storage client will work. @@ -306,7 +312,7 @@ export class AwsS3Publish implements PublisherBase { ? entityTriplet : lowerCaseEntityTriplet(entityTriplet); - const entityRootDir = path.join(this.bucketRootPath, entityDir); + const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); const stream = this.storageClient .getObject({ @@ -357,7 +363,7 @@ export class AwsS3Publish implements PublisherBase { : lowerCaseEntityTripletInStoragePath(decodedUriNoRoot); // Re-prepend the root path to the relative file path - const filePath = path.join(this.bucketRootPath, filePathNoRoot); + const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot); // Files with different extensions (CSS, HTML) need to be served with different headers const fileExtension = path.extname(filePath); @@ -396,7 +402,7 @@ export class AwsS3Publish implements PublisherBase { ? entityTriplet : lowerCaseEntityTriplet(entityTriplet); - const entityRootDir = path.join(this.bucketRootPath, entityDir); + const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); await this.storageClient .headObject({ diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index f1d60f709a..7a58d92095 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -47,6 +47,23 @@ import { const BATCH_CONCURRENCY = 3; export class AzureBlobStoragePublish implements PublisherBase { + private readonly storageClient: BlobServiceClient; + private readonly containerName: string; + private readonly legacyPathCasing: boolean; + private readonly logger: Logger; + + constructor(options: { + storageClient: BlobServiceClient; + containerName: string; + legacyPathCasing: boolean; + logger: Logger; + }) { + this.storageClient = options.storageClient; + this.containerName = options.containerName; + this.legacyPathCasing = options.legacyPathCasing; + this.logger = options.logger; + } + static fromConfig(config: Config, logger: Logger): PublisherBase { let containerName = ''; try { @@ -95,24 +112,12 @@ export class AzureBlobStoragePublish implements PublisherBase { 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; - return new AzureBlobStoragePublish( - storageClient, - containerName, - legacyPathCasing, - logger, - ); - } - - constructor( - private readonly storageClient: BlobServiceClient, - private readonly containerName: string, - private readonly legacyPathCasing: boolean, - private readonly logger: Logger, - ) { - this.storageClient = storageClient; - this.containerName = containerName; - this.legacyPathCasing = legacyPathCasing; - this.logger = logger; + return new AzureBlobStoragePublish({ + storageClient: storageClient, + containerName: containerName, + legacyPathCasing: legacyPathCasing, + logger: logger, + }); } async getReadiness(): Promise { diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index d31995ea2e..c085594325 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -332,15 +332,10 @@ describe('GoogleGCSPublish', () => { name: 'path', }; - const techDocsMetadaFilePath = path.posix.join( - ...Object.values(invalidEntityName), - 'techdocs_metadata.json', - ); - const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `The file ${techDocsMetadaFilePath} does not exist!`, + message: expect.stringMatching(/The file .* does not exist/i), }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index 5a142e800a..f072412c20 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -41,6 +41,26 @@ import { } from './types'; export class GoogleGCSPublish implements PublisherBase { + private readonly storageClient: Storage; + private readonly bucketName: string; + private readonly legacyPathCasing: boolean; + private readonly logger: Logger; + private readonly bucketRootPath: string; + + constructor(options: { + storageClient: Storage; + bucketName: string; + legacyPathCasing: boolean; + logger: Logger; + bucketRootPath: string; + }) { + this.storageClient = options.storageClient; + this.bucketName = options.bucketName; + this.legacyPathCasing = options.legacyPathCasing; + this.logger = options.logger; + this.bucketRootPath = options.bucketRootPath; + } + static fromConfig(config: Config, logger: Logger): PublisherBase { let bucketName = ''; try { @@ -84,27 +104,13 @@ export class GoogleGCSPublish implements PublisherBase { 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; - return new GoogleGCSPublish( + return new GoogleGCSPublish({ storageClient, bucketName, legacyPathCasing, logger, bucketRootPath, - ); - } - - constructor( - private readonly storageClient: Storage, - private readonly bucketName: string, - private readonly legacyPathCasing: boolean, - private readonly logger: Logger, - private readonly bucketRootPath: string, - ) { - this.storageClient = storageClient; - this.bucketName = bucketName; - this.legacyPathCasing = legacyPathCasing; - this.logger = logger; - this.bucketRootPath = bucketRootPath; + }); } /** @@ -231,7 +237,7 @@ export class GoogleGCSPublish implements PublisherBase { ? entityTriplet : lowerCaseEntityTriplet(entityTriplet); - const entityRootDir = path.join(this.bucketRootPath, entityDir); + const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); const fileStreamChunks: Array = []; this.storageClient @@ -270,7 +276,7 @@ export class GoogleGCSPublish implements PublisherBase { : lowerCaseEntityTripletInStoragePath(decodedUriNoRoot); // Re-prepend the root path to the relative file path - const filePath = path.join(this.bucketRootPath, filePathNoRoot); + const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot); // Files with different extensions (CSS, HTML) need to be served with different headers const fileExtension = path.extname(filePath); @@ -310,7 +316,7 @@ export class GoogleGCSPublish implements PublisherBase { ? entityTriplet : lowerCaseEntityTriplet(entityTriplet); - const entityRootDir = path.join(this.bucketRootPath, entityDir); + const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); this.storageClient .bucket(this.bucketName) diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index c23c72af38..bc7ed6b97d 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -198,10 +198,12 @@ export const getCloudPathForLocalPath = ( ? relativeFilePathTriplet : lowerCaseEntityTriplet(relativeFilePathTriplet); - const destinationWithRoot = path.join( - ...externalStorageRootPath.split(path.posix.sep), + // Again, the / delimiter is intentional, as it represents remote storage. + const destinationWithRoot = [ + // The extra filter prevents unintended double slashes and prefixes. + ...externalStorageRootPath.split(path.posix.sep).filter(s => s !== ''), destination, - ); + ].join('/'); return destinationWithRoot; // Remote storage file relative path }; diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index 38ead236bd..de56580e02 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -63,7 +63,11 @@ describe('local publisher', () => { const mockConfig = new ConfigReader({}); - const publisher = new LocalPublish(mockConfig, logger, testDiscovery); + const publisher = LocalPublish.fromConfig( + mockConfig, + logger, + testDiscovery, + ); const mockEntity = createMockEntity(); const lowerMockEntity = createMockEntity(undefined, true); @@ -90,7 +94,11 @@ describe('local publisher', () => { }, }); - const publisher = new LocalPublish(mockConfig, logger, testDiscovery); + const publisher = LocalPublish.fromConfig( + mockConfig, + logger, + testDiscovery, + ); const mockEntity = createMockEntity(); const lowerMockEntity = createMockEntity(undefined, true); @@ -106,7 +114,11 @@ describe('local publisher', () => { describe('docsRouter', () => { const mockConfig = new ConfigReader({}); - const publisher = new LocalPublish(mockConfig, logger, testDiscovery); + const publisher = LocalPublish.fromConfig( + mockConfig, + logger, + testDiscovery, + ); let app: express.Express; beforeEach(() => { @@ -166,7 +178,7 @@ describe('local publisher', () => { legacyUseCaseSensitiveTripletPaths: true, }, }); - const legacyPublisher = new LocalPublish( + const legacyPublisher = LocalPublish.fromConfig( legacyConfig, logger, testDiscovery, diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index de0072de7a..70b4eb3ff2 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -59,24 +59,37 @@ try { * called "static" at the root of techdocs-backend plugin. */ export class LocalPublish implements PublisherBase { - private legacyPathCasing: boolean; + private readonly legacyPathCasing: boolean; + private readonly logger: Logger; + private readonly discovery: PluginEndpointDiscovery; - // TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers. - // Move the logic of setting staticDocsDir based on config over to fromConfig, - // and set the value as a class parameter. - constructor( - // @ts-ignore - private readonly config: Config, - private readonly logger: Logger, - private readonly discovery: PluginEndpointDiscovery, - ) { - this.config = config; - this.logger = logger; - this.discovery = discovery; - this.legacyPathCasing = + // TODO: Move the logic of setting staticDocsDir based on config over to + // fromConfig, and set the value as a class parameter. + constructor(options: { + logger: Logger; + discovery: PluginEndpointDiscovery; + legacyPathCasing: boolean; + }) { + this.logger = options.logger; + this.discovery = options.discovery; + this.legacyPathCasing = options.legacyPathCasing; + } + + static fromConfig( + config: Config, + logger: Logger, + discovery: PluginEndpointDiscovery, + ): PublisherBase { + const legacyPathCasing = config.getOptionalBoolean( 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; + + return new LocalPublish({ + logger, + discovery, + legacyPathCasing, + }); } async getReadiness(): Promise { diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 51e7b76406..a463157552 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -54,6 +54,20 @@ const bufferToStream = (buffer: Buffer): Readable => { }; export class OpenStackSwiftPublish implements PublisherBase { + private readonly storageClient: SwiftClient; + private readonly containerName: string; + private readonly logger: Logger; + + constructor(options: { + storageClient: SwiftClient; + containerName: string; + logger: Logger; + }) { + this.storageClient = options.storageClient; + this.containerName = options.containerName; + this.logger = options.logger; + } + static fromConfig(config: Config, logger: Logger): PublisherBase { let containerName = ''; try { @@ -78,17 +92,7 @@ export class OpenStackSwiftPublish implements PublisherBase { secret: openStackSwiftConfig.getString('credentials.secret'), }); - return new OpenStackSwiftPublish(storageClient, containerName, logger); - } - - constructor( - private readonly storageClient: SwiftClient, - private readonly containerName: string, - private readonly logger: Logger, - ) { - this.storageClient = storageClient; - this.containerName = containerName; - this.logger = logger; + return new OpenStackSwiftPublish({ storageClient, containerName, logger }); } /* diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index c7c6154114..eb9e3104bf 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -61,10 +61,10 @@ export class Publisher { return OpenStackSwiftPublish.fromConfig(config, logger); case 'local': logger.info('Creating Local publisher for TechDocs'); - return new LocalPublish(config, logger, discovery); + return LocalPublish.fromConfig(config, logger, discovery); default: logger.info('Creating Local publisher for TechDocs'); - return new LocalPublish(config, logger, discovery); + return LocalPublish.fromConfig(config, logger, discovery); } } } diff --git a/packages/test-utils-core/CHANGELOG.md b/packages/test-utils-core/CHANGELOG.md index 24d84eb105..35bdf1b2c6 100644 --- a/packages/test-utils-core/CHANGELOG.md +++ b/packages/test-utils-core/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/test-utils-core +## 0.1.4 + +### Patch Changes + +- bb12aae352: Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. + This should have no impact since this package is considered internal and have no usages outside core packages. + + Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. + ## 0.1.3 ### Patch Changes diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 8256714560..ebe6decddb 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.3", + "version": "0.1.4", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index a47be922b7..6fcacc30fa 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/test-utils +## 0.1.21 + +### Patch Changes + +- 71fd5cd735: Update Keyboard deprecation with a link to the recommended successor +- Updated dependencies + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + +## 0.1.20 + +### Patch Changes + +- bb12aae352: Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. + This should have no impact since this package is considered internal and have no usages outside core packages. + + Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. + +- c5bb1df55d: Bump `msw` to `v0.35.0` to resolve [CVE-2021-32796](https://github.com/advisories/GHSA-5fg8-2547-mr8q). +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.19 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 624fdf28b3..6f5f78de8c 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.19", + "version": "0.1.21", "private": false, "publishConfig": { "access": "public", @@ -29,16 +29,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.18", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@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": "*", - "msw": "^0.29.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-router": "6.0.0-beta.0", @@ -46,7 +46,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/test-utils/src/testUtils/Keyboard.js b/packages/test-utils/src/testUtils/Keyboard.js index 3f4724d562..2a7928f8cd 100644 --- a/packages/test-utils/src/testUtils/Keyboard.js +++ b/packages/test-utils/src/testUtils/Keyboard.js @@ -25,7 +25,7 @@ const codes = { /** * @public - * @deprecated because it has no usages. Perhaps resurfaced in the future when need be. + * @deprecated superseded by {@link @testing-library/user-event#userEvent} */ export class Keyboard { static async type(target, input) { diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 6f55cc6190..cad1df4ecc 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,7 +18,10 @@ import React, { ComponentType, ReactNode, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; -import { createApp } from '@backstage/core-app-api'; +import { ThemeProvider } from '@material-ui/core/styles'; +import { CssBaseline } from '@material-ui/core'; +import MockIcon from '@material-ui/icons/AcUnit'; +import { createSpecializedApp } from '@backstage/core-app-api'; import { BootErrorPageProps, RouteRef, @@ -28,8 +31,34 @@ import { } from '@backstage/core-plugin-api'; import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; +import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; +const mockIcons = { + 'kind:api': MockIcon, + 'kind:component': MockIcon, + 'kind:domain': MockIcon, + 'kind:group': MockIcon, + 'kind:location': MockIcon, + 'kind:system': MockIcon, + 'kind:user': MockIcon, + + brokenImage: MockIcon, + catalog: MockIcon, + scaffolder: MockIcon, + techdocs: MockIcon, + search: MockIcon, + chat: MockIcon, + dashboard: MockIcon, + docs: MockIcon, + email: MockIcon, + github: MockIcon, + group: MockIcon, + help: MockIcon, + user: MockIcon, + warning: MockIcon, +}; + const ErrorBoundaryFallback = ({ error }: { error: Error }) => { throw new Error(`Reached ErrorBoundaryFallback Page with error, ${error}`); }; @@ -90,8 +119,9 @@ export function wrapInTestApp( const { routeEntries = ['/'] } = options; const boundRoutes = new Map(); - const app = createApp({ + const app = createSpecializedApp({ apis: mockApis, + defaultApis, // Bit of a hack to make sure that the default config loader isn't used // as that would force every single test to wait for config loading. configLoader: false as unknown as undefined, @@ -104,13 +134,18 @@ export function wrapInTestApp( ), }, + icons: mockIcons, plugins: [], themes: [ { id: 'light', - theme: lightTheme, title: 'Test App Theme', variant: 'light', + Provider: ({ children }) => ( + + {children} + + ), }, ], bindRoutes: ({ bind }) => { diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts similarity index 96% rename from packages/core-app-api/src/app/defaultApis.ts rename to packages/test-utils/src/testUtils/defaultApis.ts index fb02274cf0..f06e1ba6b7 100644 --- a/packages/core-app-api/src/app/defaultApis.ts +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -34,7 +34,7 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, -} from '../apis'; +} from '@backstage/core-app-api'; import { createApiFactory, @@ -59,8 +59,8 @@ import { atlassianAuthApiRef, } from '@backstage/core-plugin-api'; -import OAuth2Icon from '@material-ui/icons/AcUnit'; - +// TODO(Rugvip): This is just a copy of the createApp default APIs for now, but +// we should clean up this list a bit move more things over to mocks. export const defaultApis = [ createApiFactory({ api: discoveryApiRef, @@ -226,7 +226,7 @@ export const defaultApis = [ provider: { id: 'oidc', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), }), diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 97d889f450..1c8661d7cc 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/theme +## 0.2.13 + +### Patch Changes + +- c11a37710a: Added a warning variant to `DismissableBanner` component. If you are using a + custom theme, you will need to add the optional `palette.banner.warning` color, + otherwise this variant will fall back to the `palette.banner.error` color. + +## 0.2.12 + +### Patch Changes + +- 40cfec8b3f: More theme API cleanup +- a15d028517: More API fixes: mark things public, add docs, fix exports + ## 0.2.11 ### Patch Changes diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 05833e5e91..2dc6ab7b55 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -9,106 +9,130 @@ import { PaletteOptions } from '@material-ui/core/styles/createPalette'; import { Theme } from '@material-ui/core'; import { ThemeOptions } from '@material-ui/core'; -// Warning: (ae-forgotten-export) The symbol "PaletteAdditions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "BackstagePalette" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type BackstagePalette = Palette & PaletteAdditions; +// @public +export type BackstagePalette = Palette & BackstagePaletteAdditions; -// Warning: (ae-missing-release-tag) "BackstagePaletteOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; +// @public +export type BackstagePaletteAdditions = { + status: { + ok: string; + warning: string; + error: string; + pending: string; + running: string; + aborted: string; + }; + border: string; + textContrast: string; + textVerySubtle: string; + textSubtle: string; + highlight: string; + errorBackground: string; + warningBackground: string; + infoBackground: string; + errorText: string; + infoText: string; + warningText: string; + linkHover: string; + link: string; + gold: string; + navigation: { + background: string; + indicator: string; + color: string; + selectedColor: string; + }; + tabbar: { + indicator: string; + }; + bursts: { + fontColor: string; + slackChannelText: string; + backgroundColor: { + default: string; + }; + gradient: { + linear: string; + }; + }; + pinSidebarButton: { + icon: string; + background: string; + }; + banner: { + info: string; + error: string; + text: string; + link: string; + warning?: string; + }; +}; -// Warning: (ae-missing-release-tag) "BackstageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type BackstagePaletteOptions = PaletteOptions & + BackstagePaletteAdditions; + +// @public export interface BackstageTheme extends Theme { // (undocumented) - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; // (undocumented) page: PageTheme; // (undocumented) palette: BackstagePalette; } -// Warning: (ae-missing-release-tag) "BackstageThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface BackstageThemeOptions extends ThemeOptions { // (undocumented) - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; // (undocumented) page: PageTheme; // (undocumented) palette: BackstagePaletteOptions; } -// Warning: (ae-missing-release-tag) "colorVariants" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const colorVariants: Record; -// Warning: (ae-missing-release-tag) "createTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createTheme(options: SimpleThemeOptions): BackstageTheme; -// Warning: (ae-missing-release-tag) "createThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createThemeOptions( options: SimpleThemeOptions, ): BackstageThemeOptions; -// Warning: (ae-missing-release-tag) "createThemeOverrides" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createThemeOverrides(theme: BackstageTheme): Overrides; -// Warning: (ae-missing-release-tag) "darkTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const darkTheme: BackstageTheme; -// Warning: (ae-missing-release-tag) "genPageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function genPageTheme(colors: string[], shape: string): PageTheme; -// Warning: (ae-missing-release-tag) "lightTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const lightTheme: BackstageTheme; -// Warning: (ae-missing-release-tag) "PageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PageTheme = { colors: string[]; shape: string; backgroundImage: string; }; -// Warning: (ae-missing-release-tag) "pageTheme" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const pageTheme: Record; -// Warning: (ae-missing-release-tag) "PageThemeSelector" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type PageThemeSelector = { themeId: string; }; -// Warning: (ae-missing-release-tag) "shapes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const shapes: Record; -// Warning: (ae-missing-release-tag) "SimpleThemeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SimpleThemeOptions = { palette: BackstagePaletteOptions; diff --git a/packages/theme/package.json b/packages/theme/package.json index 3bc209a782..d9e89c969f 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.11", + "version": "0.2.13", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.2" }, "files": [ "dist" diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 4a45516a55..da663e3398 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -17,7 +17,6 @@ import { createTheme as createMuiTheme } from '@material-ui/core/styles'; import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { Overrides } from '@material-ui/core/styles/overrides'; - import { BackstageTheme, BackstageThemeOptions, @@ -28,6 +27,11 @@ import { pageTheme as defaultPageThemes } from './pageTheme'; const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; +/** + * A helper for creating theme options. + * + * @public + */ export function createThemeOptions( options: SimpleThemeOptions, ): BackstageThemeOptions { @@ -84,6 +88,11 @@ export function createThemeOptions( }; } +/** + * A helper for creating theme overrides. + * + * @public + */ export function createThemeOverrides(theme: BackstageTheme): Overrides { return { MuiCssBaseline: { @@ -271,8 +280,12 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { }; } -// Creates a Backstage MUI theme using a palette. -// The theme is created with the common Backstage options and component styles. +/** + * Creates a Backstage MUI theme using a palette. The theme is created with the + * common Backstage options and component styles. + * + * @public + */ export function createTheme(options: SimpleThemeOptions): BackstageTheme { const themeOptions = createThemeOptions(options); const baseTheme = createMuiTheme(themeOptions) as BackstageTheme; diff --git a/packages/theme/src/pageTheme.ts b/packages/theme/src/pageTheme.ts index 5765e13c23..ae81dc8534 100644 --- a/packages/theme/src/pageTheme.ts +++ b/packages/theme/src/pageTheme.ts @@ -15,22 +15,33 @@ */ import { PageTheme } from './types'; -/* - # How to add a shape - 1. Get the svg shape from figma, should be ~1400 wide, ~400 high - and only the white->transparent mask, no colors. - 2. Run it through https://jakearchibald.github.io/svgomg/ - 3. Run that through https://github.com/tigt/mini-svg-data-uri - with something like https://npm.runkit.com/mini-svg-data-uri - 4. Wrap the output in `url("")` - 5. Give it a name and paste it into the `shapes` object below. -*/ +/** + * The default predefined burst shapes. + * + * @public + * @remarks + * + * How to add a shape: + * + * 1. Get the svg shape from figma, should be ~1400 wide, ~400 high + * and only the white-to-transparent mask, no colors. + * 2. Run it through https://jakearchibald.github.io/svgomg/ + * 3. Run that through https://github.com/tigt/mini-svg-data-uri + * with something like https://npm.runkit.com/mini-svg-data-uri + * 4. Wrap the output in `url("")` + * 5. Give it a name and paste it into the `shapes` object below. + */ export const shapes: Record = { wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, wave2: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1764' height='479' x='-229' y='-6' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M0 400h1350C1321 336 525 33 179-2c-345-34-395 236-408 402H0z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1378 177v223H0V217s219 75 327 52C436 246 717-35 965 45s254 144 413 132z'/%3e%3cpath fill='url(%23paint2_linear)' d='M26 400l-78-16c-170 205-44-6-137-30l-4-1 4 1 137 30c37-45 89-110 159-201 399-514-45 238 1176-50 275-65 354-39 91 267H26z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='431' x2='397.3' y1='-599' y2='372.8' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='236.5' x2='446.6' y1='-586' y2='381.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='851.8' x2='640.4' y1='-867.2' y2='363.7' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, round: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='2269' height='1408' x='-610' y='-509' maskUnits='userSpaceOnUse'%3e%3ccircle cx='1212.8' cy='74.8' r='317.5' fill='url(%23paint0_linear)' transform='rotate(-52 1213 75)'/%3e%3ccircle cx='737.8' cy='445.8' r='317.5' fill='url(%23paint1_linear)' transform='rotate(-116 738 446)'/%3e%3ccircle cx='601.8' cy='52.8' r='418.6' fill='url(%23paint2_linear)' transform='rotate(-117 602 53)'/%3e%3ccircle cx='999.8' cy='364' r='389.1' fill='url(%23paint3_linear)' transform='rotate(31 1000 364)'/%3e%3cellipse cx='-109.2' cy='263.5' fill='url(%23paint4_linear)' rx='429.2' ry='465.8' transform='rotate(-85 -109 264)'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='1301.2' x2='161.4' y1='-1879.7' y2='-969.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='826.2' x2='-313.6' y1='-1508.7' y2='-598.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='718.4' x2='-784.3' y1='-2524' y2='-1324.2' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='1108.2' x2='-288.6' y1='-2031.1' y2='-915.9' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint4_linear' x1='10.4' x2='-1626.5' y1='-2603.8' y2='-1399.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, }; +/** + * The color range variants that are used in e.g. colorful bursts. + * + * @public + */ export const colorVariants: Record = { darkGrey: ['#171717', '#383838'], marineBlue: ['#006D8F', '#0049A1'], @@ -43,9 +54,15 @@ export const colorVariants: Record = { pinkSea: ['#C8077A', '#C2297D'], }; -// As the background shapes and colors are decorative, we place them onto -// the page as a css background-image instead of an html element of its own. -// Utility to not have to write colors and shapes twice. +/** + * Utility to not have to write colors and shapes twice. + * + * @public + * @remarks + * + * As the background shapes and colors are decorative, we place them onto the + * page as a css background-image instead of an html element of its own. + */ export function genPageTheme(colors: string[], shape: string): PageTheme { const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors; const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`; @@ -54,6 +71,11 @@ export function genPageTheme(colors: string[], shape: string): PageTheme { return { colors, shape, backgroundImage }; } +/** + * All of the builtin page themes. + * + * @public + */ export const pageTheme: Record = { home: genPageTheme(colorVariants.teal, shapes.wave), documentation: genPageTheme(colorVariants.pinkSea, shapes.wave2), diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index d7a4a6d785..7e99ce8e8a 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -18,6 +18,11 @@ import { createTheme } from './baseTheme'; import { pageTheme } from './pageTheme'; import { yellow } from '@material-ui/core/colors'; +/** + * The default Backstage light theme. + * + * @public + */ export const lightTheme = createTheme({ palette: { type: 'light', @@ -50,6 +55,7 @@ export const lightTheme = createTheme({ error: '#E22134', text: '#FFFFFF', link: '#000000', + warning: '#FF9800', }, border: '#E6E6E6', textContrast: '#000000', @@ -83,6 +89,11 @@ export const lightTheme = createTheme({ pageTheme, }); +/** + * The default Backstage dark theme. + * + * @public + */ export const darkTheme = createTheme({ palette: { type: 'dark', @@ -119,6 +130,7 @@ export const darkTheme = createTheme({ error: '#E22134', text: '#FFFFFF', link: '#000000', + warning: '#FF9800', }, border: '#E6E6E6', textContrast: '#FFFFFF', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 76a1b7eb5c..e1acba3a01 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -20,7 +20,12 @@ import { Palette, } from '@material-ui/core/styles/createPalette'; -type PaletteAdditions = { +/** + * Backstage specific additions to the material-ui palette. + * + * @public + */ +export type BackstagePaletteAdditions = { status: { ok: string; warning: string; @@ -71,30 +76,67 @@ type PaletteAdditions = { error: string; text: string; link: string; + warning?: string; }; }; -export type BackstagePalette = Palette & PaletteAdditions; -export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; +/** + * The full Backstage palette. + * + * @public + */ +export type BackstagePalette = Palette & BackstagePaletteAdditions; +/** + * The full Backstage palette options. + * + * @public + */ +export type BackstagePaletteOptions = PaletteOptions & + BackstagePaletteAdditions; + +/** + * Selector for what page theme to use. + * + * @public + */ export type PageThemeSelector = { themeId: string; }; +/** + * A Backstage theme. + * + * @public + */ export interface BackstageTheme extends Theme { palette: BackstagePalette; page: PageTheme; - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; -} - -export interface BackstageThemeOptions extends ThemeOptions { - palette: BackstagePaletteOptions; - page: PageTheme; - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; } /** - * A simpler configuration for creating a new theme that just tweaks some parts of the backstage one. + * Backstage theme options. + * + * @public + * @remarks + * + * This is essentially a partial theme definition made by the user, that then + * gets merged together with defaults and other values to form the final + * {@link BackstageTheme}. + * + */ +export interface BackstageThemeOptions extends ThemeOptions { + palette: BackstagePaletteOptions; + page: PageTheme; + getPageTheme: (selector: PageThemeSelector) => PageTheme; +} + +/** + * A simpler configuration for creating a new theme that just tweaks some parts + * of the backstage one. + * + * @public */ export type SimpleThemeOptions = { palette: BackstagePaletteOptions; @@ -103,6 +145,11 @@ export type SimpleThemeOptions = { fontFamily?: string; }; +/** + * The theme definitions for a given layout page. + * + * @public + */ export type PageTheme = { colors: string[]; shape: string; diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 26a5b4cdeb..b157795eec 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -23,10 +23,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,17 +36,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx index 19c7796e55..34d12aa032 100644 --- a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -19,12 +19,15 @@ import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); // setup mock response beforeEach(() => { diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 0f9759bf4f..4970f50c04 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -34,17 +34,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 188d81fcd1..78f4b08bd7 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-api-docs +## 0.6.13 + +### Patch Changes + +- 044c38e739: Lazy load all API definition widgets. The widgets use libraries like + `swagger-ui`, `graphiql`, and `@asyncapi/react-component` which are quite heavy + weight. To improve initial load times, the widgets are only loaded once used. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.6.12 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index de36857945..111660ae07 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -84,11 +84,19 @@ export const ApiTypeTitle: ({ apiEntity: ApiEntity; }) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const AsyncApiDefinitionWidget: ({ definition }: Props_5) => JSX.Element; +export const AsyncApiDefinitionWidget: ( + props: AsyncApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "AsyncApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AsyncApiDefinitionWidgetProps = { + definition: string; +}; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ConsumedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -100,7 +108,7 @@ export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element; // Warning: (ae-missing-release-tag) "ConsumingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ConsumingComponentsCard: ({ variant }: Props_6) => JSX.Element; +export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element; // Warning: (ae-missing-release-tag) "defaultDefinitionWidgets" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -169,20 +177,34 @@ export const EntityProvidingComponentsCard: ({ // @public (undocumented) export const HasApisCard: ({ variant }: Props_3) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OpenApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OpenApiDefinitionWidget: ({ definition }: Props_8) => JSX.Element; +export const OpenApiDefinitionWidget: ( + props: OpenApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "OpenApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OpenApiDefinitionWidgetProps = { + definition: string; +}; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PlainApiDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const PlainApiDefinitionWidget: ({ - definition, - language, -}: Props_9) => JSX.Element; +export const PlainApiDefinitionWidget: ( + props: PlainApiDefinitionWidgetProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "PlainApiDefinitionWidgetProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PlainApiDefinitionWidgetProps = { + definition: any; + language: string; +}; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -194,5 +216,5 @@ export const ProvidedApisCard: ({ variant }: Props_4) => JSX.Element; // Warning: (ae-missing-release-tag) "ProvidingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ProvidingComponentsCard: ({ variant }: Props_7) => JSX.Element; +export const ProvidingComponentsCard: ({ variant }: Props_6) => JSX.Element; ``` diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 78ce0c7ed6..61ae26c776 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.12", + "version": "0.6.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,14 +49,14 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", - "swagger-ui-react": "^4.0.0-rc.3", - "swagger-client": "3.16.1" + "swagger-client": "3.16.1", + "swagger-ui-react": "^4.0.0-rc.3" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -64,7 +64,7 @@ "@types/node": "^14.14.32", "@types/swagger-ui-react": "^3.23.3", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx similarity index 87% rename from plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx index 0170ada180..cb89476c03 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.test.tsx @@ -16,9 +16,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; +import { AsyncApiDefinition } from './AsyncApiDefinition'; -describe('', () => { +describe('', () => { it('renders asyncapi spec', async () => { const definition = ` asyncapi: 2.0.0 @@ -40,7 +40,7 @@ components: type: string `; const { getByText, getAllByText } = await renderInTestApp( - , + , ); expect(getByText(/Account Service/i)).toBeInTheDocument(); @@ -51,7 +51,7 @@ components: 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(); diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx new file mode 100644 index 0000000000..1c993f67d7 --- /dev/null +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx @@ -0,0 +1,145 @@ +/* + * 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 AsyncApi from '@asyncapi/react-component'; +import '@asyncapi/react-component/lib/styles/fiori.css'; +import { alpha, makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const useStyles = makeStyles(theme => ({ + root: { + '& .asyncapi': { + 'font-family': 'inherit', + background: 'none', + }, + '& h2': { + ...theme.typography.h6, + }, + '& .text-teal': { + color: theme.palette.primary.main, + }, + '& button': { + ...theme.typography.button, + background: 'none', + boxSizing: 'border-box', + minWidth: 64, + borderRadius: theme.shape.borderRadius, + transition: theme.transitions.create( + ['background-color', 'box-shadow', 'border'], + { + duration: theme.transitions.duration.short, + }, + ), + padding: '5px 15px', + color: theme.palette.primary.main, + 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': { + color: theme.palette.getContrastText(theme.palette.primary.main), + }, + }, + '& .asyncapi__enum': { + color: theme.palette.secondary.main, + }, + '& .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', + color: theme.palette.text.primary, + }, + '& .asyncapi__additional-properties-notice': { + color: theme.palette.text.hint, + }, + '& .asyncapi__code, .asyncapi__code-pre': { + background: theme.palette.background.default, + }, + '& .asyncapi__schema-example-header-title': { + color: theme.palette.text.secondary, + }, + '& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header': + { + 'background-color': 'inherit', + color: theme.palette.text.secondary, + }, + '& .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, + }, + }, +})); + +type Props = { + definition: string; +}; + +export const AsyncApiDefinition = ({ definition }: Props) => { + const classes = useStyles(); + + return ( +
+ +
+ ); +}; diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx index f32b5d01b5..03bc6fa8a6 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinitionWidget.tsx @@ -14,132 +14,27 @@ * limitations under the License. */ -import AsyncApi from '@asyncapi/react-component'; -import '@asyncapi/react-component/lib/styles/fiori.css'; -import { fade, makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -const useStyles = makeStyles(theme => ({ - root: { - '& .asyncapi': { - 'font-family': 'inherit', - background: 'none', - }, - '& h2': { - ...theme.typography.h6, - }, - '& .text-teal': { - color: theme.palette.primary.main, - }, - '& button': { - ...theme.typography.button, - background: 'none', - boxSizing: 'border-box', - minWidth: 64, - borderRadius: theme.shape.borderRadius, - transition: theme.transitions.create( - ['background-color', 'box-shadow', 'border'], - { - duration: theme.transitions.duration.short, - }, - ), - padding: '5px 15px', - color: theme.palette.primary.main, - border: `1px solid ${fade(theme.palette.primary.main, 0.5)}`, - '&:hover': { - textDecoration: 'none', - '&.Mui-disabled': { - backgroundColor: 'transparent', - }, - border: `1px solid ${theme.palette.primary.main}`, - backgroundColor: fade( - 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': { - color: theme.palette.getContrastText(theme.palette.primary.main), - }, - }, - '& .asyncapi__enum': { - color: theme.palette.secondary.main, - }, - '& .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', - color: theme.palette.text.primary, - }, - '& .asyncapi__additional-properties-notice': { - color: theme.palette.text.hint, - }, - '& .asyncapi__code, .asyncapi__code-pre': { - background: theme.palette.background.default, - }, - '& .asyncapi__schema-example-header-title': { - color: theme.palette.text.secondary, - }, - '& .asyncapi__message-headers-header, .asyncapi__message-payload-header, .asyncapi__server-variables-header, .asyncapi__server-security-header': - { - 'background-color': 'inherit', - color: theme.palette.text.secondary, - }, - '& .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, - }, - }, -})); +// The asyncapi component and related CSS has a significant size, only load it +// if the element is actually used. +const LazyAsyncApiDefinition = React.lazy(() => + import('./AsyncApiDefinition').then(m => ({ + default: m.AsyncApiDefinition, + })), +); -type Props = { +export type AsyncApiDefinitionWidgetProps = { definition: string; }; -export const AsyncApiDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - +export const AsyncApiDefinitionWidget = ( + props: AsyncApiDefinitionWidgetProps, +) => { return ( -
- -
+ }> + + ); }; diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts index dcca901be2..7cc49e3138 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; +export type { AsyncApiDefinitionWidgetProps } from './AsyncApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx similarity index 89% rename from plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx index 63e5a73692..fd78fa9204 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.test.tsx @@ -16,9 +16,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget'; +import { GraphQlDefinition } from './GraphQlDefinition'; -describe('', () => { +describe('', () => { it('renders graphql schema', async () => { const definition = ` """Hello World!""" @@ -53,7 +53,7 @@ type Film { }; const { getByText } = await renderInTestApp( - , + , ); expect(getByText(/Film/i)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx new file mode 100644 index 0000000000..d3fddf70cf --- /dev/null +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinition.tsx @@ -0,0 +1,63 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { makeStyles } from '@material-ui/core/styles'; +import GraphiQL from 'graphiql'; +import 'graphiql/graphiql.css'; +import { buildSchema } from 'graphql'; +import React from 'react'; + +const useStyles = makeStyles(() => ({ + root: { + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + }, + graphiQlWrapper: { + flex: 1, + '@global': { + '.graphiql-container': { + boxSizing: 'initial', + height: '100%', + minHeight: '600px', + flex: '1 1 auto', + }, + }, + }, +})); + +type Props = { + definition: string; +}; + +export const GraphQlDefinition = ({ definition }: Props) => { + const classes = useStyles(); + const schema = buildSchema(definition); + + return ( +
+
+ Promise.resolve(null) as any} + schema={schema} + docExplorerOpen + defaultSecondaryEditorOpen={false} + /> +
+
+ ); +}; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx index 8d15ba08a2..1f8a796249 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/GraphQlDefinitionWidget.tsx @@ -14,54 +14,27 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; -import { makeStyles } from '@material-ui/core/styles'; -import 'graphiql/graphiql.css'; -import { buildSchema } from 'graphql'; -import React, { Suspense } from 'react'; import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -const GraphiQL = React.lazy(() => import('graphiql')); +// The graphql component, graphql and related CSS has a significant size, only +// load it if the element is actually used. +const LazyGraphQlDefinition = React.lazy(() => + import('./GraphQlDefinition').then(m => ({ + default: m.GraphQlDefinition, + })), +); -const useStyles = makeStyles(() => ({ - root: { - height: '100%', - display: 'flex', - flexFlow: 'column nowrap', - }, - graphiQlWrapper: { - flex: 1, - '@global': { - '.graphiql-container': { - boxSizing: 'initial', - height: '100%', - minHeight: '600px', - flex: '1 1 auto', - }, - }, - }, -})); - -type Props = { - definition: any; +export type GraphQlDefinitionWidgetProps = { + definition: string; }; -export const GraphQlDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - const schema = buildSchema(definition); - +export const GraphQlDefinitionWidget = ( + props: GraphQlDefinitionWidgetProps, +) => { return ( }> -
-
- Promise.resolve(null) as any} - schema={schema} - docExplorerOpen - defaultSecondaryEditorOpen={false} - /> -
-
+
); }; diff --git a/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts index 9173f492b0..8645b3c372 100644 --- a/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/GraphQlDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget'; +export type { GraphQlDefinitionWidgetProps } from './GraphQlDefinitionWidget'; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx similarity index 87% rename from plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx rename to plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx index 080aa11a3a..735fba86f7 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.test.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.test.tsx @@ -17,9 +17,9 @@ import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; +import { OpenApiDefinition } from './OpenApiDefinition'; -describe('', () => { +describe('', () => { it('renders openapi spec', async () => { const definition = ` openapi: "3.0.0" @@ -39,7 +39,7 @@ paths: description: Success `; const { getByText } = await renderInTestApp( - , + , ); // swagger-ui loads the documentation asynchronously @@ -51,7 +51,7 @@ paths: it('renders error if definition is missing', async () => { const { getByText } = await renderInTestApp( - , + , ); expect(getByText(/No API definition provided/i)).toBeInTheDocument(); }); diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx new file mode 100644 index 0000000000..4a74b238f7 --- /dev/null +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -0,0 +1,132 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import React, { useEffect, useState } from 'react'; +import SwaggerUI from 'swagger-ui-react'; +import 'swagger-ui-react/swagger-ui.css'; + +const useStyles = makeStyles(theme => ({ + root: { + '& .swagger-ui': { + fontFamily: 'inherit', + 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, + }, + [`& .opblock-tag, + .opblock-tag small, + table thead tr td, + table thead tr th`]: { + fontFamily: 'inherit', + color: theme.palette.text.primary, + borderColor: theme.palette.divider, + }, + [`& section.models, + 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, + }, + [`& .opblock .opblock-summary-operation-id, + .opblock .opblock-summary-path, + .opblock .opblock-summary-path__deprecated, + .opblock .opblock-section-header h4, + .parameter__name, + .response-col_status, + .response-col_links, + .responses-inner h4, + .responses-inner h5, + .opblock-section-header .btn, + .tab li, + .info li, + .info p, + .info table, + section.models h4, + .info .title, + table.model tr.description, + .property-row`]: { + fontFamily: 'inherit', + color: theme.palette.text.primary, + }, + [`& .opblock .opblock-section-header, + .model-box, + section.models .model-container`]: { + background: theme.palette.background.default, + }, + [`& .prop-format, + .parameter__in`]: { + color: theme.palette.text.disabled, + }, + [`& .opblock-description-wrapper p, + .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, + }, + [`& .parameter__name.required:after`]: { + color: theme.palette.warning.dark, + }, + [`& .prop-type`]: { + color: theme.palette.primary.main, + }, + }, + }, +})); + +export type OpenApiDefinitionProps = { + definition: string; +}; + +export const OpenApiDefinition = ({ definition }: OpenApiDefinitionProps) => { + const classes = useStyles(); + + // Due to a bug in the swagger-ui-react component, the component needs + // to be created without content first. + const [def, setDef] = useState(''); + + useEffect(() => { + const timer = setTimeout(() => setDef(definition), 0); + return () => clearTimeout(timer); + }, [definition, setDef]); + + return ( +
+ +
+ ); +}; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx index 4c22d29bc2..606e476454 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinitionWidget.tsx @@ -14,81 +14,27 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; -import React, { useEffect, useState } from 'react'; -import SwaggerUI from 'swagger-ui-react'; -import 'swagger-ui-react/swagger-ui.css'; +import { Progress } from '@backstage/core-components'; +import React, { Suspense } from 'react'; -// TODO: Schemas +// The swagger-ui component and related CSS has a significant size, only load it +// if the element is actually used. +const LazyOpenApiDefinition = React.lazy(() => + import('./OpenApiDefinition').then(m => ({ + default: m.OpenApiDefinition, + })), +); -const useStyles = makeStyles(theme => ({ - root: { - '& .swagger-ui, .info h1, .info h2, .info h3, .info h4, .info h': { - 'font-family': 'inherit', - color: theme.palette.text.primary, - }, - '& .scheme-container': { - 'background-color': theme.palette.background.default, - }, - '& .opblock-tag, .opblock-tag small, table thead tr td, table thead tr th': - { - color: theme.palette.text.primary, - 'border-color': theme.palette.divider, - }, - '& section.models, section.models.is-open h4': { - 'border-color': theme.palette.divider, - }, - '& .opblock .opblock-summary-description, .parameter__type, table.headers td, .model-title, .model .property.primitive, section h3': - { - color: theme.palette.text.secondary, - }, - '& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, .opblock .opblock-section-header h4, .parameter__name, .response-col_status, .response-col_links, .responses-inner h4, .swagger-ui .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, .info p, .info table, section.models h4, .info .title, table.model tr.description, .property-row': - { - color: theme.palette.text.primary, - }, - '& .opblock .opblock-section-header, .model-box, section.models .model-container': - { - background: theme.palette.background.default, - }, - '& .prop-format, .parameter__in': { - color: theme.palette.text.disabled, - }, - '& ': { - color: theme.palette.text.primary, - 'border-color': theme.palette.divider, - }, - '& .opblock-description-wrapper p, .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, - }, - '& .parameter__name.required:after': { - color: theme.palette.warning.dark, - }, - '& .prop-type': { - color: theme.palette.primary.main, - }, - }, -})); - -type Props = { +export type OpenApiDefinitionWidgetProps = { definition: string; }; -export const OpenApiDefinitionWidget = ({ definition }: Props) => { - const classes = useStyles(); - - // Due to a bug in the swagger-ui-react component, the component needs - // to be created without content first. - const [def, setDef] = useState(''); - - useEffect(() => { - const timer = setTimeout(() => setDef(definition), 0); - return () => clearTimeout(timer); - }, [definition, setDef]); - +export const OpenApiDefinitionWidget = ( + props: OpenApiDefinitionWidgetProps, +) => { return ( -
- -
+ }> + + ); }; diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts index 94401c92a1..e317d42a59 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; +export type { OpenApiDefinitionWidgetProps } from './OpenApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx index efdd882564..b7f1e8afc0 100644 --- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.tsx @@ -17,13 +17,19 @@ import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; -type Props = { +export type PlainApiDefinitionWidgetProps = { definition: any; language: string; }; -export const PlainApiDefinitionWidget = ({ definition, language }: Props) => { +export const PlainApiDefinitionWidget = ( + props: PlainApiDefinitionWidgetProps, +) => { return ( - + ); }; diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts b/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts index c233652981..562a3e67c1 100644 --- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts +++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/index.ts @@ -15,3 +15,4 @@ */ export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget'; +export type { PlainApiDefinitionWidgetProps } from './PlainApiDefinitionWidget'; diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 1424936280..0658b10ebb 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-backend +## 0.3.18 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/backend-common@0.9.8 + - @backstage/config-loader@0.7.1 + ## 0.3.17 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 3483bfa8bd..d989457c79 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.17", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config-loader": "^0.7.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.8", + "@backstage/config-loader": "^0.7.1", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,10 +42,10 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.1", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index f8896c95ba..a11f561986 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-auth-backend +## 0.4.7 + +### Patch Changes + +- 5ee31f860b: Only use settings that have a value when creating a new FirestoreKeyStore instance +- 3e0e2f09d5: Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification. +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/test-utils@0.1.21 + - @backstage/catalog-client@0.5.1 + +## 0.4.6 + +### Patch Changes + +- 3b767f19c9: Allow OAuth state to be encoded by a stateEncoder. +- Updated dependencies + - @backstage/test-utils@0.1.20 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + ## 0.4.5 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 572f182082..35c93e9537 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -293,6 +293,7 @@ export type GithubProviderOptions = { signIn?: { resolver?: SignInResolver; }; + stateEncoder?: StateEncoder; }; // Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -582,6 +583,12 @@ export type WebMessageResponse = // src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative +// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag +// src/providers/github/provider.d.ts:71:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/providers/github/provider.d.ts:71:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name +// 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/config.d.ts b/plugins/auth-backend/config.d.ts index 25da7fa332..791bd3606b 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -75,6 +75,7 @@ export interface Config { logoutUrl?: string; issuer: string; cert: string; + audience?: string; privateKey?: string; authnContext?: string[]; identifierFormat?: string; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d593db2aa5..e9ec29970b 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.5", + "version": "0.4.7", "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.7", - "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/test-utils": "^0.1.19", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/test-utils": "^0.1.21", "@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.8.0", + "@backstage/cli": "^0.8.2", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", @@ -84,7 +84,7 @@ "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 74e60a0502..3d238adba9 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -15,6 +15,7 @@ */ import { Logger } from 'winston'; +import { pickBy } from 'lodash'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Config } from '@backstage/config'; @@ -64,16 +65,20 @@ export class KeyStores { if (provider === 'firestore') { const settings = ks?.getConfig(provider); - const keyStore = await FirestoreKeyStore.create({ - projectId: settings?.getOptionalString('projectId'), - keyFilename: settings?.getOptionalString('keyFilename'), - host: settings?.getOptionalString('host'), - port: settings?.getOptionalNumber('port'), - ssl: settings?.getOptionalBoolean('ssl'), - path: settings?.getOptionalString('path'), - timeout: settings?.getOptionalNumber('timeout'), - }); - + const keyStore = await FirestoreKeyStore.create( + pickBy( + { + projectId: settings?.getOptionalString('projectId'), + keyFilename: settings?.getOptionalString('keyFilename'), + host: settings?.getOptionalString('host'), + port: settings?.getOptionalNumber('port'), + ssl: settings?.getOptionalBoolean('ssl'), + path: settings?.getOptionalString('path'), + timeout: settings?.getOptionalNumber('timeout'), + }, + value => value !== undefined, + ), + ); await FirestoreKeyStore.verifyConnection(keyStore, logger); return keyStore; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index d224e8d723..e418ab22c2 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -25,6 +25,7 @@ import { } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; +import { OAuthStartRequest, encodeState } from '../../lib/oauth'; const mockFrameHandler = jest.spyOn( helpers, @@ -56,6 +57,9 @@ describe('GithubAuthProvider', () => { authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), + stateEncoder: async (req: OAuthStartRequest) => ({ + encodedState: encodeState(req.state), + }), callbackUrl: 'mock', clientId: 'mock', clientSecret: 'mock', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 82d231edfa..c7e82cc5ff 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -31,6 +31,7 @@ import { AuthProviderFactory, AuthHandler, SignInResolver, + StateEncoder, } from '../types'; import { OAuthAdapter, @@ -66,6 +67,7 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { authorizationUrl?: string; signInResolver?: SignInResolver; authHandler: AuthHandler; + stateEncoder: StateEncoder; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -78,10 +80,12 @@ export class GithubAuthProvider implements OAuthHandlers { private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; + private readonly stateEncoder: StateEncoder; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; + this.stateEncoder = options.stateEncoder; this.tokenIssuer = options.tokenIssuer; this.catalogIdentityClient = options.catalogIdentityClient; this.logger = options.logger; @@ -109,7 +113,7 @@ export class GithubAuthProvider implements OAuthHandlers { async start(req: OAuthStartRequest): Promise { return await executeRedirectStrategy(req, this._strategy, { scope: req.scope, - state: encodeState(req.state), + state: (await this.stateEncoder(req)).encodedState, }); } @@ -209,6 +213,24 @@ export type GithubProviderOptions = { */ resolver?: SignInResolver; }; + + /** + * The state encoder used to encode the 'state' parameter on the OAuth request. + * + * It should return a string that takes the state params (from the request), url encodes the params + * and finally base64 encodes them. + * + * Providing your own stateEncoder will allow you to add addition parameters to the state field. + * + * It is typed as follows: + * export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>; + * + * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail + * (These two values will be set by the req.state by default) + * + * For more information, please see the helper module in ../../oauth/helpers #readState + */ + stateEncoder?: StateEncoder; }; export const createGithubProvider = ( @@ -263,6 +285,12 @@ export const createGithubProvider = ( logger, }); + const stateEncoder: StateEncoder = + options?.stateEncoder ?? + (async (req: OAuthStartRequest): Promise<{ encodedState: string }> => { + return { encodedState: encodeState(req.state) }; + }); + const provider = new GithubAuthProvider({ clientId, clientSecret, @@ -274,6 +302,7 @@ export const createGithubProvider = ( authHandler, tokenIssuer, catalogIdentityClient, + stateEncoder, logger, }); diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index c302c44082..ac5035e9a5 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -15,7 +15,7 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import express from 'express'; import { Session } from 'express-session'; import { JWK, JWT } from 'jose'; @@ -52,7 +52,7 @@ const clientMetadata = { describe('OidcAuthProvider', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 07fc77e4f7..8929a1dbf0 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -125,6 +125,7 @@ export const createSamlProvider = ( 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'), diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 7f7b841c68..089c82b293 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -21,6 +21,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity/types'; +import { OAuthStartRequest } from '../lib/oauth/types'; import { CatalogIdentityClient } from '../lib/catalog'; export type AuthProviderConfig = { @@ -223,3 +224,7 @@ export type AuthHandlerResult = { profile: ProfileInfo }; export type AuthHandler = ( input: AuthResult, ) => Promise; + +export type StateEncoder = ( + req: OAuthStartRequest, +) => Promise<{ encodedState: string }>; diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index a776aea332..d2fe981eb0 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-azure-devops-backend +## 0.2.0 + +### Minor Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-azure-devops-common@0.0.2 + +## 0.1.4 + +### Patch Changes + +- 2eebc9bac3: Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/backend-common@0.9.8 + ## 0.1.3 ### Patch Changes diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index fac6261a39..1eabccc720 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -4,13 +4,13 @@ ```ts import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; -import { BuildResult } from 'azure-devops-node-api/interfaces/BuildInterfaces'; -import { BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Config } from '@backstage/config'; import express from 'express'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { Logger as Logger_2 } from 'winston'; -import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces'; +import { PullRequest } from '@backstage/plugin-azure-devops-common'; +import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; +import { RepoBuild } from '@backstage/plugin-azure-devops-common'; import { WebApi } from 'azure-devops-node-api'; // Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -29,8 +29,6 @@ export class AzureDevOpsApi { projectName: string, repoName: string, ): Promise; - // Warning: (ae-forgotten-export) The symbol "PullRequestOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) getPullRequests( projectName: string, @@ -45,48 +43,11 @@ export class AzureDevOpsApi { ): Promise; } -export { BuildResult }; - -export { BuildStatus }; - // 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(options: RouterOptions): Promise; -// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type PullRequest = { - pullRequestId?: number; - repoName?: string; - title?: string; - uniqueName?: string; - createdBy?: string; - creationDate?: Date; - sourceRefName?: string; - targetRefName?: string; - status?: PullRequestStatus; - isDraft?: boolean; - link: string; -}; - -// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type RepoBuild = { - id?: number; - title: string; - link?: string; - status?: BuildStatus; - result?: BuildResult; - queueTime?: Date; - startTime?: Date; - finishTime?: Date; - source: string; - uniqueName?: string; -}; - // 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) diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index ec2bc84627..901ad0b960 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.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/config": "^0.1.9", + "@backstage/backend-common": "^0.9.9", + "@backstage/config": "^0.1.11", + "@backstage/plugin-azure-devops-common": "^0.0.2", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -30,10 +31,10 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index fdd49c0440..429d608bd6 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -16,17 +16,21 @@ import { Build, + DefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildResult, BuildStatus, - GitPullRequest, - GitRepository, PullRequest, PullRequestStatus, RepoBuild, -} from './types'; +} from '@backstage/plugin-azure-devops-common'; +import { + GitPullRequest, + GitRepository, +} from 'azure-devops-node-api/interfaces/GitInterfaces'; import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; -import { DefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; describe('AzureDevOpsApi', () => { diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 26378496ea..1ed1f1d5f5 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -15,17 +15,19 @@ */ import { - Build, BuildResult, BuildStatus, - GitPullRequest, - GitPullRequestSearchCriteria, - GitRepository, PullRequest, PullRequestOptions, RepoBuild, -} from './types'; +} from '@backstage/plugin-azure-devops-common'; +import { + GitPullRequest, + GitPullRequestSearchCriteria, + GitRepository, +} from 'azure-devops-node-api/interfaces/GitInterfaces'; +import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { WebApi } from 'azure-devops-node-api'; diff --git a/plugins/azure-devops-backend/src/api/index.ts b/plugins/azure-devops-backend/src/api/index.ts index 97c48cb0bf..903c1bdb6a 100644 --- a/plugins/azure-devops-backend/src/api/index.ts +++ b/plugins/azure-devops-backend/src/api/index.ts @@ -15,5 +15,3 @@ */ export { AzureDevOpsApi } from './AzureDevOpsApi'; -export { BuildResult, BuildStatus } from './types'; -export type { RepoBuild, PullRequest } from './types'; diff --git a/plugins/azure-devops-backend/src/api/types.ts b/plugins/azure-devops-backend/src/api/types.ts deleted file mode 100644 index fab61d26c9..0000000000 --- a/plugins/azure-devops-backend/src/api/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Build, - BuildResult, - BuildStatus, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; -import { - GitPullRequest, - GitPullRequestSearchCriteria, - GitRepository, - PullRequestStatus, -} from 'azure-devops-node-api/interfaces/GitInterfaces'; - -export { BuildResult, BuildStatus, PullRequestStatus }; -export type { - Build, - GitPullRequest, - GitPullRequestSearchCriteria, - GitRepository, -}; - -export type RepoBuild = { - id?: number; - title: string; - link?: string; - status?: BuildStatus; - result?: BuildResult; - queueTime?: Date; - startTime?: Date; - finishTime?: Date; - source: string; - uniqueName?: string; -}; - -export type PullRequest = { - pullRequestId?: number; - repoName?: string; - title?: string; - uniqueName?: string; - createdBy?: string; - creationDate?: Date; - sourceRefName?: string; - targetRefName?: string; - status?: PullRequestStatus; - isDraft?: boolean; - link: string; -}; - -export type PullRequestOptions = { - top: number; - status: PullRequestStatus; -}; diff --git a/plugins/azure-devops-backend/src/index.ts b/plugins/azure-devops-backend/src/index.ts index 7a2a347b65..6977d3580f 100644 --- a/plugins/azure-devops-backend/src/index.ts +++ b/plugins/azure-devops-backend/src/index.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { AzureDevOpsApi, BuildResult, BuildStatus } from './api'; -export type { RepoBuild, PullRequest } from './api'; +export { AzureDevOpsApi } from './api'; export * from './service/router'; diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 4143291409..f5d533d2d8 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -14,22 +14,22 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import express from 'express'; -import request from 'supertest'; -import { AzureDevOpsApi } from '../api'; -import { createRouter } from './router'; -import { PullRequest, RepoBuild } from '../api/types'; import { - GitRepository, - PullRequestStatus, -} from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { - Build, BuildResult, BuildStatus, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; + PullRequest, + PullRequestStatus, + RepoBuild, +} 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'; +import express from 'express'; +import { getVoidLogger } from '@backstage/backend-common'; +import request from 'supertest'; describe('createRouter', () => { let azureDevOpsApi: jest.Mocked; diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index b3505eae1c..d61148b4ff 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { PullRequestOptions, PullRequestStatus } from '../api/types'; +import { + PullRequestOptions, + PullRequestStatus, +} from '@backstage/plugin-azure-devops-common'; import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api'; import { AzureDevOpsApi } from '../api'; diff --git a/plugins/azure-devops-common/.eslintrc.js b/plugins/azure-devops-common/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/azure-devops-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md new file mode 100644 index 0000000000..b414cd3ce6 --- /dev/null +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/plugin-azure-devops-common + +## 0.0.2 + +### Patch Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md new file mode 100644 index 0000000000..f03c0b4059 --- /dev/null +++ b/plugins/azure-devops-common/api-report.md @@ -0,0 +1,90 @@ +## API Report File for "@backstage/plugin-azure-devops-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// Warning: (ae-missing-release-tag) "BuildResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum BuildResult { + Canceled = 32, + Failed = 8, + None = 0, + PartiallySucceeded = 4, + Succeeded = 2, +} + +// 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) +export enum BuildStatus { + All = 47, + Cancelling = 4, + Completed = 2, + InProgress = 1, + None = 0, + NotStarted = 32, + Postponed = 8, +} + +// Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PullRequest = { + pullRequestId?: number; + repoName?: string; + title?: string; + uniqueName?: string; + createdBy?: string; + creationDate?: Date; + sourceRefName?: string; + targetRefName?: string; + status?: PullRequestStatus; + isDraft?: boolean; + link: string; +}; + +// Warning: (ae-missing-release-tag) "PullRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PullRequestOptions = { + top: number; + status: PullRequestStatus; +}; + +// Warning: (ae-missing-release-tag) "PullRequestStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum PullRequestStatus { + Abandoned = 2, + Active = 1, + All = 4, + Completed = 3, + NotSet = 0, +} + +// Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RepoBuild = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: Date; + startTime?: Date; + finishTime?: Date; + source: string; + uniqueName?: string; +}; + +// Warning: (ae-missing-release-tag) "RepoBuildOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RepoBuildOptions = { + top?: number; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json new file mode 100644 index 0000000000..a1ddddcbe0 --- /dev/null +++ b/plugins/azure-devops-common/package.json @@ -0,0 +1,37 @@ +{ + "name": "@backstage/plugin-azure-devops-common", + "version": "0.0.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/azure-devops-common" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test --passWithNoTests", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "^0.8.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-devops-common/src/index.ts b/plugins/azure-devops-common/src/index.ts new file mode 100644 index 0000000000..26a854de89 --- /dev/null +++ b/plugins/azure-devops-common/src/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 './types'; diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts new file mode 100644 index 0000000000..eacc2af30d --- /dev/null +++ b/plugins/azure-devops-common/src/types.ts @@ -0,0 +1,128 @@ +/* + * 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 enum BuildResult { + /** + * No result + */ + None = 0, + /** + * The build completed successfully. + */ + Succeeded = 2, + /** + * The build completed compilation successfully but had other errors. + */ + PartiallySucceeded = 4, + /** + * The build completed unsuccessfully. + */ + Failed = 8, + /** + * The build was canceled before starting. + */ + Canceled = 32, +} + +export enum BuildStatus { + /** + * No status. + */ + None = 0, + /** + * The build is currently in progress. + */ + InProgress = 1, + /** + * The build has completed. + */ + Completed = 2, + /** + * The build is cancelling + */ + Cancelling = 4, + /** + * The build is inactive in the queue. + */ + Postponed = 8, + /** + * The build has not yet started. + */ + NotStarted = 32, + /** + * All status. + */ + All = 47, +} + +export type RepoBuild = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: Date; + startTime?: Date; + finishTime?: Date; + source: string; + uniqueName?: string; +}; + +export type RepoBuildOptions = { + top?: number; +}; + +export enum PullRequestStatus { + /** + * Status not set. Default state. + */ + NotSet = 0, + /** + * Pull request is active. + */ + Active = 1, + /** + * Pull request is abandoned. + */ + Abandoned = 2, + /** + * Pull request is completed. + */ + Completed = 3, + /** + * Used in pull request search criteria to include all statuses. + */ + All = 4, +} + +export type PullRequest = { + pullRequestId?: number; + repoName?: string; + title?: string; + uniqueName?: string; + createdBy?: string; + creationDate?: Date; + sourceRefName?: string; + targetRefName?: string; + status?: PullRequestStatus; + isDraft?: boolean; + link: string; +}; + +export type PullRequestOptions = { + top: number; + status: PullRequestStatus; +}; diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 3687c2ca6c..2f75f64236 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-azure-devops +## 0.1.3 + +### Patch Changes + +- b85acc8c35: refactor(`@backstage/plugin-azure-devops`): Consume types from `@backstage/plugin-azure-devops-common`. + Stop re-exporting types from `@backstage/plugin-azure-devops-backend`. + Added new types to `@backstage/plugin-azure-devops-common`. +- 84ace9a29c: Simplified queue time calculation in `BuildTable`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/plugin-azure-devops-common@0.0.2 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.1.2 + +### Patch Changes + +- 7359623e87: Azure DevOps frontend refactoring items from issue #7641 + + - Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions + - Improved documentation to be easier to expand with new features in the future + - Removed Router based on feedback from maintainers + - Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable + +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.1 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 38f3216c7b..235304c9fa 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.1", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,16 +27,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-azure-devops-common": "^0.0.2", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "azure-devops-node-api": "^11.0.1", "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -44,17 +44,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index f0bad53b43..05377b0b7b 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { RepoBuild, RepoBuildOptions } from './types'; +import { + RepoBuild, + RepoBuildOptions, +} from '@backstage/plugin-azure-devops-common'; + import { createApiRef } from '@backstage/core-plugin-api'; export const azureDevOpsApiRef = createApiRef({ diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index e03428e511..4fd4e781c4 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { AzureDevOpsApi } from './AzureDevOpsApi'; -import { RepoBuild, RepoBuildOptions } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { + RepoBuild, + RepoBuildOptions, +} from '@backstage/plugin-azure-devops-common'; + +import { AzureDevOpsApi } from './AzureDevOpsApi'; import { ResponseError } from '@backstage/errors'; export class AzureDevOpsClient implements AzureDevOpsApi { diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx new file mode 100644 index 0000000000..76a19155ba --- /dev/null +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.stories.tsx @@ -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 { + BuildResult, + BuildStatus, + RepoBuild, +} from '@backstage/plugin-azure-devops-common'; + +import { BuildTable } from './BuildTable'; +import { MemoryRouter } from 'react-router'; +import React from 'react'; + +export default { + title: 'Plugins/Azure Devops/Build Table', + component: BuildTable, +}; + +const buildStatuses: Array<[BuildStatus, BuildResult]> = [ + [BuildStatus.InProgress, BuildResult.None], // In Progress + [BuildStatus.Completed, BuildResult.Succeeded], // Succeeded + [BuildStatus.Completed, BuildResult.Failed], // Failed + [BuildStatus.Completed, BuildResult.PartiallySucceeded], // Partially Succeeded + [BuildStatus.Completed, BuildResult.Canceled], // Cancelled + [BuildStatus.Completed, BuildResult.None], // Unknown + [BuildStatus.Cancelling, BuildResult.None], // Cancelling + [BuildStatus.Postponed, BuildResult.None], // Postponed + [BuildStatus.NotStarted, BuildResult.None], // Not Started + [BuildStatus.None, BuildResult.None], // Unknown +]; + +const generateTestData = (rows = 10): RepoBuild[] => { + const repoBuilds: RepoBuild[] = []; + + for (let i = 0; i < rows; i++) { + const [status, result] = buildStatuses[i] ?? [ + BuildStatus.Completed, + BuildResult.Succeeded, + ]; + + repoBuilds.push({ + id: rows - i + 12534, + title: `backstage ci - 1.0.0-preview-${rows - i}`, + status, + result, + queueTime: new Date(Date.now() - i * 60000), + source: 'refs/heads/main', + link: '', + }); + } + + return repoBuilds; +}; + +export const Default = () => ( + + + +); + +export const Empty = () => ( + + + +); + +export const Loading = () => ( + + + +); + +export const ErrorMessage = () => ( + + + +); diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts b/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts index 42408f37b3..cb240aef42 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.test.ts @@ -17,7 +17,7 @@ import { BuildResult, BuildStatus, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; +} from '@backstage/plugin-azure-devops-common'; import { getBuildResultComponent, getBuildStateComponent } from './BuildTable'; import { renderInTestApp } from '@backstage/test-utils'; diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index a308afb81b..5667c65711 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -18,7 +18,8 @@ import { Box, Typography } from '@material-ui/core'; import { BuildResult, BuildStatus, -} from 'azure-devops-node-api/interfaces/BuildInterfaces'; + RepoBuild, +} from '@backstage/plugin-azure-devops-common'; import { Link, ResponseErrorPanel, @@ -34,7 +35,6 @@ import { import { DateTime } from 'luxon'; import React from 'react'; -import { RepoBuild } from '../../api/types'; export const getBuildResultComponent = (result: number | undefined) => { switch (result) { @@ -149,9 +149,7 @@ const columns: TableColumn[] = [ field: 'queueTime', width: 'auto', render: (row: Partial) => - DateTime.fromISO( - row.queueTime ? row.queueTime.toString() : new Date().toString(), - ).toRelative(), + DateTime.fromJSDate(row.queueTime ?? new Date()).toRelative(), }, ]; diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts index df4f76c57e..af2d181c34 100644 --- a/plugins/azure-devops/src/hooks/useRepoBuilds.ts +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { RepoBuild, RepoBuildOptions } from '../api/types'; +import { + RepoBuild, + RepoBuildOptions, +} from '@backstage/plugin-azure-devops-common'; import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants'; import { Entity } from '@backstage/catalog-model'; diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 5bc2f3e519..0c792808bc 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -42,17 +42,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 3cd13cfa8a..731f62b9e9 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-bazaar +## 0.1.3 + +### Patch Changes + +- 4a336fd292: Name extension to remove deprecation warning +- Updated dependencies + - @backstage/cli@0.8.2 + - @backstage/core-components@0.7.3 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.2 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index a5dc9f7b1e..4d18784d2d 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/cli": "^0.8.0", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/cli": "^0.8.2", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.6.3", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,7 +39,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@backstage/dev-utils": "^0.2.12", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bazaar/src/plugin.ts b/plugins/bazaar/src/plugin.ts index f96eaf522d..5eb5e2aacd 100644 --- a/plugins/bazaar/src/plugin.ts +++ b/plugins/bazaar/src/plugin.ts @@ -44,6 +44,7 @@ export const bazaarPlugin = createPlugin({ export const BazaarPage = bazaarPlugin.provide( createRoutableExtension({ + name: 'BazaarPage', component: () => import('./components/HomePage').then(m => m.HomePage), mountPoint: rootRouteRef, }), diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 9123188947..5ebfd453a2 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -50,7 +50,7 @@ "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/bitrise/src/api/bitriseApi.client.test.ts b/plugins/bitrise/src/api/bitriseApi.client.test.ts index dde4e1ef69..a85be2a9cb 100644 --- a/plugins/bitrise/src/api/bitriseApi.client.test.ts +++ b/plugins/bitrise/src/api/bitriseApi.client.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { BitriseClientApi } from './bitriseApi.client'; @@ -24,7 +24,7 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); describe('BitriseClientApi', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx index 43e596e8d7..a206e28de6 100644 --- a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx +++ b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx @@ -18,7 +18,10 @@ import React from 'react'; import { bitriseApiRef } from '../../plugin'; import { BitriseClientApi } from '../../api/bitriseApi.client'; import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; import { useBitriseBuilds } from '../../hooks/useBitriseBuilds'; import { BitriseBuildsTable } from './BitriseBuildsTableComponent'; import { @@ -34,7 +37,7 @@ jest.mock('../../hooks/useBitriseBuilds', () => ({ const server = setupServer(); describe('BitriseBuildsFetchComponent', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); let apis: ApiRegistry; diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 6bac00a7a3..a776f3f329 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.2 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/catalog-model@0.9.6 + ## 0.3.5 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 51c8780a9d..a8c3b71345 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.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-backend": "^0.17.2", "@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.8.0", + "@backstage/cli": "^0.8.1", "@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 7f3b8657f9..ee8aa49065 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.9 + +### Patch Changes + +- 779d7a2304: Tweak logic for msgraph catalog ingesting for display names with security groups + + Previously security groups that weren't mail enabled were imported with UUIDs, now they use the display name. + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.3 + ## 0.2.8 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index d0f664f428..b3c9494f78 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.8", + "version": "0.2.9", "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.5", "@backstage/config": "^0.1.10", - "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-catalog-backend": "^0.17.3", "@microsoft/microsoft-graph-types": "^2.6.0", "cross-fetch": "^3.0.6", "lodash": "^4.17.21", @@ -41,11 +41,11 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/backend-common": "^0.9.9", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/lodash": "^4.14.151", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index cefa4a52ce..6413a97c15 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import * as msal from '@azure/msal-node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -28,7 +28,7 @@ describe('MicrosoftGraphClient', () => { let client: MicrosoftGraphClient; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { confidentialClientApplication.acquireTokenByClientCredential.mockResolvedValue( diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts index cb9c73d7b1..bf13859c41 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.test.ts @@ -21,6 +21,14 @@ describe('normalizeEntityName', () => { expect(normalizeEntityName('User Name')).toBe('user_name'); }); + it('should normalize complex name to valid entity name', () => { + expect(normalizeEntityName('User (Name)')).toBe('user_name'); + }); + + it('should normalize complex name to valid entity name without extra underscore', () => { + expect(normalizeEntityName('User :(Name:)')).toBe('user_name'); + }); + it('should normalize e-mail to valid entity name', () => { expect(normalizeEntityName('user.name@example.com')).toBe( 'user.name_example.com', diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts index c1d2606018..41dbc1aed8 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/helper.ts @@ -15,8 +15,21 @@ */ export function normalizeEntityName(name: string): string { - return name + let cleaned = name .trim() .toLocaleLowerCase() .replace(/[^a-zA-Z0-9_\-\.]/g, '_'); + + // invalid to end with _ + while (cleaned.endsWith('_')) { + cleaned = cleaned.substring(0, cleaned.length - 1); + } + + // cleans up format for groups like 'my group (Reader)' + while (cleaned.includes('__')) { + // replaceAll from node.js >= 15 + cleaned = cleaned.replace('__', '_'); + } + + return cleaned; } diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index fa86d77b27..b0273e1f58 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -445,6 +445,92 @@ describe('read microsoft graph', () => { // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); }); + + it('should read security groups', async () => { + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + mailNickname: 'df546d53-4f5f-4462-b371-d4a855787047', + mailEnabled: false, + securityEnabled: true, + }; + } + + async function* getExampleGroupMembers(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, rootGroup } = await readMicrosoftGraphGroups( + client, + 'tenantid', + { + groupFilter: 'securityEnabled eq true', + }, + ); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(client.getGroups).toBeCalledWith({ + filter: 'securityEnabled eq true', + }); + expect(client.getGroupMembers).toBeCalledTimes(1); + expect(client.getGroupMembers).toBeCalledWith('groupid'); + }); }); describe('resolveRelations', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index fa229ca956..7dd6f83e2e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -251,6 +251,13 @@ export async function readMicrosoftGraphOrganization( return { rootGroup }; } +function extractGroupName(group: MicrosoftGraph.Group): string { + if (group.securityEnabled) { + return group.displayName as string; + } + return (group.mailNickname || group.displayName) as string; +} + export async function defaultGroupTransformer( group: MicrosoftGraph.Group, groupPhoto?: string, @@ -259,7 +266,7 @@ export async function defaultGroupTransformer( return undefined; } - const name = normalizeEntityName(group.mailNickname || group.displayName); + const name = normalizeEntityName(extractGroupName(group)); const entity: GroupEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', diff --git a/plugins/catalog-backend/.snyk b/plugins/catalog-backend/.snyk new file mode 100644 index 0000000000..ad62599e66 --- /dev/null +++ b/plugins/catalog-backend/.snyk @@ -0,0 +1,47 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-TAR-1579155: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579152: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1579147: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536758: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536531: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z + SNYK-JS-TAR-1536528: + - 'sqlite3 > node-gyp > tar': + reason: >- + The only usage is via node-gyp; there is no unpacking of untrusted tar + files + expires: 2022-11-11T14:30:05.581Z + created: 2021-11-11T14:30:05.582Z +patch: {} diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index b098270431..abb50a345c 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-catalog-backend +## 0.17.3 + +### Patch Changes + +- 86bef79ad1: Allow singleton and flexibly nested EntityFilters +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + +## 0.17.2 + +### Patch Changes + +- b9ce1ce2c1: Allow custom LocationAnalyzer in NextCatalogBuilder +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + ## 0.17.1 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index cbef4c90a1..143e31bc21 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -870,11 +870,14 @@ export type EntityAncestryResponse = { // Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type EntityFilter = { - anyOf: { - allOf: EntitiesSearchFilter[]; - }[]; -}; +export type EntityFilter = + | { + allOf: EntityFilter[]; + } + | { + anyOf: EntityFilter[]; + } + | EntitiesSearchFilter; // Warning: (ae-missing-release-tag) "EntityPagination" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1295,6 +1298,7 @@ export class NextCatalogBuilder { setEntityDataParser(parser: CatalogProcessorParser): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen setFieldFormatValidators(validators: Partial): NextCatalogBuilder; + setLocationAnalyzer(locationAnalyzer: LocationAnalyzer): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen setPlaceholderResolver( diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9a44258958..f13ff2d66c 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.17.1", + "version": "0.17.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/search-common": "^0.2.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/search-common": "^0.2.1", "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", @@ -63,8 +63,8 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.8", - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", @@ -72,7 +72,7 @@ "@types/uuid": "^8.0.0", "@types/yup": "^0.29.13", "aws-sdk-mock": "^5.2.1", - "msw": "^0.29.0", + "msw": "^0.35.0", "sqlite3": "^5.0.1", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 0bc0ec4311..df693bfd42 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -22,9 +22,10 @@ import { Entity, EntityRelationSpec } from '@backstage/catalog-model'; * Any (at least one) of the outer sets must match, within which all of the * individual filters must match. */ -export type EntityFilter = { - anyOf: { allOf: EntitiesSearchFilter[] }[]; -}; +export type EntityFilter = + | { allOf: EntityFilter[] } + | { anyOf: EntityFilter[] } + | EntitiesSearchFilter; /** * A pagination rule for entities. diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index a63a001082..b7dc0084f2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -20,7 +20,7 @@ import { UrlReaders, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -39,7 +39,7 @@ describe('UrlReaderProcessor', () => { set: jest.fn(), }; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts index e481355fb4..b38153a193 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { graphql } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; @@ -28,7 +28,7 @@ import { describe('github', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); describe('getOrganizationUsers', () => { it('reads members', async () => { diff --git a/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts b/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts index 5cc31b5be5..6a150f7583 100644 --- a/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/legacy/database/CommonDatabase.ts @@ -46,7 +46,11 @@ import { DbPageInfo, Transaction, } from './types'; -import { EntityPagination } from '../../catalog/types'; +import { EntityPagination, EntitiesSearchFilter } from '../../catalog/types'; + +type LegacyEntityFilter = { + anyOf: { allOf: EntitiesSearchFilter[] }[]; +}; // The number of items that are sent per batch to the database layer, when // doing .batchInsert calls to knex. This needs to be low enough to not cause @@ -217,13 +221,28 @@ export class CommonDatabase implements Database { let entitiesQuery = tx('entities'); - for (const singleFilter of request?.filter?.anyOf ?? []) { + if ( + request?.filter && + (request.filter.hasOwnProperty('key') || + request.filter.hasOwnProperty('allOf')) + ) { + throw new Error( + 'Filters for the legacy CommonDatabase must obey the { anyOf: [{ allOf: [] }] } format.', + ); + } + for (const singleFilter of (request?.filter as LegacyEntityFilter)?.anyOf ?? + []) { entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() { - for (const { - key, - matchValueIn, - matchValueExists, - } of singleFilter.allOf) { + for (const filter of singleFilter.allOf) { + if ( + filter.hasOwnProperty('anyOf') || + filter.hasOwnProperty('allOf') + ) { + throw new Error( + 'Nested filters are not supported in the legacy CommonDatabase', + ); + } + const { key, matchValueIn, matchValueExists } = filter; // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to // make a lot of sense. However, it had abysmal performance on sqlite // when datasets grew large, so we're using IN instead. diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 60a1668f37..835c1dc700 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -122,6 +122,7 @@ export class NextCatalogBuilder { minSeconds: 100, maxSeconds: 150, }); + private locationAnalyzer: LocationAnalyzer | undefined = undefined; constructor(env: CatalogEnvironment) { this.env = env; @@ -176,6 +177,14 @@ export class NextCatalogBuilder { return this; } + /** + * Overwrites the default location analyzer. + */ + setLocationAnalyzer(locationAnalyzer: LocationAnalyzer): NextCatalogBuilder { + this.locationAnalyzer = locationAnalyzer; + return this; + } + /** * Sets what policies to use for validation of entities between the pre- * processing and post-processing stages. All such policies must pass for the @@ -338,7 +347,8 @@ export class NextCatalogBuilder { ); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationAnalyzer = new RepoLocationAnalyzer(logger, integrations); + const locationAnalyzer = + this.locationAnalyzer ?? new RepoLocationAnalyzer(logger, integrations); const locationService = new DefaultLocationService( locationStore, orchestrator, diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts index 082aa47df5..32d5fa843e 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts @@ -23,6 +23,7 @@ import { DbFinalEntitiesRow, DbRefreshStateReferencesRow, DbRefreshStateRow, + DbSearchRow, } from '../database/tables'; import { NextEntitiesCatalog } from './NextEntitiesCatalog'; @@ -73,6 +74,52 @@ describe('NextEntitiesCatalog', () => { } } + async function addEntityToSearch(knex: Knex, entity: Entity) { + const id = uuid(); + const entityRef = stringifyEntityRef(entity); + const entityJson = JSON.stringify(entity); + + await knex('refresh_state').insert({ + entity_id: id, + entity_ref: entityRef, + unprocessed_entity: entityJson, + errors: '[]', + next_update_at: '2031-01-01 23:00:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + + await knex('final_entities').insert({ + entity_id: id, + final_entity: entityJson, + hash: 'h', + stitch_ticket: '', + }); + + await insertSearchRow(knex, id, null, entity); + } + + async function insertSearchRow( + knex: Knex, + id: string, + previousKey: string | null, + previousValue: Object, + ) { + return Promise.all( + Object.entries(previousValue).map(async ([key, value]) => { + const currentKey = `${previousKey ? `${previousKey}.` : ``}${key}`; + if (typeof value === 'object') { + await insertSearchRow(knex, id, currentKey, value); + } else { + await knex('search').insert({ + entity_id: id, + key: currentKey, + value: value, + }); + } + }), + ); + } + describe('entityAncestry', () => { it.each(databases.eachSupportedId())( 'should return the ancestry with one parent, %p', @@ -209,4 +256,106 @@ describe('NextEntitiesCatalog', () => { 60_000, ); }); + + describe('entities', () => { + it.each(databases.eachSupportedId())( + 'should return correct entity for simple filter', + async databaseId => { + const { knex } = await createDatabase(databaseId); + const entity1: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + }; + const entity2: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: { + test: 'test value', + }, + }; + await addEntityToSearch(knex, entity1); + await addEntityToSearch(knex, entity2); + const catalog = new NextEntitiesCatalog(knex); + + const testFilter = { + key: 'spec.test', + matchValueExists: true, + }; + const request = { filter: testFilter }; + const { entities } = await catalog.entities(request); + + expect(entities.length).toBe(1); + expect(entities[0]).toEqual(entity2); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return correct entity for nested filter', + async databaseId => { + const { knex } = await createDatabase(databaseId); + const entity1: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one', org: 'a', desc: 'description' }, + spec: {}, + }; + const entity2: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two', org: 'b', desc: 'description' }, + spec: {}, + }; + const entity3: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'three', org: 'b', color: 'red' }, + spec: {}, + }; + const entity4: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'four', org: 'b', color: 'blue' }, + spec: {}, + }; + await addEntityToSearch(knex, entity1); + await addEntityToSearch(knex, entity2); + await addEntityToSearch(knex, entity3); + await addEntityToSearch(knex, entity4); + const catalog = new NextEntitiesCatalog(knex); + + const testFilter1 = { + key: 'metadata.org', + matchValueExists: true, + matchValueIn: ['b'], + }; + const testFilter2 = { + key: 'metadata.desc', + matchValueExists: true, + }; + const testFilter3 = { + key: 'metadata.color', + matchValueExists: true, + matchValueIn: ['blue'], + }; + const request = { + filter: { + allOf: [ + testFilter1, + { + anyOf: [testFilter2, testFilter3], + }, + ], + }, + }; + const { entities } = await catalog.entities(request); + + expect(entities.length).toBe(2); + expect(entities).toContainEqual(entity2); + expect(entities).toContainEqual(entity4); + }, + ); + }); }); diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index 0891be045d..841e57c921 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -23,6 +23,8 @@ import { EntitiesResponse, EntityAncestryResponse, EntityPagination, + EntityFilter, + EntitiesSearchFilter, } from '../catalog/types'; import { DbFinalEntitiesRow, @@ -73,6 +75,90 @@ function stringifyPagination(input: { limit: number; offset: number }) { return base64; } +function addCondition( + queryBuilder: Knex.QueryBuilder, + db: Knex, + { key, matchValueIn, matchValueExists }: EntitiesSearchFilter, +) { + // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to + // make a lot of sense. However, it had abysmal performance on sqlite + // when datasets grew large, so we're using IN instead. + const matchQuery = db('search') + .select('entity_id') + .where(function keyFilter() { + this.andWhere({ key: key.toLowerCase() }); + if (matchValueExists !== false && matchValueIn) { + if (matchValueIn.length === 1) { + this.andWhere({ value: matchValueIn[0].toLowerCase() }); + } else if (matchValueIn.length > 1) { + this.andWhere( + 'value', + 'in', + matchValueIn.map(v => v.toLowerCase()), + ); + } + } + }); + // Explicitly evaluate matchValueExists as a boolean since it may be undefined + queryBuilder.andWhere( + 'entity_id', + matchValueExists === false ? 'not in' : 'in', + matchQuery, + ); +} + +function isEntitiesSearchFilter( + filter: EntitiesSearchFilter | EntityFilter, +): filter is EntitiesSearchFilter { + return filter.hasOwnProperty('key'); +} + +function isAndEntityFilter( + filter: { allOf: EntityFilter[] } | EntityFilter, +): filter is { allOf: EntityFilter[] } { + return filter.hasOwnProperty('allOf'); +} + +function isOrEntityFilter( + filter: { anyOf: EntityFilter[] } | EntityFilter, +): filter is { anyOf: EntityFilter[] } { + return filter.hasOwnProperty('anyOf'); +} + +function parseFilter( + filter: EntityFilter, + query: Knex.QueryBuilder, + db: Knex, +): Knex.QueryBuilder { + if (isEntitiesSearchFilter(filter)) { + return query.where(function filterFunction() { + addCondition(this, db, filter); + }); + } + + if (isOrEntityFilter(filter)) { + let cumulativeQuery = query; + for (const subFilter of filter.anyOf ?? []) { + cumulativeQuery = cumulativeQuery.orWhere(subQuery => + parseFilter(subFilter, subQuery, db), + ); + } + return cumulativeQuery; + } + + if (isAndEntityFilter(filter)) { + let cumulativeQuery = query; + for (const subFilter of filter.allOf ?? []) { + cumulativeQuery = cumulativeQuery.andWhere(subQuery => + parseFilter(subFilter, subQuery, db), + ); + } + return cumulativeQuery; + } + + return query; +} + export class NextEntitiesCatalog implements EntitiesCatalog { constructor(private readonly database: Knex) {} @@ -80,41 +166,8 @@ export class NextEntitiesCatalog implements EntitiesCatalog { const db = this.database; let entitiesQuery = db('final_entities'); - - for (const singleFilter of request?.filter?.anyOf ?? []) { - entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() { - for (const { - key, - matchValueIn, - matchValueExists, - } of singleFilter.allOf) { - // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to - // make a lot of sense. However, it had abysmal performance on sqlite - // when datasets grew large, so we're using IN instead. - const matchQuery = db('search') - .select('entity_id') - .where(function keyFilter() { - this.andWhere({ key: key.toLowerCase() }); - if (matchValueExists !== false && matchValueIn) { - if (matchValueIn.length === 1) { - this.andWhere({ value: matchValueIn[0].toLowerCase() }); - } else if (matchValueIn.length > 1) { - this.andWhere( - 'value', - 'in', - matchValueIn.map(v => v.toLowerCase()), - ); - } - } - }); - // Explicitly evaluate matchValueExists as a boolean since it may be undefined - this.andWhere( - 'entity_id', - matchValueExists === false ? 'not in' : 'in', - matchQuery, - ); - } - }); + if (request?.filter) { + entitiesQuery = parseFilter(request.filter, entitiesQuery, db); } // TODO: move final_entities to use entity_ref diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index b0dca011ca..e58d66b1f2 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -23,32 +23,32 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@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-use": "^17.2.4", - "classnames": "^2.3.1", "react-router": "6.0.0-beta.0", - "qs": "^6.9.4", - "lodash": "^4.17.15", - "p-limit": "^3.1.0" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", - "@backstage/core-app-api": "^0.1.18", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^13.1.8", "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 6a909c57b7..e123f3b17f 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-graphql +## 0.2.13 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/catalog-model@0.9.6 + ## 0.2.12 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index f5705dcc76..5df4f9ba5c 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.12", + "version": "0.2.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.8", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -43,13 +43,13 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.11", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/catalog-graphql/src/graphql/module.test.ts b/plugins/catalog-graphql/src/graphql/module.test.ts index 3d1219df31..3e835b3111 100644 --- a/plugins/catalog-graphql/src/graphql/module.test.ts +++ b/plugins/catalog-graphql/src/graphql/module.test.ts @@ -21,7 +21,7 @@ import { setupServer } from 'msw/node'; import { ConfigReader } from '@backstage/config'; import { ReaderEntity } from '../service/client'; import { createLogger } from 'winston'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { gql } from 'apollo-server'; describe('Catalog Module', () => { @@ -33,7 +33,7 @@ describe('Catalog Module', () => { }, }); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); describe('Default Entity', () => { beforeEach(() => { diff --git a/plugins/catalog-graphql/src/service/client.test.ts b/plugins/catalog-graphql/src/service/client.test.ts index f10912dc24..8d674a6814 100644 --- a/plugins/catalog-graphql/src/service/client.test.ts +++ b/plugins/catalog-graphql/src/service/client.test.ts @@ -16,11 +16,11 @@ import { CatalogClient } from './client'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; describe('Catalog GraphQL Module', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); const baseUrl = 'http://localhost:1234'; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index fe453ca45d..242231bf81 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.0", "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.12", @@ -46,26 +46,26 @@ "@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", - "lodash": "^4.17.21" + "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 0ddd92e121..276b6d351c 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -50,7 +50,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -58,7 +58,7 @@ import { CatalogImportClient } from './CatalogImportClient'; describe('CatalogImportClient', () => { const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 26999f9e35..79dfa5e72a 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-catalog-react +## 0.6.3 + +### Patch Changes + +- 03b47a476d: export `loadIdentityOwnerRefs` and `loadCatalogOwnerRefs` all the way +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/catalog-client@0.5.1 + - @backstage/core-plugin-api@0.1.13 + - @backstage/core-app-api@0.1.20 + +## 0.6.2 + +### Patch Changes + +- f9cc2509f8: EntityTypePicker can be hidden and have an initial filter value set, similar to EntityKindPicker +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-app-api@0.1.19 + - @backstage/core-plugin-api@0.1.12 + ## 0.6.1 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 06dccf4b7f..c4aa254226 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -15,6 +15,7 @@ import { Context } from 'react'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { IconButton } from '@material-ui/core'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { LinkProps } from '@backstage/core-components'; import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; @@ -658,10 +659,20 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } +// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EntityTypeFilterProps = { + initialFilter?: string; + hidden?: boolean; +}; + // Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityTypePicker: () => JSX.Element | null; +export const EntityTypePicker: ( + props: EntityTypeFilterProps, +) => JSX.Element | null; // 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 "Props" needs to be exported by the entry point index.d.ts @@ -726,6 +737,17 @@ export function getEntitySourceLocation( // @public export function isOwnerOf(owner: Entity, owned: Entity): boolean; +// @public +export function loadCatalogOwnerRefs( + catalogApi: CatalogApi, + identityOwnerRefs: string[], +): Promise; + +// @public +export function loadIdentityOwnerRefs( + identityApi: IdentityApi, +): Promise; + // Warning: (ae-missing-release-tag) "MockEntityListContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -813,8 +835,6 @@ export function useEntityListProvider< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, >(): EntityListContextProps; -// Warning: (ae-missing-release-tag) "useEntityOwnership" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function useEntityOwnership(): { loading: boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 404ba6893f..9778163391 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.1", + "version": "0.6.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,13 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-app-api": "^0.1.18", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", + "@backstage/catalog-client": "^0.5.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -51,8 +51,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@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-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 9912f3d371..8eba11aeca 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -121,4 +121,26 @@ describe('', () => { expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined }); }); + + it('respects the query parameter filter value', async () => { + const updateFilters = jest.fn(); + const queryParameters = { type: 'tool' }; + await renderWithEffects( + + + + , + , + ); + + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter(['tool']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index bc30bf5b17..225193dc93 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,7 +22,13 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -export const EntityTypePicker = () => { +export type EntityTypeFilterProps = { + initialFilter?: string; + hidden?: boolean; +}; + +export const EntityTypePicker = (props: EntityTypeFilterProps) => { + const { hidden, initialFilter } = props; const alertApi = useApi(alertApiRef); const { error, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); @@ -34,7 +40,10 @@ export const EntityTypePicker = () => { severity: 'error', }); } - }, [error, alertApi]); + if (initialFilter) { + setSelectedTypes([initialFilter]); + } + }, [error, alertApi, initialFilter, setSelectedTypes]); if (availableTypes.length === 0 || error) return null; @@ -46,7 +55,7 @@ export const EntityTypePicker = () => { })), ]; - return ( + return hidden ? null : (
@@ -127,12 +130,24 @@ const DetailsPage = () => { > {details?.name && ( )} {details?.name && ( )} @@ -151,16 +166,20 @@ const labels = ( ); -export const ProjectDetailsPage = () => ( - -
- {labels} -
- - - Support Button - - - -
-); +export const ProjectDetailsPage = () => { + const docsRootLink = useRouteRef(rootRouteRef)(); + + return ( + +
+ {labels} +
+ + + Support Button + + + +
+ ); +}; diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index db3999272a..b37dc2fa50 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -15,19 +15,9 @@ */ // NEEDS WORK -import { - Button, - LinearProgress, - Paper, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from '@material-ui/core'; +import { Button, LinearProgress, Tooltip, Typography } from '@material-ui/core'; import React from 'react'; + import { useAsync } from 'react-use'; import { gcpApiRef, Project } from '../../api'; @@ -38,12 +28,15 @@ import { HeaderLabel, Link, Page, + Table, SupportButton, WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { Link as RouterLink } from 'react-router-dom'; + const LongText = ({ text, max }: { text: string; max: number }) => { if (text.length < max) { return {text}; @@ -77,60 +70,59 @@ const PageContents = () => { ); } + function renderLink(id: string) { + return ( + + + + + + ); + } + return ( -
-
- - - Name - Project Number - Project ID - State - Creation Time - - - - {value?.map((project: Project) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - -
- +
+ renderLink(rowData.id), + }, + { + field: 'state', + title: 'State', + }, + { + field: 'creationTime', + title: 'Creation Time', + }, + ]} + data={ + value?.map((project: Project) => ({ + id: project.projectId, + name: project.name, + projectNumber: project?.projectNumber || 'Error', + projectID: project.projectId, + state: project?.lifecycleState || 'Error', + creationTime: project?.createTime || 'Error', + })) || [] + } + options={{ + pageSize: 5, + pageSizeOptions: [5, 10, 25, 50, 100], + }} + /> + ); }; @@ -141,7 +133,12 @@ export const ProjectListPage = () => ( - All your software catalog entities diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index a63f491f48..b1b2c11732 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/integration": "^0.6.8", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -51,7 +51,7 @@ "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 222bc4c851..c8fce8e501 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.4.23 + +### Patch Changes + +- e849f9cc0a: Use `` instead of `` in combination with `Suspense`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.4.22 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7958a97cb8..1a4079a5ca 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.22", + "version": "0.4.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -52,17 +52,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index ce33ae3781..399fed9557 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -14,12 +14,15 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { Accordion, AccordionSummary, CircularProgress, Fade, - LinearProgress, makeStyles, Modal, Theme, @@ -27,15 +30,11 @@ import { Typography, Zoom, } from '@material-ui/core'; - -import React, { Suspense } from 'react'; -import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; -import { useProjectName } from '../useProjectName'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import DescriptionIcon from '@material-ui/icons/Description'; -import { Entity } from '@backstage/catalog-model'; -import { readGitHubIntegrationConfigs } from '@backstage/integration'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import React, { Suspense } 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')); @@ -72,7 +71,7 @@ const DisplayLog = ({ className: string; }) => { return ( - }> + }>
{ describe('github-deployments', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { worker.resetHandlers(); diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 9088c79fd9..c290599c2c 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,17 +44,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 1c5ebe17f6..df1ad6f887 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -55,7 +55,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "react-router-dom": "6.0.0-beta.0" }, "files": [ diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index 6f1f0000ea..9d90dd8782 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -50,7 +50,7 @@ "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index aa35cdf968..06beb827a6 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-home +## 0.4.5 + +### Patch Changes + +- 4a336fd292: Add name option to `createCardExtension` to remove deprecation warnings for extensions without name. Name will be required for extensions in a future release of `core-plugin-api` and therefore also in `createCardExtension`. +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + ## 0.4.4 ### Patch Changes diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 57b0b899e2..99d88fa4b5 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -71,9 +71,11 @@ export const ComponentTabs: ({ export function createCardExtension({ title, components, + name, }: { title: string; components: () => Promise; + name?: string; }): Extension< ({ Renderer, diff --git a/plugins/home/package.json b/plugins/home/package.json index 28b9fa9e66..8020d7af03 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.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,17 +35,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx index 93fe4ceb4f..82394cf1ea 100644 --- a/plugins/home/src/extensions.tsx +++ b/plugins/home/src/extensions.tsx @@ -37,11 +37,14 @@ type RendererProps = { title: string } & ComponentParts; export function createCardExtension({ title, components, + name, }: { title: string; components: () => Promise; + name?: string; }) { return createReactExtension({ + name, component: { lazy: () => components().then(({ Content, Actions, Settings, ContextProvider }) => { diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts index d67ea846a5..b4b9f9b6ee 100644 --- a/plugins/home/src/plugin.ts +++ b/plugins/home/src/plugin.ts @@ -71,6 +71,7 @@ export const ComponentTab = homePlugin.provide( */ export const WelcomeTitle = homePlugin.provide( createComponentExtension({ + name: 'WelcomeTitle', component: { lazy: () => import('./homePageComponents/WelcomeTitle').then(m => m.WelcomeTitle), @@ -80,6 +81,7 @@ export const WelcomeTitle = homePlugin.provide( export const HomePageRandomJoke = homePlugin.provide( createCardExtension<{ defaultCategory?: 'any' | 'programming' }>({ + name: 'HomePageRandomJoke', title: 'Random Joke', components: () => import('./homePageComponents/RandomJoke'), }), diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index efa2d5d8f6..f43f2add09 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-ilert +## 0.1.16 + +### Patch Changes + +- fa325fd89d: Change the version of `@date-io/luxon` from 2.x to 1.x to make it compatible with material-ui-pickers +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.1.15 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 34db6580fe..69c4025181 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.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", - "@date-io/luxon": "2.x", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -39,17 +39,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 635fa080ce..48f4461f7f 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins-backend +## 0.1.7 + +### Patch Changes + +- e8a1c1afe2: Don't require a validation pattern for the Jenkins base URL. +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + ## 0.1.6 ### Patch Changes diff --git a/plugins/jenkins-backend/README.md b/plugins/jenkins-backend/README.md index bac56b6eaf..1413a45136 100644 --- a/plugins/jenkins-backend/README.md +++ b/plugins/jenkins-backend/README.md @@ -12,6 +12,12 @@ This is the backend half of the 2 Jenkins plugins and is responsible for: This plugin needs to be added to an existing backstage instance. +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-jenkins-backend +``` + Typically, this means creating a `src/plugins/jenkins.ts` file and adding a reference to it to `src/index.ts` ### jenkins.ts @@ -42,6 +48,29 @@ export default async function createPlugin({ } ``` +### src/index.ts + +```diff +diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts +index f2b14b2..2c64f47 100644 +--- a/packages/backend/src/index.ts ++++ b/packages/backend/src/index.ts +@@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; + import app from './plugins/app'; ++import jenkins from './plugins/jenkins'; + import scaffolder from './plugins/scaffolder'; +@@ -56,6 +57,7 @@ async function main() { + const authEnv = useHotMemoize(module, () => createEnv('auth')); ++ const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); +@@ -63,6 +65,7 @@ async function main() { + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); ++ apiRouter.use('/jenkins', await jenkins(jenkinsEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); +``` + This plugin must be provided with a JenkinsInfoProvider, this is a strategy object for finding the Jenkins instance and job for an entity. There is a standard one provided, but the Integrator is free to build their own. diff --git a/plugins/jenkins-backend/config.d.ts b/plugins/jenkins-backend/config.d.ts index bfb519bb58..1fc41c1592 100644 --- a/plugins/jenkins-backend/config.d.ts +++ b/plugins/jenkins-backend/config.d.ts @@ -17,7 +17,6 @@ export interface Config { jenkins?: { /** * Default instance baseUrl, can be specified on a named instance called "default" - * @pattern "^https?://" */ baseUrl?: string; /** @@ -39,9 +38,6 @@ export interface Config { */ name: string; - /** - * @pattern "^https?://" - */ baseUrl: string; username: string; /** @visibility secret */ diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index bdae720c0e..7049fc0a32 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.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/catalog-client": "^0.5.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", @@ -35,10 +35,10 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 1af511b16f..2012555d93 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -16,7 +16,7 @@ cd packages/app yarn add @backstage/plugin-jenkins ``` -2. Add and configure the backend plugin according to it's instructions +2. Add and configure the [jenkins-backend](../jenkins-backend) plugin according to it's instructions 3. Add the `EntityJenkinsContent` extension to the `CI/CD` page and `EntityLatestJenkinsRunCard` to the `overview` page in the app (or wherever you'd prefer): diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index fa6a5e6d52..80d8b40322 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -59,7 +59,7 @@ "@types/node": "^14.14.32", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 3697f8de28..2fe42565da 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kafka +## 0.2.20 + +### Patch Changes + +- 91f8644f17: Use IdentityApi to provide Auth Token for KafkaBackendClient Api calls +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + ## 0.2.19 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 6b91797359..888c79db59 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.19", + "version": "0.2.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -47,7 +47,7 @@ "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "jest-when": "^3.1.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts index 988939ff1a..aea5736aad 100644 --- a/plugins/kafka/src/api/KafkaBackendClient.ts +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -15,21 +15,28 @@ */ import { KafkaApi, ConsumerGroupOffsetsResponse } from './types'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; export class KafkaBackendClient implements KafkaApi { private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; } private async internalGet(path: string): Promise { const url = `${await this.discoveryApi.getBaseUrl('kafka')}${path}`; + const idToken = await this.identityApi.getIdToken(); const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), }, }); diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index e389e5e27a..539b0a0fda 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -21,6 +21,7 @@ import { createRoutableExtension, createRouteRef, discoveryApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; export const rootCatalogKafkaRouteRef = createRouteRef({ @@ -33,8 +34,9 @@ export const kafkaPlugin = createPlugin({ apis: [ createApiFactory({ api: kafkaApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new KafkaBackendClient({ discoveryApi, identityApi }), }), ], routes: { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 1b90e65fd7..de0dcdf373 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import { getVoidLogger, loadBackendConfig } from '@backstage/backend-common'; +import { getVoidLogger } from '@backstage/backend-common'; +import { Config, ConfigReader } from '@backstage/config'; +import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import express from 'express'; import request from 'supertest'; -import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { ClusterDetails, FetchResponseWrapper, @@ -27,8 +27,8 @@ import { KubernetesServiceLocator, ObjectFetchParams, } from '../types/types'; -import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { KubernetesBuilder } from './KubernetesBuilder'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; describe('KubernetesBuilder', () => { let app: express.Express; @@ -37,7 +37,12 @@ describe('KubernetesBuilder', () => { beforeAll(async () => { const logger = getVoidLogger(); - config = await loadBackendConfig({ logger, argv: [] }); + config = new ConfigReader({ + kubernetes: { + serviceLocatorMethod: { type: 'multiTenant' }, + clusterLocatorMethods: [{ type: 'config', clusters: [] }], + }, + }); const clusters: ClusterDetails[] = [ { diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index a0a4ef2a51..a757041b32 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-kubernetes +## 0.4.19 + +### Patch Changes + +- 9f3dd76bc7: fix: kubernetes plugin shall pass id token on get clusters request if possible +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.4.18 + +### Patch Changes + +- 14df942bae: The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.4.17 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 22db871ccd..9dbca8aff8 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.17", + "version": "0.4.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", "@backstage/plugin-kubernetes-common": "^0.1.5", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -62,7 +62,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 3f6a0394d3..4d44c021c4 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -79,10 +79,14 @@ export class KubernetesBackendClient implements KubernetesApi { } async getClusters(): Promise<{ name: string; authProvider: string }[]> { + const idToken = await this.identityApi.getIdToken(); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`; const response = await fetch(url, { method: 'GET', + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, }); return (await this.handleResponse(response)).items; diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 8d9cf70bad..f0dca639fc 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,10 +34,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -59,7 +59,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index e9a70bd54b..4c92997755 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp, msw } from '@backstage/test-utils'; +import { wrapInTestApp, setupRequestMockHandlers } from '@backstage/test-utils'; import AuditListTable from './AuditListTable'; import { @@ -36,7 +36,7 @@ describe('AuditListTable', () => { let apis: ApiRegistry; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { apis = ApiRegistry.from([ diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 2781bc4c5b..ea3587fea9 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -23,7 +23,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -44,7 +44,7 @@ describe('AuditList', () => { let apis: ApiRegistry; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { apis = ApiRegistry.from([ diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 8a8146199d..ed1bbbcaf9 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -26,7 +26,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -46,7 +46,7 @@ describe('AuditView', () => { let id: string; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { server.use( diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index ca078c7e84..84493e243f 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -23,7 +23,7 @@ jest.mock('react-router-dom', () => { }; }); -import { msw, wrapInTestApp } from '@backstage/test-utils'; +import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -44,7 +44,7 @@ describe('CreateAudit', () => { let apis: ApiRegistry; let errorApi: ErrorApi; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { errorApi = { post: jest.fn(), error$: jest.fn() }; diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index c44608e2ef..271daa566e 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,17 +43,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/org/package.json b/plugins/org/package.json index 4e89865f44..5348f67523 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,32 +22,32 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@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", - "qs": "^6.10.1" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 35a240b5de..7b42a15bc6 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,17 +48,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "node-fetch": "^2.6.1" }, "files": [ diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index 11a1068b68..df999dbe2a 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import { - getVoidLogger, - loadBackendConfig, - SingleHostDiscovery, -} from '@backstage/backend-common'; +import { getVoidLogger, SingleHostDiscovery } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { Request, Response } from 'express'; import * as http from 'http'; import { createProxyMiddleware, Options } from 'http-proxy-middleware'; @@ -35,7 +32,14 @@ const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction< describe('createRouter', () => { it('works', async () => { const logger = getVoidLogger(); - const config = await loadBackendConfig({ logger, argv: [] }); + const config = new ConfigReader({ + backend: { + baseUrl: 'https://example.com:7000', + listen: { + port: 7000, + }, + }, + }); const discovery = SingleHostDiscovery.fromConfig(config); const router = await createRouter({ config, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index f35852fe74..2e2da0a284 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -61,7 +61,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index bf7db656c5..43a522482f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.1.4 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-scaffolder-backend@0.15.12 + +## 0.1.3 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/plugin-scaffolder-backend@0.15.11 + ## 0.1.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 6fa57fd0f3..132d3d4225 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -10,9 +10,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; import { UrlReader } from '@backstage/backend-common'; -// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 1f004b8804..36a6310aa2 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.2", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.2", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.9", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/plugin-scaffolder-backend": "^0.15.12", + "@backstage/config": "^0.1.11", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "10.0.0", @@ -33,13 +33,13 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", "@types/command-exists": "^1.2.0", "mock-fs": "^5.1.0", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 5120cd3b27..c4d7f1e3e8 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -121,6 +121,15 @@ export class CookiecutterRunner { } } +/** + * Creates a `fetch:cookiecutter` Scaffolder action. + * + * @remarks + * + * See {@link https://cookiecutter.readthedocs.io/} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}. + * @param options - Templating configuration. + * @public + */ export function createFetchCookiecutterAction(options: { reader: UrlReader; integrations: ScmIntegrations; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts index cce8011059..4f0ba9a407 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/index.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/index.ts @@ -15,7 +15,7 @@ */ /** - * A module for the scaffolder backend that lets you template projects using cookiecutter + * A module for the scaffolder backend that lets you template projects using {@link https://cookiecutter.readthedocs.io/ | cookiecutter}. * * @packageDocumentation */ diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 721160b06e..9dcd6bcb88 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.1.7 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/plugin-scaffolder-backend@0.15.12 + +## 0.1.6 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/plugin-scaffolder-backend@0.15.11 + ## 0.1.5 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index 477483f70e..9c0610d4f2 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -8,9 +8,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; import { UrlReader } from '@backstage/backend-common'; -// Warning: (ae-missing-release-tag) "createFetchRailsAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createFetchRailsAction(options: { reader: UrlReader; integrations: ScmIntegrations; diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 5e1c9d0107..f4ace02eca 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.1.5", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,17 +21,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/plugin-scaffolder-backend": "^0.15.10", - "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.2", + "@backstage/backend-common": "^0.9.9", + "@backstage/plugin-scaffolder-backend": "^0.15.12", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", + "@backstage/cli": "^0.8.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 5ce7d46689..00a5690c0a 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -27,6 +27,16 @@ import { import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; +/** + * Creates the `fetch:rails` Scaffolder action. + * + * @remarks + * + * See {@link https://guides.rubyonrails.org/rails_application_templates.html} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}. + * + * @param options - Configuration of the templater. + * @public + */ export function createFetchRailsAction(options: { reader: UrlReader; integrations: ScmIntegrations; diff --git a/plugins/scaffolder-backend-module-rails/src/index.ts b/plugins/scaffolder-backend-module-rails/src/index.ts index eb51c06c49..773e1feabd 100644 --- a/plugins/scaffolder-backend-module-rails/src/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/index.ts @@ -15,7 +15,7 @@ */ /** - * A module for the scaffolder backend that lets you template projects using Rails + * A module for the scaffolder backend that lets you template projects using {@link https://guides.rubyonrails.org/rails_application_templates.html | Rails}. * * @packageDocumentation */ diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md new file mode 100644 index 0000000000..9373564b1c --- /dev/null +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -0,0 +1,18 @@ +# @backstage/plugin-scaffolder-backend-module-yeoman + +## 0.1.2 + +### Patch Changes + +- 290fbb3ec2: Add missing API docs to scaffolder action plugins +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.12 + +## 0.1.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/plugin-scaffolder-backend@0.15.11 diff --git a/plugins/scaffolder-backend-module-yeoman/api-report.md b/plugins/scaffolder-backend-module-yeoman/api-report.md index 1f38d3945c..fc18814faa 100644 --- a/plugins/scaffolder-backend-module-yeoman/api-report.md +++ b/plugins/scaffolder-backend-module-yeoman/api-report.md @@ -5,10 +5,6 @@ ```ts import { TemplateAction } from '@backstage/plugin-scaffolder-backend'; -// Warning: (ae-missing-release-tag) "createRunYeomanAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createRunYeomanAction(): TemplateAction; - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index a1f1635808..8d17df0239 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.1.0", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,14 +20,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.8", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/config": "^0.1.11", + "@backstage/plugin-scaffolder-backend": "^0.15.12", "@backstage/types": "^0.1.1", "winston": "^3.2.1", "yeoman-environment": "^3.6.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.2", + "@backstage/backend-common": "^0.9.9", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts index f6a29037a3..f7b0c54b41 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.ts @@ -18,6 +18,15 @@ import { JsonObject } from '@backstage/types'; import { createTemplateAction } from '@backstage/plugin-scaffolder-backend'; import { yeomanRun } from './yeomanRun'; +/** + * Creates a `run:yeoman` Scaffolder action. + * + * @remarks + * + * See {@link https://yeoman.io/} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}. + * + * @public + */ export function createRunYeomanAction() { return createTemplateAction<{ namespace: string; diff --git a/plugins/scaffolder-backend-module-yeoman/src/index.ts b/plugins/scaffolder-backend-module-yeoman/src/index.ts index 4f06b14a86..1aac8e12f9 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/index.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/index.ts @@ -13,4 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * A module for the scaffolder backend that lets you template projects using + * {@link https://yeoman.io/ | Yeoman}. + * + * @packageDocumentation + */ export * from './actions'; diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index d027ec52e7..de033eabbd 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-scaffolder-backend +## 0.15.12 + +### Patch Changes + +- 9990df8a1f: Expose some classes and interfaces public so TaskWorkers can run externally from the scaffolder API. +- b45a34fb15: Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events. + + This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers. + + It is intended to switch the endpoint globally for the whole instance. + If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`: + + ```tsx + // packages/app/src/apis.ts + + // ... + import { + scaffolderApiRef, + ScaffolderClient, + } from '@backstage/plugin-scaffolder'; + + export const apis: AnyApiFactory[] = [ + // ... + + createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + }, + factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => + new ScaffolderClient({ + discoveryApi, + identityApi, + scmIntegrationsApi, + // use long polling instead of an eventsource + useLongPollingLogs: true, + }), + }), + ]; + ``` + +- a794c341ca: Fix a bug where only file mode 775 is considered an executable +- Updated dependencies + - @backstage/backend-common@0.9.9 + - @backstage/catalog-client@0.5.1 + - @backstage/plugin-catalog-backend@0.17.3 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.4 + +## 0.15.11 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- e55a5dea09: Fixed bug where the mode of an executable file was ignored +- Updated dependencies + - @backstage/plugin-catalog-backend@0.17.2 + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.3 + - @backstage/plugin-scaffolder-common@0.1.1 + ## 0.15.10 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 1a7aebf2f5..3f9b77a3a3 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; +import { Knex } from 'knex'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Octokit } from '@octokit/rest'; @@ -55,6 +56,9 @@ export class CatalogEntityClient { ): Promise; } +// @public +export type CompletedTaskState = 'failed' | 'completed'; + // Warning: (ae-missing-release-tag) "createBuiltinActions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -189,6 +193,64 @@ export const createTemplateAction: < templateAction: TemplateAction, ) => TemplateAction; +// @public +export type CreateWorkerOptions = { + taskBroker: TaskBroker; + actionRegistry: TemplateActionRegistry; + integrations: ScmIntegrations; + workingDirectory: string; + logger: Logger_2; +}; + +// @public +export class DatabaseTaskStore implements TaskStore { + constructor(options: DatabaseTaskStoreOptions); + // (undocumented) + claimTask(): Promise; + // (undocumented) + completeTask({ + taskId, + status, + eventBody, + }: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + // Warning: (ae-forgotten-export) The symbol "DatabaseTaskStoreOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static create(options: DatabaseTaskStoreOptions): Promise; + // (undocumented) + createTask( + spec: TaskSpec, + secrets?: TaskSecrets, + ): Promise<{ + taskId: string; + }>; + // (undocumented) + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + heartbeatTask(taskId: string): Promise; + // (undocumented) + listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ + events: SerializedTaskEvent[]; + }>; + // (undocumented) + listStaleTasks({ timeoutS }: { timeoutS: number }): Promise<{ + tasks: { + taskId: string; + }[]; + }>; +} + +// @public +export type DispatchResult = { + taskId: string; +}; + // Warning: (ae-missing-release-tag) "fetchContents" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -216,9 +278,7 @@ export class OctokitProvider { getOctokit(repoUrl: string): 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) +// @public export interface RouterOptions { // (undocumented) actions?: TemplateAction[]; @@ -235,6 +295,8 @@ export interface RouterOptions { // (undocumented) reader: UrlReader; // (undocumented) + taskBroker?: TaskBroker; + // (undocumented) taskWorkers?: number; } @@ -260,6 +322,216 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { validateEntityKind(entity: Entity): Promise; } +// @public +export type SerializedTask = { + id: string; + spec: TaskSpec; + status: Status; + createdAt: string; + lastHeartbeatAt?: string; + secrets?: TaskSecrets; +}; + +// @public +export type SerializedTaskEvent = { + id: number; + taskId: string; + body: JsonObject; + type: TaskEventType; + createdAt: string; +}; + +// @public +export type Status = + | 'open' + | 'processing' + | 'failed' + | 'cancelled' + | 'completed'; + +// @public +export interface TaskBroker { + // (undocumented) + claim(): Promise; + // (undocumented) + dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; + // (undocumented) + get(taskId: string): Promise; + // (undocumented) + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { + events: SerializedTaskEvent[]; + }, + ) => void, + ): { + unsubscribe: () => void; + }; + // (undocumented) + vacuumTasks(timeoutS: { timeoutS: number }): Promise; +} + +// @public +export interface TaskContext { + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonValue): Promise; + // (undocumented) + done: boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonValue): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; +} + +// @public +export type TaskEventType = 'completion' | 'log'; + +// @public +export class TaskManager implements TaskContext { + // (undocumented) + complete(result: CompletedTaskState, metadata?: JsonObject): Promise; + // (undocumented) + static create( + state: TaskState, + storage: TaskStore, + logger: Logger_2, + ): TaskManager; + // (undocumented) + get done(): boolean; + // (undocumented) + emitLog(message: string, metadata?: JsonObject): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + get secrets(): TaskSecrets | undefined; + // (undocumented) + get spec(): TaskSpec; +} + +// @public +export type TaskSecrets = { + token: string | undefined; +}; + +// @public +export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; + +// @public +export interface TaskSpecV1beta2 { + // (undocumented) + apiVersion: 'backstage.io/v1beta2'; + // (undocumented) + baseUrl?: string; + // (undocumented) + output: { + [name: string]: string; + }; + // (undocumented) + steps: Array<{ + id: string; + name: string; + action: string; + input?: JsonObject; + if?: string | boolean; + }>; + // (undocumented) + values: JsonObject; +} + +// @public +export interface TaskSpecV1beta3 { + // (undocumented) + apiVersion: 'scaffolder.backstage.io/v1beta3'; + // (undocumented) + baseUrl?: string; + // (undocumented) + output: { + [name: string]: JsonValue; + }; + // (undocumented) + parameters: JsonObject; + // Warning: (ae-forgotten-export) The symbol "TaskStep" needs to be exported by the entry point index.d.ts + // + // (undocumented) + steps: TaskStep[]; +} + +// @public +export interface TaskState { + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; + // (undocumented) + taskId: string; +} + +// @public +export interface TaskStore { + // (undocumented) + claimTask(): Promise; + // (undocumented) + completeTask(options: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + // (undocumented) + createTask( + task: TaskSpec, + secrets?: TaskSecrets, + ): Promise<{ + taskId: string; + }>; + // (undocumented) + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + // (undocumented) + getTask(taskId: string): Promise; + // (undocumented) + heartbeatTask(taskId: string): Promise; + // (undocumented) + listEvents({ taskId, after }: TaskStoreListEventsOptions): Promise<{ + events: SerializedTaskEvent[]; + }>; + // (undocumented) + listStaleTasks(options: { timeoutS: number }): Promise<{ + tasks: { + taskId: string; + }[]; + }>; +} + +// @public +export type TaskStoreEmitOptions = { + taskId: string; + body: JsonObject; +}; + +// @public +export type TaskStoreListEventsOptions = { + taskId: string; + after?: number | undefined; +}; + +// @public +export class TaskWorker { + // (undocumented) + static create(options: CreateWorkerOptions): Promise; + // (undocumented) + runOneTask(task: TaskContext): Promise; + // (undocumented) + start(): void; +} + // Warning: (ae-missing-release-tag) "TemplateAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cfea66284d..e5d13801ba 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.10", + "version": "0.15.12", "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.7", - "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/plugin-catalog-backend": "^0.17.1", - "@backstage/plugin-scaffolder-common": "^0.1.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", + "@backstage/backend-common": "^0.9.9", + "@backstage/catalog-client": "^0.5.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/plugin-catalog-backend": "^0.17.3", + "@backstage/plugin-scaffolder-common": "^0.1.1", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.4", "@backstage/types": "^0.1.1", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", @@ -65,14 +65,14 @@ "luxon": "^2.0.2", "morgan": "^1.10.0", "nunjucks": "^3.2.3", - "octokit-plugin-create-pull-request": "^3.9.3", + "octokit-plugin-create-pull-request": "^3.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", @@ -81,7 +81,7 @@ "@types/supertest": "^2.0.8", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3", "yaml": "^1.10.0" }, diff --git a/plugins/scaffolder-backend/sample-templates/all-templates.yaml b/plugins/scaffolder-backend/sample-templates/all-templates.yaml index 2d5adb8e3b..6637c9479b 100644 --- a/plugins/scaffolder-backend/sample-templates/all-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -6,7 +6,6 @@ metadata: spec: targets: - ./remote-templates.yaml - # For local development of a template, you can reference your local templates here. # Examples: # diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 4eee4b4b96..2678abd9dd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -19,7 +19,7 @@ jest.mock('../helpers'); import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; @@ -60,7 +60,7 @@ describe('publish:bitbucket', () => { createTemporaryDirectory: jest.fn(), }; const server = setupServer(); - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index ed72b14a65..0d4deb87e0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -98,7 +98,11 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'file.txt': 'Hello there!', + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, }, }, ], @@ -167,7 +171,11 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'foo.txt': 'Hello there!', + 'foo.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, }, }, ], @@ -221,7 +229,147 @@ describe('createPublishGithubPullRequestAction', () => { { commit: 'Create my new app', files: { - 'file.txt': 'Hello there!', + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + }); + + it('creates outputs for the url', async () => { + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + }); + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + }); + + describe('with executable file mode 755', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + }; + + mockFs({ + [workspacePath]: { + 'hello.sh': mockFs.file({ + content: 'echo Hello there!', + mode: 0o100755, + }), + }, + }); + + ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + }); + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + changes: [ + { + commit: 'Create my new app', + files: { + 'hello.sh': { + content: Buffer.from('echo Hello there!').toString('base64'), + encoding: 'base64', + mode: '100755', + }, + }, + }, + ], + }); + }); + + it('creates outputs for the url', async () => { + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + }); + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + }); + + describe('with executable file mode 775', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + }; + + mockFs({ + [workspacePath]: { + 'hello.sh': mockFs.file({ + content: 'echo Hello there!', + mode: 0o100775, + }), + }, + }); + + ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + }); + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + changes: [ + { + commit: 'Create my new app', + files: { + 'hello.sh': { + content: Buffer.from('echo Hello there!').toString('base64'), + encoding: 'base64', + mode: '100755', + }, }, }, ], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 253fd72b38..0cceb47d71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { readFile } from 'fs-extra'; +import fs from 'fs-extra'; import path from 'path'; -import { parseRepoUrl } from './util'; +import { parseRepoUrl, isExecutable } from './util'; import { GithubCredentialsProvider, @@ -30,6 +30,8 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request'; import globby from 'globby'; import { resolveSafeChildPath } from '@backstage/backend-common'; +export type Encoding = 'utf-8' | 'base64'; + class GithubResponseError extends CustomErrorBase {} type CreatePullRequestResponse = { @@ -194,7 +196,30 @@ export const createPublishGithubPullRequestAction = ({ }); const fileContents = await Promise.all( - localFilePaths.map(p => readFile(path.resolve(fileRoot, p))), + localFilePaths.map(filePath => { + const absPath = path.resolve(fileRoot, filePath); + const base64EncodedContent = fs + .readFileSync(absPath) + .toString('base64'); + const fileStat = fs.statSync(absPath); + // See the properties of tree items + // in https://docs.github.com/en/rest/reference/git#trees + const githubTreeItemMode = isExecutable(fileStat.mode) + ? '100755' + : '100644'; + // Always use base64 encoding to avoid doubling a binary file in size + // due to interpreting a binary file as utf-8 and sending github + // the utf-8 encoded content. + // + // For example, the original gradle-wrapper.jar is 57.8k in https://github.com/kennethzfeng/pull-request-test/pull/5/files. + // Its size could be doubled to 98.3K (See https://github.com/kennethzfeng/pull-request-test/pull/4/files) + const encoding: Encoding = 'base64'; + return { + encoding: encoding, + content: base64EncodedContent, + mode: githubTreeItemMode, + }; + }), ); const repoFilePaths = localFilePaths.map(repoFilePath => { @@ -203,10 +228,7 @@ export const createPublishGithubPullRequestAction = ({ const changes = [ { - files: zipObject( - repoFilePaths, - fileContents.map(buf => buf.toString()), - ), + files: zipObject(repoFilePaths, fileContents), commit: title, }, ]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts index f179289624..cc879d4f55 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import { getRepoSourceDirectory } from './util'; +import { getRepoSourceDirectory, isExecutable } from './util'; describe('getRepoSourceDirectory', () => { it('should return workspace root if no sub folder is given', () => { @@ -55,3 +55,33 @@ describe('getRepoSourceDirectory', () => { ).toEqual(path.join('/', 'var', 'workspace', 'absolute', 'secret')); }); }); + +describe('isExecutable', () => { + it('should return true for file mode 777', () => { + expect(isExecutable(0o100777)).toBe(true); + }); + it('should return true for file mode 775', () => { + expect(isExecutable(0o100775)).toBe(true); + }); + it('should return true for file mode 755', () => { + expect(isExecutable(0o100755)).toBe(true); + }); + it('should return true for file mode 700', () => { + expect(isExecutable(0o100700)).toBe(true); + }); + it('should return true for file mode 770', () => { + expect(isExecutable(0o100770)).toBe(true); + }); + it('should return true for file mode 670', () => { + expect(isExecutable(0o100670)).toBe(true); + }); + it('should return false for file mode 644', () => { + expect(isExecutable(0o100644)).toBe(false); + }); + it('should return false for file mode 600', () => { + expect(isExecutable(0o100600)).toBe(false); + }); + it('should return false for file mode 640', () => { + expect(isExecutable(0o100640)).toBe(false); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index 58ba5bfdd1..49156e7d97 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -101,3 +101,8 @@ export const parseRepoUrl = ( return { host, owner, repo, organization, workspace, project }; }; +export const isExecutable = (fileMode: number) => { + const executeBitMask = 0o000111; + const res = fileMode & executeBitMask; + return res > 0; +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index 284e372b4c..e055223d8c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export * from './actions'; +export * from './tasks'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 9c72a310c1..813d8d0f5b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -20,15 +20,15 @@ import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { - DbTaskEventRow, - DbTaskRow, + SerializedTaskEvent, + SerializedTask, Status, TaskEventType, TaskSecrets, TaskSpec, TaskStore, TaskStoreEmitOptions, - TaskStoreGetEventsOptions, + TaskStoreListEventsOptions, } from './types'; import { DateTime } from 'luxon'; @@ -54,17 +54,37 @@ export type RawDbTaskEventRow = { created_at: string; }; +/** + * DatabaseTaskStore + * + * @public + */ +export type DatabaseTaskStoreOptions = { + database: Knex; +}; + +/** + * DatabaseTaskStore + * + * @public + */ export class DatabaseTaskStore implements TaskStore { - static async create(knex: Knex): Promise { - await knex.migrate.latest({ + private readonly db: Knex; + + static async create( + options: DatabaseTaskStoreOptions, + ): Promise { + await options.database.migrate.latest({ directory: migrationsDir, }); - return new DatabaseTaskStore(knex); + return new DatabaseTaskStore(options); } - constructor(private readonly db: Knex) {} + constructor(options: DatabaseTaskStoreOptions) { + this.db = options.database; + } - async getTask(taskId: string): Promise { + async getTask(taskId: string): Promise { const [result] = await this.db('tasks') .where({ id: taskId }) .select(); @@ -101,7 +121,7 @@ export class DatabaseTaskStore implements TaskStore { return { taskId }; } - async claimTask(): Promise { + async claimTask(): Promise { return this.db.transaction(async tx => { const [task] = await tx('tasks') .where({ @@ -243,7 +263,7 @@ export class DatabaseTaskStore implements TaskStore { async listEvents({ taskId, after, - }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { + }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[] }> { const rawEvents = await this.db('task_events') .where({ task_id: taskId, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts index d6d1b0cfd2..d07ff29fae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts @@ -18,16 +18,16 @@ import mockFs from 'mock-fs'; import * as winston from 'winston'; import { getVoidLogger } from '@backstage/backend-common'; -import { DefaultWorkflowRunner } from './DefaultWorkflowRunner'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { Task, TaskSpec } from './types'; +import { TaskContext, TaskSpec } from './types'; describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); - let runner: DefaultWorkflowRunner; + let runner: NunjucksWorkflowRunner; let fakeActionHandler: jest.Mock; const integrations = ScmIntegrations.fromConfig( @@ -38,7 +38,7 @@ describe('DefaultWorkflowRunner', () => { }), ); - const createMockTaskWithSpec = (spec: TaskSpec): Task => ({ + const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ spec, complete: async () => {}, done: false, @@ -88,7 +88,7 @@ describe('DefaultWorkflowRunner', () => { }, }); - runner = new DefaultWorkflowRunner({ + runner = new NunjucksWorkflowRunner({ actionRegistry, integrations, workingDirectory: '/tmp', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts similarity index 98% rename from plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts index 6d43ae978c..77d0a7be09 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { - Task, + TaskContext, WorkflowRunner, WorkflowResponse, TaskSpecV1beta2, @@ -47,7 +47,7 @@ const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced * with the default workflow runner interface in the future so this entire thing can go bye bye. */ -export class LegacyWorkflowRunner implements WorkflowRunner { +export class HandlebarsWorkflowRunner implements WorkflowRunner { private readonly handlebars: typeof Handlebars; constructor(private readonly options: Options) { @@ -72,7 +72,7 @@ export class LegacyWorkflowRunner implements WorkflowRunner { this.handlebars.registerHelper('eq', (a, b) => a === b); } - async execute(task: Task): Promise { + async execute(task: TaskContext): Promise { if (!isValidTaskSpec(task.spec)) { throw new InputError(`Task spec is not a valid v1beta2 task spec`); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts index 2714f1b80c..91e772dc4e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/LegacyWorkflowRunner.test.ts @@ -20,12 +20,12 @@ import { createTemplateAction, TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; -import { Task, TaskSpec } from './types'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; +import { TaskContext, TaskSpec } from './types'; import { RepoSpec } from '../actions/builtin/publish/util'; describe('LegacyWorkflowRunner', () => { - let runner: LegacyWorkflowRunner; + let runner: HandlebarsWorkflowRunner; const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); @@ -37,7 +37,7 @@ describe('LegacyWorkflowRunner', () => { }), ); - const createMockTaskWithSpec = (spec: TaskSpec): Task => ({ + const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ spec, complete: async () => {}, done: false, @@ -60,7 +60,7 @@ describe('LegacyWorkflowRunner', () => { }, }); - runner = new LegacyWorkflowRunner({ + runner = new HandlebarsWorkflowRunner({ actionRegistry, integrations, workingDirectory: '/tmp', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts similarity index 95% rename from plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 6c68e0c4c5..587a93e1f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -15,7 +15,7 @@ */ import { ScmIntegrations } from '@backstage/integration'; import { - Task, + TaskContext, TaskSpec, TaskSpecV1beta3, TaskStep, @@ -34,7 +34,7 @@ import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; -type Options = { +type NunjucksWorkflowRunnerOptions = { workingDirectory: string; actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; @@ -52,7 +52,13 @@ const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { return taskSpec.apiVersion === 'scaffolder.backstage.io/v1beta3'; }; -const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { +const createStepLogger = ({ + task, + step, +}: { + task: TaskContext; + step: TaskStep; +}) => { const metadata = { stepId: step.id }; const taskLogger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', @@ -77,7 +83,7 @@ const createStepLogger = ({ task, step }: { task: Task; step: TaskStep }) => { return { taskLogger, streamLogger }; }; -export class DefaultWorkflowRunner implements WorkflowRunner { +export class NunjucksWorkflowRunner implements WorkflowRunner { private readonly nunjucks: nunjucks.Environment; private readonly nunjucksOptions: nunjucks.ConfigureOptions = { @@ -88,7 +94,7 @@ export class DefaultWorkflowRunner implements WorkflowRunner { }, }; - constructor(private readonly options: Options) { + constructor(private readonly options: NunjucksWorkflowRunnerOptions) { this.nunjucks = nunjucks.configure(this.nunjucksOptions); // TODO(blam): let's work out how we can deprecate these. @@ -162,7 +168,7 @@ export class DefaultWorkflowRunner implements WorkflowRunner { }); } - async execute(task: Task): Promise { + async execute(task: TaskContext): Promise { if (!isValidTaskSpec(task.spec)) { throw new InputError( 'Wrong template version executed with the workflow engine', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 1a4d0d68c5..76fb8e00f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -17,8 +17,8 @@ import { getVoidLogger, DatabaseManager } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; -import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; -import { TaskSecrets, TaskSpec, DbTaskEventRow } from './types'; +import { StorageTaskBroker, TaskManager } from './StorageTaskBroker'; +import { TaskSecrets, TaskSpec, SerializedTaskEvent } from './types'; async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -31,7 +31,9 @@ async function createStore(): Promise { }, }), ).forPlugin('scaffolder'); - return await DatabaseTaskStore.create(await manager.getClient()); + return await DatabaseTaskStore.create({ + database: await manager.getClient(), + }); } describe('StorageTaskBroker', () => { @@ -46,7 +48,7 @@ describe('StorageTaskBroker', () => { it('should claim a dispatched work item', async () => { const broker = new StorageTaskBroker(storage, logger); await broker.dispatch({} as TaskSpec); - await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); + await expect(broker.claim()).resolves.toEqual(expect.any(TaskManager)); }); it('should wait for a dispatched work item', async () => { @@ -56,7 +58,7 @@ describe('StorageTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); await broker.dispatch({} as TaskSpec); - await expect(promise).resolves.toEqual(expect.any(TaskAgent)); + await expect(promise).resolves.toEqual(expect.any(TaskManager)); }); it('should dispatch multiple items and claim them in order', async () => { @@ -68,9 +70,9 @@ describe('StorageTaskBroker', () => { const taskA = await broker.claim(); const taskB = await broker.claim(); const taskC = await broker.claim(); - await expect(taskA).toEqual(expect.any(TaskAgent)); - await expect(taskB).toEqual(expect.any(TaskAgent)); - await expect(taskC).toEqual(expect.any(TaskAgent)); + await expect(taskA).toEqual(expect.any(TaskManager)); + await expect(taskB).toEqual(expect.any(TaskManager)); + await expect(taskC).toEqual(expect.any(TaskManager)); await expect(taskA.spec.steps[0].id).toBe('a'); await expect(taskB.spec.steps[0].id).toBe('b'); await expect(taskC.spec.steps[0].id).toBe('c'); @@ -127,8 +129,8 @@ describe('StorageTaskBroker', () => { const { taskId } = await broker1.dispatch({} as TaskSpec); - const logPromise = new Promise(resolve => { - const observedEvents = new Array(); + const logPromise = new Promise(resolve => { + const observedEvents = new Array(); broker2.observe({ taskId, after: undefined }, (_err, { events }) => { observedEvents.push(...events); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 2a99c3eb30..7e7ebe5436 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -18,23 +18,28 @@ import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; import { CompletedTaskState, - Task, + TaskContext, TaskSecrets, TaskSpec, TaskStore, TaskBroker, DispatchResult, - DbTaskEventRow, - DbTaskRow, + SerializedTaskEvent, + SerializedTask, } from './types'; -export class TaskAgent implements Task { +/** + * TaskManager + * + * @public + */ +export class TaskManager implements TaskContext { private isDone = false; private heartbeatTimeoutId?: ReturnType; static create(state: TaskState, storage: TaskStore, logger: Logger) { - const agent = new TaskAgent(state, storage, logger); + const agent = new TaskManager(state, storage, logger); agent.startTimeout(); return agent; } @@ -104,7 +109,12 @@ export class TaskAgent implements Task { } } -interface TaskState { +/** + * TaskState + * + * @public + */ +export interface TaskState { spec: TaskSpec; taskId: string; secrets?: TaskSecrets; @@ -125,11 +135,11 @@ export class StorageTaskBroker implements TaskBroker { ) {} private deferredDispatch = defer(); - async claim(): Promise { + async claim(): Promise { for (;;) { const pendingTask = await this.storage.claimTask(); if (pendingTask) { - return TaskAgent.create( + return TaskManager.create( { taskId: pendingTask.id, spec: pendingTask.spec, @@ -155,7 +165,7 @@ export class StorageTaskBroker implements TaskBroker { }; } - async get(taskId: string): Promise { + async get(taskId: string): Promise { return this.storage.getTask(taskId); } @@ -166,9 +176,9 @@ export class StorageTaskBroker implements TaskBroker { }, callback: ( error: Error | undefined, - result: { events: DbTaskEventRow[] }, + result: { events: SerializedTaskEvent[] }, ) => void, - ): () => void { + ): { unsubscribe: () => void } { const { taskId } = options; let cancelled = false; @@ -195,7 +205,7 @@ export class StorageTaskBroker implements TaskBroker { } })(); - return unsubscribe; + return { unsubscribe }; } async vacuumTasks(timeoutS: { timeoutS: number }): Promise { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 87a0229b5d..9b6baf3b84 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -19,8 +19,20 @@ import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; -import { WorkflowRunner } from './types'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; +import { ScmIntegrations } from '@backstage/integration'; +import { TemplateActionRegistry } from '../actions'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; + +jest.mock('./HandlebarsWorkflowRunner'); +const MockedHandlebarsWorkflowRunner = + HandlebarsWorkflowRunner as jest.Mock; +MockedHandlebarsWorkflowRunner.mockImplementation(); + +jest.mock('./NunjucksWorkflowRunner'); +const MockedNunjucksWorkflowRunner = + NunjucksWorkflowRunner as jest.Mock; +MockedNunjucksWorkflowRunner.mockImplementation(); async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -33,18 +45,26 @@ async function createStore(): Promise { }, }), ).forPlugin('scaffolder'); - return await DatabaseTaskStore.create(await manager.getClient()); + return await DatabaseTaskStore.create({ + database: await manager.getClient(), + }); } describe('TaskWorker', () => { let storage: DatabaseTaskStore; - const workflowRunner: WorkflowRunner = { - execute: jest.fn(), - } as unknown as WorkflowRunner; - const legacyWorkflowRunner: LegacyWorkflowRunner = { + const integrations: ScmIntegrations = {} as ScmIntegrations; + + const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry; + const workingDirectory = '/tmp/scaffolder'; + + const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = { execute: jest.fn(), - } as unknown as LegacyWorkflowRunner; + } as unknown as HandlebarsWorkflowRunner; + + const workflowRunner: NunjucksWorkflowRunner = { + execute: jest.fn(), + } as unknown as NunjucksWorkflowRunner; beforeAll(async () => { storage = await createStore(); @@ -52,18 +72,22 @@ describe('TaskWorker', () => { beforeEach(() => { jest.resetAllMocks(); + MockedHandlebarsWorkflowRunner.mockImplementation( + () => handlebarsWorkflowRunner, + ); + MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner); }); const logger = getVoidLogger(); it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = await TaskWorker.create({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); await broker.dispatch({ @@ -78,17 +102,23 @@ describe('TaskWorker', () => { const task = await broker.claim(); await taskWorker.runOneTask(task); - expect(legacyWorkflowRunner.execute).toHaveBeenCalled(); + expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({ + actionRegistry, + integrations, + logger, + workingDirectory, + }); + expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled(); }); it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = await TaskWorker.create({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); await broker.dispatch({ @@ -112,12 +142,12 @@ describe('TaskWorker', () => { }); const broker = new StorageTaskBroker(storage, logger); - const taskWorker = new TaskWorker({ + const taskWorker = await TaskWorker.create({ + logger, + workingDirectory, + integrations, taskBroker: broker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, }); const { taskId } = await broker.dispatch({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 6a5a1f7843..8d89735361 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -14,20 +14,77 @@ * limitations under the License. */ -import { Task, TaskBroker, WorkflowRunner } from './types'; -import { LegacyWorkflowRunner } from './LegacyWorkflowRunner'; +import { TaskContext, TaskBroker, WorkflowRunner } from './types'; +import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; +import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; +import { Logger } from 'winston'; +import { TemplateActionRegistry } from '../actions'; +import { ScmIntegrations } from '@backstage/integration'; import { assertError } from '@backstage/errors'; -type Options = { +/** + * TaskWorkerOptions + * + * @public + */ +export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { - legacyWorkflowRunner: LegacyWorkflowRunner; + legacyWorkflowRunner: HandlebarsWorkflowRunner; workflowRunner: WorkflowRunner; }; }; +/** + * CreateWorkerOptions + * + * @public + */ +export type CreateWorkerOptions = { + taskBroker: TaskBroker; + actionRegistry: TemplateActionRegistry; + integrations: ScmIntegrations; + workingDirectory: string; + logger: Logger; +}; + +/** + * TaskWorker + * + * @public + */ export class TaskWorker { - constructor(private readonly options: Options) {} + private constructor(private readonly options: TaskWorkerOptions) {} + + static async create(options: CreateWorkerOptions): Promise { + const { + taskBroker, + logger, + actionRegistry, + integrations, + workingDirectory, + } = options; + + const legacyWorkflowRunner = new HandlebarsWorkflowRunner({ + logger, + actionRegistry, + integrations, + workingDirectory, + }); + + const workflowRunner = new NunjucksWorkflowRunner({ + actionRegistry, + integrations, + logger, + workingDirectory, + }); + + return new TaskWorker({ + taskBroker: taskBroker, + runners: { legacyWorkflowRunner, workflowRunner }, + }); + } + start() { (async () => { for (;;) { @@ -37,7 +94,7 @@ export class TaskWorker { })(); } - async runOneTask(task: Task) { + async runOneTask(task: TaskContext) { try { const { output } = task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3' diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index dd13264aed..510c862dae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -13,7 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - export { DatabaseTaskStore } from './DatabaseTaskStore'; -export { StorageTaskBroker } from './StorageTaskBroker'; +export { TaskManager } from './StorageTaskBroker'; +export type { TaskState } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; +export type { CreateWorkerOptions } from './TaskWorker'; +export type { + TaskSecrets, + TaskSpec, + CompletedTaskState, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, + SerializedTask, + SerializedTaskEvent, + TaskSpecV1beta2, + TaskSpecV1beta3, + Status, + TaskEventType, + TaskBroker, + TaskContext, + TaskStore, + DispatchResult, +} from './types'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index b91930c56a..6cfd914bd2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,6 +16,11 @@ import { JsonValue, JsonObject } from '@backstage/types'; +/** + * Status + * + * @public + */ export type Status = | 'open' | 'processing' @@ -23,9 +28,19 @@ export type Status = | 'cancelled' | 'completed'; +/** + * CompletedTaskState + * + * @public + */ export type CompletedTaskState = 'failed' | 'completed'; -export type DbTaskRow = { +/** + * SerializedTask + * + * @public + */ +export type SerializedTask = { id: string; spec: TaskSpec; status: Status; @@ -34,8 +49,19 @@ export type DbTaskRow = { secrets?: TaskSecrets; }; +/** + * TaskEventType + * + * @public + */ export type TaskEventType = 'completion' | 'log'; -export type DbTaskEventRow = { + +/** + * SerializedTaskEvent + * + * @public + */ +export type SerializedTaskEvent = { id: number; taskId: string; body: JsonObject; @@ -43,6 +69,11 @@ export type DbTaskEventRow = { createdAt: string; }; +/** + * TaskSpecV1beta2 + * + * @public + */ export interface TaskSpecV1beta2 { apiVersion: 'backstage.io/v1beta2'; baseUrl?: string; @@ -64,6 +95,12 @@ export interface TaskStep { input?: JsonObject; if?: string | boolean; } + +/** + * TaskSpecV1beta3 + * + * @public + */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; baseUrl?: string; @@ -72,17 +109,37 @@ export interface TaskSpecV1beta3 { output: { [name: string]: JsonValue }; } +/** + * TaskSpec + * + * @public + */ export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; +/** + * TaskSecrets + * + * @public + */ export type TaskSecrets = { token: string | undefined; }; +/** + * DispatchResult + * + * @public + */ export type DispatchResult = { taskId: string; }; -export interface Task { +/** + * Task + * + * @public + */ +export interface TaskContext { spec: TaskSpec; secrets?: TaskSecrets; done: boolean; @@ -91,8 +148,13 @@ export interface Task { getWorkspaceName(): Promise; } +/** + * TaskBroker + * + * @public + */ export interface TaskBroker { - claim(): Promise; + claim(): Promise; dispatch(spec: TaskSpec, secrets?: TaskSecrets): Promise; vacuumTasks(timeoutS: { timeoutS: number }): Promise; observe( @@ -102,28 +164,44 @@ export interface TaskBroker { }, callback: ( error: Error | undefined, - result: { events: DbTaskEventRow[] }, + result: { events: SerializedTaskEvent[] }, ) => void, - ): () => void; + ): { unsubscribe: () => void }; + get(taskId: string): Promise; } +/** + * TaskStoreEmitOptions + * + * @public + */ export type TaskStoreEmitOptions = { taskId: string; body: JsonObject; }; -export type TaskStoreGetEventsOptions = { +/** + * TaskStoreListEventsOptions + * + * @public + */ +export type TaskStoreListEventsOptions = { taskId: string; after?: number | undefined; }; +/** + * TaskStore + * + * @public + */ export interface TaskStore { createTask( task: TaskSpec, secrets?: TaskSecrets, ): Promise<{ taskId: string }>; - getTask(taskId: string): Promise; - claimTask(): Promise; + getTask(taskId: string): Promise; + claimTask(): Promise; completeTask(options: { taskId: string; status: Status; @@ -138,10 +216,10 @@ export interface TaskStore { listEvents({ taskId, after, - }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }>; + }: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[] }>; } export type WorkflowResponse = { output: { [key: string]: JsonValue } }; export interface WorkflowRunner { - execute(task: Task): Promise; + execute(task: TaskContext): Promise; } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index c0fdcb2ef7..d8ba15279f 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -29,18 +29,25 @@ jest.doMock('fs-extra', () => ({ })); import { + DatabaseManager, + DockerContainerRunner, getVoidLogger, PluginDatabaseManager, - DatabaseManager, UrlReaders, - DockerContainerRunner, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; +import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; -import { createRouter } from './router'; +/** + * TODO: The following should import directly from the router file. + * Due to a circular dependency between this plugin and the + * plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error: + * TypeError: _pluginscaffolderbackend.createTemplateAction is not a function + */ +import { createRouter, DatabaseTaskStore, TaskBroker } from '../index'; +import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; const createCatalogClient = (templates: any[] = []) => ({ @@ -67,6 +74,7 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; + let taskBroker: TaskBroker; const template: TemplateEntityV1beta2 = { apiVersion: 'backstage.io/v1beta2', kind: 'Template', @@ -96,7 +104,17 @@ describe('createRouter', () => { }, }; - beforeAll(async () => { + beforeEach(async () => { + const logger = getVoidLogger(); + const databaseTaskStore = await DatabaseTaskStore.create({ + database: await createDatabase().getClient(), + }); + taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + + jest.spyOn(taskBroker, 'dispatch'); + jest.spyOn(taskBroker, 'get'); + jest.spyOn(taskBroker, 'observe'); + const router = await createRouter({ logger: getVoidLogger(), config: new ConfigReader({}), @@ -104,11 +122,12 @@ describe('createRouter', () => { catalogClient: createCatalogClient([template]), containerRunner: new DockerContainerRunner({} as any), reader: mockUrlReader, + taskBroker, }); app = express().use(router); }); - beforeEach(() => { + afterEach(() => { jest.resetAllMocks(); }); @@ -136,6 +155,12 @@ describe('createRouter', () => { }); it('return the template id', async () => { + ( + taskBroker.dispatch as jest.Mocked['dispatch'] + ).mockResolvedValue({ + taskId: 'a-random-id', + }); + const response = await request(app) .post('/v2/tasks') .send({ @@ -145,28 +170,241 @@ describe('createRouter', () => { }, }); - expect(response.body.id).toBeDefined(); + expect(response.body.id).toBe('a-random-id'); expect(response.status).toEqual(201); }); }); describe('GET /v2/tasks/:taskId', () => { it('does not divulge secrets', async () => { - const postResponse = await request(app) - .post('/v2/tasks') - .set('Authorization', 'Bearer secret') - .send({ - templateName: 'create-react-app-template', - values: { - required: 'required-value', - }, - }); + (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + secrets: { token: 'secret' }, + }); - const response = await request(app) - .get(`/v2/tasks/${postResponse.body.id}`) - .send(); + const response = await request(app).get(`/v2/tasks/a-random-id`); expect(response.status).toEqual(200); + expect(response.body.status).toBe('completed'); expect(response.body.secrets).toBeUndefined(); }); }); + + describe('GET /v2/tasks/:taskId/eventstream', () => { + it('should return log messages', async () => { + const unsubscribe = jest.fn(); + ( + taskBroker.observe as jest.Mocked['observe'] + ).mockImplementation(({ taskId }, callback) => { + // emit after this function returned + setImmediate(() => { + callback(undefined, { + events: [ + { + id: 0, + taskId, + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + ], + }); + callback(undefined, { + events: [ + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); + }); + + return { unsubscribe }; + }); + + let statusCode: any = undefined; + let headers: any = {}; + const responseDataFn = jest.fn(); + + const req = request(app) + .get('/v2/tasks/a-random-id/eventstream') + .set('accept', 'text/event-stream') + .parse((res, _) => { + ({ statusCode, headers } = res as any); + + res.on('data', chunk => { + responseDataFn(chunk.toString()); + + // the server expects the client to abort the request + if (chunk.includes('completion')) { + req.abort(); + } + }); + }); + + // wait for the request to finish + await req.catch(() => { + // ignore 'aborted' error + }); + + expect(statusCode).toBe(200); + expect(headers['content-type']).toBe('text/event-stream'); + expect(responseDataFn).toBeCalledTimes(2); + expect(responseDataFn).toBeCalledWith(`event: log +data: {"id":0,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}} + +`); + expect(responseDataFn).toBeCalledWith(`event: completion +data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}} + +`); + + expect(taskBroker.observe).toBeCalledTimes(1); + expect(taskBroker.observe).toBeCalledWith( + { taskId: 'a-random-id' }, + expect.any(Function), + ); + + expect(unsubscribe).toBeCalledTimes(1); + }); + + it('should return log messages with after query', async () => { + const unsubscribe = jest.fn(); + ( + taskBroker.observe as jest.Mocked['observe'] + ).mockImplementation(({ taskId }, callback) => { + setImmediate(() => { + callback(undefined, { + events: [ + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); + }); + return { unsubscribe }; + }); + + let statusCode: any = undefined; + let headers: any = {}; + + const req = request(app) + .get('/v2/tasks/a-random-id/eventstream') + .query({ after: 10 }) + .set('accept', 'text/event-stream') + .parse((res, _) => { + ({ statusCode, headers } = res as any); + + res.on('data', () => { + // close immediately + req.abort(); + }); + }); + + // wait for the request to finish + await req.catch(() => { + // ignore 'aborted' error + }); + + expect(statusCode).toBe(200); + expect(headers['content-type']).toBe('text/event-stream'); + + expect(taskBroker.observe).toBeCalledTimes(1); + expect(taskBroker.observe).toBeCalledWith( + { taskId: 'a-random-id', after: 10 }, + expect.any(Function), + ); + + expect(unsubscribe).toBeCalledTimes(1); + }); + }); + + describe('GET /v2/tasks/:taskId/events', () => { + it('should return log messages', async () => { + const unsubscribe = jest.fn(); + ( + taskBroker.observe as jest.Mocked['observe'] + ).mockImplementation(({ taskId }, callback) => { + callback(undefined, { + events: [ + { + id: 0, + taskId, + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + { + id: 1, + taskId, + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ], + }); + return { unsubscribe }; + }); + + const response = await request(app).get('/v2/tasks/a-random-id/events'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + id: 0, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + { + id: 1, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ]); + + expect(taskBroker.observe).toBeCalledTimes(1); + expect(taskBroker.observe).toBeCalledWith( + { taskId: 'a-random-id' }, + expect.any(Function), + ); + expect(unsubscribe).toBeCalledTimes(1); + }); + + it('should return log messages with after query', async () => { + const unsubscribe = jest.fn(); + ( + taskBroker.observe as jest.Mocked['observe'] + ).mockImplementation((_, callback) => { + callback(undefined, { events: [] }); + return { unsubscribe }; + }); + + const response = await request(app) + .get('/v2/tasks/a-random-id/events') + .query({ after: 10 }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); + + expect(taskBroker.observe).toBeCalledTimes(1); + expect(taskBroker.observe).toBeCalledWith( + { taskId: 'a-random-id', after: 10 }, + expect.any(Function), + ); + expect(unsubscribe).toBeCalledTimes(1); + }); + }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index f8ba99336f..2ae30593f2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,36 +14,39 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import express from 'express'; -import Router from 'express-promise-router'; -import { Logger } from 'winston'; -import { CatalogEntityClient } from '../lib/catalog'; -import { validate } from 'jsonschema'; -import { - DatabaseTaskStore, - StorageTaskBroker, - TaskWorker, -} from '../scaffolder/tasks'; -import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry'; -import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { ContainerRunner, PluginDatabaseManager, UrlReader, } from '@backstage/backend-common'; -import { InputError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; - +import { Entity, TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { InputError, NotFoundError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; -import { TemplateAction } from '../scaffolder/actions'; -import { createBuiltinActions } from '../scaffolder/actions/builtin/createBuiltinActions'; -import { LegacyWorkflowRunner } from '../scaffolder/tasks/LegacyWorkflowRunner'; -import { DefaultWorkflowRunner } from '../scaffolder/tasks/DefaultWorkflowRunner'; -import { TaskSpec } from '../scaffolder/tasks/types'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { validate } from 'jsonschema'; +import { Logger } from 'winston'; +import { CatalogEntityClient } from '../lib/catalog'; +import { + createBuiltinActions, + DatabaseTaskStore, + TaskBroker, + TaskSpec, + TaskWorker, + TemplateAction, + TemplateActionRegistry, +} from '../scaffolder'; +import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; +import { getEntityBaseUrl, getWorkingDirectory } from './helpers'; +/** + * RouterOptions + * + * @public + */ export interface RouterOptions { logger: Logger; config: Config; @@ -53,6 +56,7 @@ export interface RouterOptions { actions?: TemplateAction[]; taskWorkers?: number; containerRunner: ContainerRunner; + taskBroker?: TaskBroker; } function isSupportedTemplate( @@ -85,34 +89,27 @@ export async function createRouter( const workingDirectory = await getWorkingDirectory(config, logger); const entityClient = new CatalogEntityClient(catalogClient); const integrations = ScmIntegrations.fromConfig(config); + let taskBroker: TaskBroker; + + if (!options.taskBroker) { + const databaseTaskStore = await DatabaseTaskStore.create({ + database: await database.getClient(), + }); + taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + } else { + taskBroker = options.taskBroker; + } - const databaseTaskStore = await DatabaseTaskStore.create( - await database.getClient(), - ); - const taskBroker = new StorageTaskBroker(databaseTaskStore, logger); const actionRegistry = new TemplateActionRegistry(); - const legacyWorkflowRunner = new LegacyWorkflowRunner({ - logger, - actionRegistry, - integrations, - workingDirectory, - }); - - const workflowRunner = new DefaultWorkflowRunner({ - actionRegistry, - integrations, - logger, - workingDirectory, - }); const workers = []; for (let i = 0; i < (taskWorkers || 1); i++) { - const worker = new TaskWorker({ + const worker = await TaskWorker.create({ taskBroker, - runners: { - legacyWorkflowRunner, - workflowRunner, - }, + actionRegistry, + integrations, + logger, + workingDirectory, }); workers.push(worker); } @@ -250,7 +247,9 @@ export async function createRouter( }) .get('/v2/tasks/:taskId/eventstream', async (req, res) => { const { taskId } = req.params; - const after = Number(req.query.after) || undefined; + const after = + req.query.after !== undefined ? Number(req.query.after) : undefined; + logger.debug(`Event stream observing taskId '${taskId}' opened`); // Mandatory headers and http status to keep connection open @@ -261,7 +260,7 @@ export async function createRouter( }); // After client opens connection send all events as string - const unsubscribe = taskBroker.observe( + const { unsubscribe } = taskBroker.observe( { taskId, after }, (error, { events }) => { if (error) { @@ -281,7 +280,8 @@ export async function createRouter( // to automatically reconnect because it lost connection. } } - res.flush(); + // res.flush() is only available with the compression middleware + res.flush?.(); if (shouldUnsubscribe) unsubscribe(); }, ); @@ -291,6 +291,43 @@ export async function createRouter( unsubscribe(); logger.debug(`Event stream observing taskId '${taskId}' closed`); }); + }) + .get('/v2/tasks/:taskId/events', async (req, res) => { + const { taskId } = req.params; + const after = Number(req.query.after) || undefined; + + let unsubscribe = () => {}; + + // cancel the request after 30 seconds. this aligns with the recommendations of RFC 6202. + const timeout = setTimeout(() => { + unsubscribe(); + res.json([]); + }, 30_000); + + // Get all known events after an id (always includes the completion event) and return the first callback + ({ unsubscribe } = taskBroker.observe( + { taskId, after }, + (error, { events }) => { + // stop the timeout + clearTimeout(timeout); + unsubscribe(); + + if (error) { + logger.error( + `Received error from log when observing taskId '${taskId}', ${error}`, + ); + } + + res.json(events); + }, + )); + + // When client closes connection we update the clients list + // avoiding the disconnected one + req.on('close', () => { + unsubscribe(); + clearTimeout(timeout); + }); }); const app = express(); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md new file mode 100644 index 0000000000..db139b2782 --- /dev/null +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/plugin-scaffolder-common + +## 0.1.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/catalog-model@0.9.6 diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 3c1c444a78..006d8ad7aa 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", + "@backstage/catalog-model": "^0.9.6", "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0" + "@backstage/cli": "^0.8.1" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 119ed36685..dd5653240a 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-scaffolder +## 0.11.10 + +### Patch Changes + +- fe5738fe1c: Lazy load `LazyLog` as it is rarely used. +- b45a34fb15: Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events. + + This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers. + + It is intended to switch the endpoint globally for the whole instance. + If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`: + + ```tsx + // packages/app/src/apis.ts + + // ... + import { + scaffolderApiRef, + ScaffolderClient, + } from '@backstage/plugin-scaffolder'; + + export const apis: AnyApiFactory[] = [ + // ... + + createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + }, + factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => + new ScaffolderClient({ + discoveryApi, + identityApi, + scmIntegrationsApi, + // use long polling instead of an eventsource + useLongPollingLogs: true, + }), + }), + ]; + ``` + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/catalog-client@0.5.1 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.11.9 + +### Patch Changes + +- 5e10974af6: Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page +- 5df2435892: Scaffolder: Enable back Template action buttons if template fails to execute +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/integration-react@0.1.13 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.11.8 ### Patch Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 554fccabb7..0ac6910264 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -178,6 +178,7 @@ export class ScaffolderClient implements ScaffolderApi { discoveryApi: DiscoveryApi; identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; + useLongPollingLogs?: boolean; }); // (undocumented) getIntegrationsList(options: { allowedHosts: string[] }): Promise< @@ -199,13 +200,7 @@ export class ScaffolderClient implements ScaffolderApi { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen scaffold(templateName: string, values: Record): Promise; // (undocumented) - streamLogs({ - taskId, - after, - }: { - taskId: string; - after?: number; - }): Observable; + streamLogs(opts: { taskId: string; after?: number }): Observable; } // Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index aa4412204a..1d8bc4a0ea 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.8", + "version": "0.11.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,16 +31,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.12", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-client": "^0.5.1", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/integration-react": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,6 +55,7 @@ "json-schema": "^0.3.0", "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", @@ -65,10 +66,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -77,7 +78,8 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "event-source-polyfill": "^1.0.25", + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index cc4b70283c..01463a6e1c 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -14,12 +14,24 @@ * limitations under the License. */ -import { ScmIntegrations } from '@backstage/integration'; -import { ScaffolderClient } from './api'; import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ScaffolderClient } from './api'; + +const MockedEventSource = global.EventSource as jest.MockedClass< + typeof EventSource +>; + +const server = setupServer(); describe('api', () => { - const discoveryApi = {} as any; + setupRequestMockHandlers(server); + const mockBaseUrl = 'http://backstage/api'; + + const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; const identityApi = {} as any; const scmIntegrationsApi = ScmIntegrations.fromConfig( new ConfigReader({ @@ -32,10 +44,14 @@ describe('api', () => { }, }), ); - const apiClient = new ScaffolderClient({ - scmIntegrationsApi, - discoveryApi, - identityApi, + + let apiClient: ScaffolderClient; + beforeEach(() => { + apiClient = new ScaffolderClient({ + scmIntegrationsApi, + discoveryApi, + identityApi, + }); }); it('should return default and custom integrations', async () => { @@ -51,4 +67,240 @@ describe('api', () => { expect(allowedHosts).toContain(integration.host), ); }); + + describe('streamEvents', () => { + describe('eventsource', () => { + it('should work', async () => { + MockedEventSource.prototype.addEventListener.mockImplementation( + (type, fn) => { + if (typeof fn !== 'function') { + return; + } + + if (type === 'log') { + fn({ + data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}', + } as any); + } else if (type === 'completion') { + fn({ + data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}', + } as any); + } + }, + ); + + const next = jest.fn(); + + await new Promise(complete => { + apiClient + .streamLogs({ taskId: 'a-random-task-id' }) + .subscribe({ next, complete }); + }); + + expect(MockedEventSource).toBeCalledWith( + 'http://backstage/api/v2/tasks/a-random-task-id/eventstream', + { withCredentials: true }, + ); + expect(MockedEventSource.prototype.close).toBeCalled(); + + expect(next).toBeCalledTimes(2); + expect(next).toBeCalledWith({ + id: 1, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }); + expect(next).toBeCalledWith({ + id: 2, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }); + }); + }); + + describe('longPolling', () => { + beforeEach(() => { + apiClient = new ScaffolderClient({ + scmIntegrationsApi, + discoveryApi, + identityApi, + useLongPollingLogs: true, + }); + }); + + it('should work', async () => { + server.use( + rest.get( + `${mockBaseUrl}/v2/tasks/:taskId/events`, + (req, res, ctx) => { + const { taskId } = req.params; + const after = req.url.searchParams.get('after'); + + if (taskId === 'a-random-task-id') { + if (!after) { + return res( + ctx.json([ + { + id: 1, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + ]), + ); + } else if (after === '1') { + return res( + ctx.json([ + { + id: 2, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ]), + ); + } + } + + return res(ctx.status(500)); + }, + ), + ); + + const next = jest.fn(); + + await new Promise(complete => + apiClient + .streamLogs({ taskId: 'a-random-task-id' }) + .subscribe({ next, complete }), + ); + + expect(next).toBeCalledTimes(2); + expect(next).toBeCalledWith({ + id: 1, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }); + expect(next).toBeCalledWith({ + id: 2, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }); + }); + + it('should unsubscribe', async () => { + expect.assertions(3); + + server.use( + rest.get( + `${mockBaseUrl}/v2/tasks/:taskId/events`, + (req, res, ctx) => { + const { taskId } = req.params; + + const after = req.url.searchParams.get('after'); + + // use assertion to make sure it is not called after unsubscribing + expect(after).toBe(null); + + if (taskId === 'a-random-task-id') { + return res( + ctx.json([ + { + id: 1, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }, + ]), + ); + } + + return res(ctx.status(500)); + }, + ), + ); + + const next = jest.fn(); + + await new Promise(complete => { + const subscription = apiClient + .streamLogs({ taskId: 'a-random-task-id' }) + .subscribe({ + next: (...args) => { + next(...args); + subscription.unsubscribe(); + complete(); + }, + }); + }); + + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith({ + id: 1, + taskId: 'a-random-id', + type: 'log', + createdAt: '', + body: { message: 'My log message' }, + }); + }); + + it('should continue after error', async () => { + const called = jest.fn(); + + server.use( + rest.get( + `${mockBaseUrl}/v2/tasks/:taskId/events`, + (_req, res, ctx) => { + called(); + + if (called.mock.calls.length > 1) { + return res( + ctx.json([ + { + id: 2, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }, + ]), + ); + } + + return res(ctx.status(500)); + }, + ), + ); + + const next = jest.fn(); + + await new Promise(complete => + apiClient + .streamLogs({ taskId: 'a-random-task-id' }) + .subscribe({ next, complete }), + ); + + expect(called).toBeCalledTimes(2); + + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith({ + id: 2, + taskId: 'a-random-id', + type: 'completion', + createdAt: '', + body: { message: 'Finished!' }, + }); + }); + }); + }); }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 1b531b652d..a16ab59053 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,17 +15,18 @@ */ import { EntityName } from '@backstage/catalog-model'; -import { JsonObject, JsonValue, Observable } from '@backstage/types'; -import { ResponseError } from '@backstage/errors'; -import { ScmIntegrationRegistry } from '@backstage/integration'; -import { Field, FieldValidation } from '@rjsf/core'; -import ObservableImpl from 'zen-observable'; -import { ListActionsResponse, ScaffolderTask, Status } from './types'; import { createApiRef, DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { JsonObject, JsonValue, Observable } from '@backstage/types'; +import { Field, FieldValidation } from '@rjsf/core'; +import qs from 'qs'; +import ObservableImpl from 'zen-observable'; +import { ListActionsResponse, ScaffolderTask, Status } from './types'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', @@ -94,15 +95,18 @@ export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; + private readonly useLongPollingLogs: boolean; constructor(options: { discoveryApi: DiscoveryApi; identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; + useLongPollingLogs?: boolean; }) { this.discoveryApi = options.discoveryApi; this.identityApi = options.identityApi; this.scmIntegrationsApi = options.scmIntegrationsApi; + this.useLongPollingLogs = options.useLongPollingLogs ?? false; } async getIntegrationsList(options: { allowedHosts: string[] }) { @@ -189,7 +193,15 @@ export class ScaffolderClient implements ScaffolderApi { return await response.json(); } - streamLogs({ + streamLogs(opts: { taskId: string; after?: number }): Observable { + if (this.useLongPollingLogs) { + return this.streamLogsPolling(opts); + } + + return this.streamLogsEventStream(opts); + } + + private streamLogsEventStream({ taskId, after, }: { @@ -239,6 +251,46 @@ export class ScaffolderClient implements ScaffolderApi { }); } + private streamLogsPolling({ + taskId, + after: inputAfter, + }: { + taskId: string; + after?: number; + }): Observable { + let after = inputAfter; + + return new ObservableImpl(subscriber => { + this.discoveryApi.getBaseUrl('scaffolder').then(async baseUrl => { + while (!subscriber.closed) { + const url = `${baseUrl}/v2/tasks/${encodeURIComponent( + taskId, + )}/events?${qs.stringify({ after })}`; + const response = await fetch(url); + + if (!response.ok) { + // wait for one second to not run into an + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + const logs = (await response.json()) as LogEvent[]; + + for (const event of logs) { + after = Number(event.id); + + subscriber.next(event); + + if (event.type === 'completion') { + subscriber.complete(); + return; + } + } + } + }); + }); + } + /** * @returns ListActionsResponse containing all registered actions. */ diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index aeb47b2789..11d12282d2 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -14,38 +14,40 @@ * limitations under the License. */ -import React, { useState, useEffect, memo, useMemo } from 'react'; -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; -import Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/StepLabel'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import { useParams } from 'react-router'; -import { useTaskEventStream } from '../hooks/useEventStream'; -import LazyLog from 'react-lazylog/build/LazyLog'; +import { + Content, + ErrorPage, + Header, + Lifecycle, + Page, + Progress, +} from '@backstage/core-components'; +import { BackstageTheme } from '@backstage/theme'; import { CircularProgress, Paper, StepButton, StepIconProps, } from '@material-ui/core'; -import { Status, TaskOutput } from '../../types'; -import { DateTime, Interval } from 'luxon'; -import { useInterval } from 'react-use'; -import Check from '@material-ui/icons/Check'; +import Grid from '@material-ui/core/Grid'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Stepper from '@material-ui/core/Stepper'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; import Cancel from '@material-ui/icons/Cancel'; +import Check from '@material-ui/icons/Check'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import classNames from 'classnames'; -import { BackstageTheme } from '@backstage/theme'; +import { DateTime, Interval } from 'luxon'; +import React, { memo, Suspense, 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'; -import { - Page, - Header, - Lifecycle, - Content, - ErrorPage, -} from '@backstage/core-components'; + +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,9 +215,17 @@ export const TaskStatusStepper = memo( const TaskLogger = memo(({ log }: { log: string }) => { return ( -
- -
+ }> +
+ +
+
); }); diff --git a/plugins/scaffolder/src/setupTests.ts b/plugins/scaffolder/src/setupTests.ts index 963c0f188b..10252413a7 100644 --- a/plugins/scaffolder/src/setupTests.ts +++ b/plugins/scaffolder/src/setupTests.ts @@ -15,3 +15,7 @@ */ import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; + +const { EventSourcePolyfill } = jest.requireMock('event-source-polyfill'); +global.EventSource = EventSourcePolyfill; diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index e950678cef..6bd39200c0 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.0.5 + +### Patch Changes + +- 36350bf8b3: Pinning version of elastic search client to 7.13.0 to prevent breaking change towards third party ElasticSearch clusters on 7.14.0. + ## 0.0.4 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 87688e455f..df0a783396 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.4", + "version": "0.0.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "dependencies": { "@backstage/config": "^0.1.8", "@backstage/search-common": "^0.2.0", - "@elastic/elasticsearch": "^7.13.0", + "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", "elastic-builder": "^2.16.0", @@ -30,8 +30,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0", + "@backstage/backend-common": "^0.9.9", + "@backstage/cli": "^0.8.2", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 95ed3ad2d4..d4f4381f83 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-node +## 0.4.3 + +### Patch Changes + +- a369f19e7e: Handle special case when filter array has single value optimizing Lunr search behaviour. +- Updated dependencies + - @backstage/search-common@0.2.1 + ## 0.4.2 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 6681d31819..4514d07756 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.4.2", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,14 +20,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/search-common": "^0.2.0", + "@backstage/search-common": "^0.2.1", "winston": "^3.2.1", "lunr": "^2.3.9", "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.9.7", - "@backstage/cli": "^0.8.0" + "@backstage/backend-common": "^0.9.8", + "@backstage/cli": "^0.8.1" }, "files": [ "dist" diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index f92a021e5c..808c847914 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-search +## 0.4.17 + +### Patch Changes + +- 5dcea2586c: Added `SearchModal` component. + + Now you can import `SearchModal` in your apps: + + ```js + import { SearchModal } from '@backstage/plugin-search'; + ``` + + You can also use the `SidebarSearchModal` component to integrate it into the sidebar of your sample apps: + + ```js + import { SidebarSearchModal } from '@backstage/plugin-search'; + ``` + +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.4.16 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + - @backstage/core-plugin-api@0.1.12 + ## 0.4.15 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index c638fb4af6..0e0ad01fca 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -126,6 +126,24 @@ export const SearchFilterNext: { Select(props: Omit & Component): JSX.Element; }; +// Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SearchModal: ({ + open, + toggleModal, +}: SearchModalProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchModalProps { + // (undocumented) + open?: boolean; + // (undocumented) + toggleModal: () => void; +} + // Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -179,6 +197,11 @@ export const SearchType: ({ // @public (undocumented) export const SidebarSearch: () => JSX.Element; +// Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SidebarSearchModal: () => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "useSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search/package.json b/plugins/search/package.json index ee8a2fb726..9d25ec723d 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.15", + "version": "0.4.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,14 +30,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/search-common": "^0.2.0", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/search-common": "^0.2.1", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -62,7 +62,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx new file mode 100644 index 0000000000..d7f873abf3 --- /dev/null +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { + ApiProvider, + ApiRegistry, + ConfigReader, +} from '@backstage/core-app-api'; +import { rootRouteRef } from '../../plugin'; +import { searchApiRef } from '../../apis'; + +import { SearchModal } from './SearchModal'; + +jest.mock('../SearchContext', () => ({ + ...jest.requireActual('../SearchContext'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchModal', () => { + const query = jest.fn().mockResolvedValue({}); + + const apiRegistry = ApiRegistry.from([ + [configApiRef, new ConfigReader({ app: { title: 'Mock app' } })], + [searchApiRef, { query }], + ]); + + const toggleModal = jest.fn(); + + it('Should render the Modal correctly', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + it('Calls toggleModal handler', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + userEvent.keyboard('{esc}'); + expect(toggleModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx new file mode 100644 index 0000000000..feabbc55af --- /dev/null +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -0,0 +1,162 @@ +/* + * 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, useState } from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + List, + Paper, +} from '@material-ui/core'; +import { Launch } from '@material-ui/icons/'; +import { makeStyles } from '@material-ui/core/styles'; +import { SearchBarBase } from '../SearchBar'; +import { DefaultResultListItem } from '../DefaultResultListItem'; +import { SearchResult } from '../SearchResult'; +import { SearchContextProvider, useSearch } from '../SearchContext'; +import { SearchResultPager } from '../SearchResultPager'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; +import { rootRouteRef } from '../../plugin'; + +import { useDebounce } from 'react-use'; + +export interface SearchModalProps { + open?: boolean; + toggleModal: () => void; +} + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + input: { + flex: 1, + }, + // Reduces default height of the modal, keeping a gap of 128px between the top and bottom of the page. + paperFullWidth: { height: 'calc(100% - 128px)' }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, + viewResultsLink: { verticalAlign: '0.5em' }, +})); + +export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { + const getSearchLink = useRouteRef(rootRouteRef); + const classes = useStyles(); + + const { term, setTerm } = useSearch(); + const [value, setValue] = useState(term); + + useEffect(() => { + setValue(prevValue => (prevValue !== term ? term : prevValue)); + }, [term]); + + useDebounce(() => setTerm(value), 1000, [value]); + + const handleQuery = (newValue: string) => { + setValue(newValue); + }; + + const handleClear = () => setValue(''); + + const handleResultClick = () => { + toggleModal(); + handleClear(); + }; + + const handleKeyPress = () => { + handleResultClick(); + }; + + return ( + + + + + + + + + + + View Full Results + + + + + + + {({ results }) => ( + + {results.map(({ document }) => ( +
+ +
+ ))} +
+ )} +
+
+ + + + + + + +
+ ); +}; + +export const SearchModal = ({ open = true, toggleModal }: SearchModalProps) => { + return ( + + + + ); +}; diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx new file mode 100644 index 0000000000..ec0053951d --- /dev/null +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -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 { SearchModal } from './SearchModal'; +export type { SearchModalProps } from './SearchModal'; diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx new file mode 100644 index 0000000000..4d4fa1a9fc --- /dev/null +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -0,0 +1,36 @@ +/* + * 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, { useState } from 'react'; +import SearchIcon from '@material-ui/icons/Search'; +import { SearchModal } from '../SearchModal'; +import { SidebarItem } from '@backstage/core-components'; + +export const SidebarSearchModal = () => { + const [open, setOpen] = useState(false); + const toggleModal = (): void => setOpen(prevState => !prevState); + + return ( + <> + + + + ); +}; diff --git a/plugins/search/src/components/SidebarSearchModal/index.ts b/plugins/search/src/components/SidebarSearchModal/index.ts new file mode 100644 index 0000000000..849180878d --- /dev/null +++ b/plugins/search/src/components/SidebarSearchModal/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { SidebarSearchModal } from './SidebarSearchModal'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 8b6493ae9b..6a4eae4001 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -19,8 +19,10 @@ export * from './Filters'; export * from './SearchBar'; export * from './SearchContext'; export * from './SearchFilter'; +export * from './SearchModal'; export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; export * from './SidebarSearch'; +export * from './SidebarSearchModal'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 07e4443f04..09cf204017 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -28,20 +28,23 @@ export { SearchContextProvider, SearchFilter, SearchFilterNext, + SearchModal, SearchPage as Router, SearchResultPager, SearchType, SidebarSearch, useSearch, } from './components'; +export type { SearchModalProps } from './components'; export type { FiltersState } from './components'; export { DefaultResultListItem, + HomePageSearchBar, SearchBarNext, SearchPage, SearchPageNext, searchPlugin as plugin, searchPlugin, SearchResult, - HomePageSearchBar, + SidebarSearchModal, } from './plugin'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 491582aead..b8c016fb36 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -122,6 +122,18 @@ export const SearchResultNext = searchPlugin.provide( }), ); +export const SidebarSearchModal = searchPlugin.provide( + createComponentExtension({ + name: 'SidebarSearchModal', + component: { + lazy: () => + import('./components/SidebarSearchModal').then( + m => m.SidebarSearchModal, + ), + }, + }), +); + export const DefaultResultListItem = searchPlugin.provide( createComponentExtension({ name: 'DefaultResultListItem', diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index cfaab7754c..b0bcaad261 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-sentry +## 0.3.28 + +### Patch Changes + +- 33d7bd0baa: fix: sentry-plugin can forward identity token to backend (for case when it requires authorization) +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.3.27 + +### Patch Changes + +- 97231f5581: feature: sentry-plugin allows passing search query for listing issues +- 61fe752dad: fix: sentry-plugin correction for allowed period values +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/core-plugin-api@0.1.12 + ## 0.3.26 ### Patch Changes diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index a23fa112c1..6d83ddc5e6 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -34,7 +35,11 @@ export class MockSentryApi implements SentryApi { // // @public (undocumented) export class ProductionSentryApi implements SentryApi { - constructor(discoveryApi: DiscoveryApi, organization: string); + constructor( + discoveryApi: DiscoveryApi, + organization: string, + identityApi?: IdentityApi | undefined, + ); // (undocumented) fetchIssues( project: string, diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index ae3aad815f..ee2421bf7f 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.26", + "version": "0.3.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/catalog-model": "^0.9.6", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -60,7 +60,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/sentry/src/api/production-api.ts b/plugins/sentry/src/api/production-api.ts index 5968d20011..1fc6ece37f 100644 --- a/plugins/sentry/src/api/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -16,12 +16,13 @@ import { SentryIssue } from './sentry-issue'; import { SentryApi } from './sentry-api'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; export class ProductionSentryApi implements SentryApi { constructor( private readonly discoveryApi: DiscoveryApi, private readonly organization: string, + private readonly identityApi?: IdentityApi, ) {} async fetchIssues( @@ -34,11 +35,13 @@ export class ProductionSentryApi implements SentryApi { } const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`; + const options = await this.authOptions(); const queryPart = query ? `&query=${query}` : ''; const response = await fetch( `${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`, + options, ); if (response.status >= 400 && response.status < 600) { @@ -47,4 +50,16 @@ export class ProductionSentryApi implements SentryApi { return (await response.json()) as SentryIssue[]; } + + private async authOptions() { + if (!this.identityApi) { + return {}; + } + const token = await this.identityApi.getIdToken(); + return { + headers: { + authorization: `Bearer ${token}`, + }, + }; + } } diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts index 75de6ba6dc..454c1f02a6 100644 --- a/plugins/sentry/src/plugin.ts +++ b/plugins/sentry/src/plugin.ts @@ -21,6 +21,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ @@ -33,11 +34,16 @@ export const sentryPlugin = createPlugin({ apis: [ createApiFactory({ api: sentryApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi }) => new ProductionSentryApi( discoveryApi, configApi.getString('sentry.organization'), + identityApi, ), }), ], diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index 8e454f53e3..41581cc5b4 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-shortcuts +## 0.1.13 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- Updated dependencies + - @backstage/theme@0.2.12 + - @backstage/core-components@0.7.2 + - @backstage/core-plugin-api@0.1.12 + ## 0.1.12 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 14dfd16896..e73e047bc1 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.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,17 +38,17 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 971eb1e8cb..bb55e68954 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -49,17 +49,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index dbc65ee59e..f8fd7c2264 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { FindingSummary, SonarQubeClient } from './index'; @@ -54,7 +54,7 @@ const identityApiGuest: IdentityApi = { }; describe('SonarQubeClient', () => { - msw.setupDefaultHandlers(server); + setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 007707a735..9d9ffdb370 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -58,7 +58,7 @@ "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0", + "msw": "^0.35.0", "node-fetch": "^2.6.1" }, "configSchema": "config.d.ts", diff --git a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md new file mode 100644 index 0000000000..48c1e69919 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -0,0 +1,85 @@ +# Tech Insights Backend JSON Rules engine fact checker module + +This is an extension to module to tech-insights-backend plugin, which provides basic framework and functionality to implement tech insights within Backstage. + +This module provides functionality to run checks against a [json-rules-engine](https://github.com/CacheControl/json-rules-engine) and provide boolean logic by simply building checks using JSON conditions. + +## Getting started + +To add this FactChecker into your Tech Insights you need to install the module into your backend application: + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend-module-jsonfc +``` + +and modify the `techInsights.ts` file to contain a reference to the FactCheckers implementation. + +```diff ++import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + ++const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++}), + + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); +``` + +By default this implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows + +```diff + const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip + const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry + }), + +``` + +## Adding 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'; + +export const exampleCheck: TechInsightJsonRuleCheck = { + id: 'demodatacheck', // Unique identifier of this check + name: 'demodatacheck', // A human readable name of this check to be displayed in the UI + type: JSON_RULE_ENGINE_CHECK_TYPE, // Type identifier of the check. Used to run logic against, determine persistence option to use and render correct components on the UI + description: 'A fact check for demoing purposes', // A description to be displayed in the UI + factRefs: ['demo-poc.factretriever'], // References to fact containers that this check uses. See documentation on FactRetrievers for more information on these + rule: { + // The actual rule + conditions: { + all: [ + // 2 options are available, all and any conditions. + { + fact: 'examplenumberfact', // Reference to an individual fact to check against + operator: 'greaterThanInclusive', // Operator to use. See: https://github.com/CacheControl/json-rules-engine/blob/master/docs/rules.md#operators for more + value: 2, // The threshold value that the fact must satisfy + }, + ], + }, + }, + successMetadata: { + // Additional metadata to be returned if the check has passed + link: 'https://link.to.some.information.com', + }, + failureMetadata: { + // Additional metadata to be returned if the check has failed + link: 'https://sonar.mysonarqube.com/increasing-number-value', + }, +}; +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md new file mode 100644 index 0000000000..7093fbfc2b --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -0,0 +1,114 @@ +## API Report File for "@backstage/plugin-tech-insights-backend-module-jsonfc" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; +import { CheckResponse } from '@backstage/plugin-tech-insights-common'; +import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node'; +import { FactChecker } from '@backstage/plugin-tech-insights-node'; +import { Logger as Logger_2 } from 'winston'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; +import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { TopLevelCondition } from 'json-rules-engine'; + +// @public (undocumented) +export type CheckCondition = { + operator: string; + fact: string; + factValue: any; + factResult: any; + result: boolean; +}; + +// @public (undocumented) +export const JSON_RULE_ENGINE_CHECK_TYPE = 'json-rules-engine'; + +// @public (undocumented) +export interface JsonRuleBooleanCheckResult extends BooleanCheckResult { + // (undocumented) + check: JsonRuleCheckResponse; +} + +// @public (undocumented) +export interface JsonRuleCheckResponse extends CheckResponse { + // (undocumented) + rule: { + conditions: ResponseTopLevelCondition & { + priority: number; + }; + }; +} + +// @public +export class JsonRulesEngineFactChecker + implements FactChecker +{ + constructor({ + checks, + repository, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerOptions); + // (undocumented) + getChecks(): Promise; + // (undocumented) + runChecks( + entity: string, + checks?: string[], + ): Promise; + // (undocumented) + validate(check: TechInsightJsonRuleCheck): Promise; +} + +// @public +export class JsonRulesEngineFactCheckerFactory { + constructor({ + checks, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerFactoryOptions); + // (undocumented) + construct(repository: TechInsightsStore): JsonRulesEngineFactChecker; +} + +// @public +export type JsonRulesEngineFactCheckerFactoryOptions = { + checks: TechInsightJsonRuleCheck[]; + logger: Logger_2; + checkRegistry?: TechInsightCheckRegistry; +}; + +// @public +export type JsonRulesEngineFactCheckerOptions = { + checks: TechInsightJsonRuleCheck[]; + repository: TechInsightsStore; + logger: Logger_2; + checkRegistry?: TechInsightCheckRegistry; +}; + +// @public (undocumented) +export type ResponseTopLevelCondition = + | { + all: CheckCondition[]; + } + | { + any: CheckCondition[]; + }; + +// @public (undocumented) +export type Rule = { + conditions: TopLevelCondition; + name?: string; + priority?: number; +}; + +// @public (undocumented) +export interface TechInsightJsonRuleCheck extends TechInsightCheck { + // (undocumented) + rule: Rule; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json new file mode 100644 index 0000000000..8943b0e461 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-insights-backend-module-jsonfc" + }, + "keywords": [ + "backstage", + "tech-insights", + "scorecard" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.0", + "@backstage/config": "^0.1.8", + "@backstage/errors": "^0.1.1", + "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", + "ajv": "^7.0.3", + "json-rules-engine": "^6.1.2", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0", + "@types/node-cron": "^2.0.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/constants.ts b/plugins/tech-insights-backend-module-jsonfc/src/constants.ts new file mode 100644 index 0000000000..5fd69a3fef --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/constants.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. + */ + +/** + * @public + */ +export const JSON_RULE_ENGINE_CHECK_TYPE = 'json-rules-engine'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/index.ts b/plugins/tech-insights-backend-module-jsonfc/src/index.ts new file mode 100644 index 0000000000..1239211f73 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/index.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. + */ +export { + JsonRulesEngineFactCheckerFactory, + JsonRulesEngineFactChecker, +} from './service/JsonRulesEngineFactChecker'; +export { JSON_RULE_ENGINE_CHECK_TYPE } from './constants'; +export type { + JsonRulesEngineFactCheckerFactoryOptions, + JsonRulesEngineFactCheckerOptions, +} from './service/JsonRulesEngineFactChecker'; +export type { + JsonRuleCheckResponse, + JsonRuleBooleanCheckResult, + TechInsightJsonRuleCheck, + ResponseTopLevelCondition, + Rule, + CheckCondition, +} from './types'; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.ts new file mode 100644 index 0000000000..dec84a8634 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/CheckRegistry.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 { ConflictError, NotFoundError } from '@backstage/errors'; +import { + TechInsightCheck, + TechInsightCheckRegistry, +} from '@backstage/plugin-tech-insights-node'; + +export class DefaultCheckRegistry + implements TechInsightCheckRegistry +{ + private readonly checks = new Map(); + + constructor(checks: CheckType[]) { + checks.forEach(check => { + this.register(check); + }); + } + + async register(check: CheckType): Promise { + if (this.checks.has(check.id)) { + throw new ConflictError( + `Tech insight check with id ${check.id} has already been registered`, + ); + } + this.checks.set(check.id, check); + return check; + } + + async get(checkId: string): Promise { + const check = this.checks.get(checkId); + if (!check) { + throw new NotFoundError( + `Tech insight check with id '${checkId}' is not registered.`, + ); + } + return check; + } + async getAll(checks: string[]): Promise { + return Promise.all(checks.map(checkId => this.get(checkId))); + } + + async list(): Promise { + return [...this.checks.values()]; + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts new file mode 100644 index 0000000000..77b3e6c8b1 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.test.ts @@ -0,0 +1,301 @@ +/* + * 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 { + TechInsightCheckRegistry, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-node'; +import { + JSON_RULE_ENGINE_CHECK_TYPE, + JsonRulesEngineFactCheckerFactory, +} from '../index'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TechInsightJsonRuleCheck } from '../types'; + +const testChecks: Record = { + broken: [ + { + id: 'brokenTestCheck', + name: 'brokenTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + description: 'Broken Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'largerThan', + value: 1, + }, + ], + }, + }, + }, + ], + broken2: [ + { + id: 'brokenTestCheck2', + name: 'brokenTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, + description: 'Second Broken Check For Testing', + factIds: ['non-existing-factretriever'], + rule: { + conditions: { + any: [ + { + fact: 'somefact', + operator: 'lessThan', + value: 1, + }, + ], + }, + }, + }, + ], + simple: [ + { + id: 'simpleTestCheck', + name: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + description: 'Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'lessThan', + value: 5, + }, + ], + }, + }, + }, + ], + + simple2: [ + { + id: 'simpleTestCheck2', + name: 'simpleTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, + description: 'Second Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + operator: 'greaterThan', + value: 2, + }, + ], + }, + }, + }, + ], +}; + +const latestSchemasMock = jest.fn().mockImplementation(() => [ + { + version: '0.0.1', + id: 2, + ref: 'test-factretriever', + entityTypes: ['component'], + testnumberfact: { + type: 'integer', + description: '', + }, + }, +]); +const factsBetweenTimestampsByIdsMock = jest.fn(); +const latestFactsByIdsMock = jest.fn().mockImplementation(() => ({})); +const mockCheckRegistry = { + getAll(checks: string[]) { + return checks.flatMap(check => testChecks[check]); + }, +} as unknown as TechInsightCheckRegistry; + +const mockRepository: TechInsightsStore = { + getLatestFactsByIds: latestFactsByIdsMock, + getFactsBetweenTimestampsByIds: factsBetweenTimestampsByIdsMock, + getLatestSchemas: latestSchemasMock, +} as unknown as TechInsightsStore; + +describe('JsonRulesEngineFactChecker', () => { + const factChecker = new JsonRulesEngineFactCheckerFactory({ + checkRegistry: mockCheckRegistry, + checks: [], + logger: getVoidLogger(), + }).construct(mockRepository); + + describe('when running checks', () => { + it('should throw on incorrectly configured checks conditions', async () => { + const cur = async () => await factChecker.runChecks('a/a/a', ['broken']); + await expect(cur()).rejects.toThrowError( + 'Failed to run rules engine, Unknown operator: largerThan', + ); + }); + + it('should handle cases where wrong facts are referenced', async () => { + const cur = async () => await factChecker.runChecks('a/a/a', ['broken2']); + await expect(cur()).rejects.toThrowError( + 'Failed to run rules engine, Undefined fact: somefact', + ); + }); + it('should respond with result, facts, fact schemas and checks', async () => { + latestFactsByIdsMock.mockImplementation(() => + Promise.resolve({ + ['test-factretriever']: { + id: 'test-factretriever', + facts: { + testnumberfact: 3, + }, + }, + }), + ); + const results = await factChecker.runChecks('a/a/a', ['simple']); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + }, + }, + result: true, + check: { + id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + all: [ + { + fact: 'testnumberfact', + factResult: 3, + operator: 'lessThan', + result: true, + value: 5, + }, + ], + priority: 1, + }, + }, + }, + }); + }); + + it('should gracefully handle multiple check at once', async () => { + latestFactsByIdsMock.mockImplementation(() => + Promise.resolve({ + ['test-factretriever']: { + id: 'test-factretriever', + facts: { + testnumberfact: 3, + }, + }, + }), + ); + const results = await factChecker.runChecks('a/a/a', [ + 'simple', + 'simple2', + ]); + expect(results).toMatchObject([ + { + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + }, + }, + result: true, + check: { + id: 'simpleTestCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck', + description: 'Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + priority: 1, + all: [ + { + operator: 'lessThan', + value: 5, + fact: 'testnumberfact', + factResult: 3, + result: true, + }, + ], + }, + }, + }, + }, + { + facts: { + testnumberfact: { + value: 3, + type: 'integer', + description: '', + }, + }, + result: true, + check: { + id: 'simpleTestCheck2', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'simpleTestCheck2', + description: 'Second Simple Check For Testing', + factIds: ['test-factretriever'], + rule: { + conditions: { + priority: 1, + all: [ + { + operator: 'greaterThan', + value: 2, + fact: 'testnumberfact', + factResult: 3, + result: true, + }, + ], + }, + }, + }, + }, + ]); + }); + }); + + describe('when validating checks', () => { + it('should succeed on valid rules', async () => { + const validationResponse = await factChecker.validate( + testChecks.simple[0], + ); + expect(validationResponse.valid).toBeTruthy(); + }); + it('should fail on broken rules', async () => { + const validationResponse = await factChecker.validate( + testChecks.broken[0], + ); + expect(validationResponse.valid).toBeFalsy(); + }); + }); +}); diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts new file mode 100644 index 0000000000..ae1f18a373 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/JsonRulesEngineFactChecker.ts @@ -0,0 +1,343 @@ +/* + * 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 { JsonRuleBooleanCheckResult, TechInsightJsonRuleCheck } from '../types'; +import { + FactChecker, + TechInsightCheckRegistry, + FlatTechInsightFact, + TechInsightsStore, + CheckValidationResponse, +} from '@backstage/plugin-tech-insights-node'; +import { FactResponse } from '@backstage/plugin-tech-insights-common'; +import { Engine, EngineResult, TopLevelCondition } from 'json-rules-engine'; +import { DefaultCheckRegistry } from './CheckRegistry'; +import { Logger } from 'winston'; +import { pick } from 'lodash'; +import Ajv from 'ajv'; +import * as validationSchema from './validation-schema.json'; +import { JSON_RULE_ENGINE_CHECK_TYPE } from '../constants'; + +const noopEvent = { + type: 'noop', +}; + +/** + * @public + * Should actually be at-internal + * + * Constructor options for JsonRulesEngineFactChecker + */ +export type JsonRulesEngineFactCheckerOptions = { + checks: TechInsightJsonRuleCheck[]; + repository: TechInsightsStore; + logger: Logger; + checkRegistry?: TechInsightCheckRegistry; +}; + +/** + * @public + * Should actually be at-internal + * + * FactChecker implementation using json-rules-engine + */ +export class JsonRulesEngineFactChecker + implements FactChecker +{ + private readonly checkRegistry: TechInsightCheckRegistry; + private repository: TechInsightsStore; + private readonly logger: Logger; + + constructor({ + checks, + repository, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerOptions) { + this.repository = repository; + this.logger = logger; + checks.forEach(check => this.validate(check)); + this.checkRegistry = + checkRegistry ?? + new DefaultCheckRegistry(checks); + } + + async runChecks( + entity: string, + checks?: string[], + ): Promise { + const engine = new Engine(); + const techInsightChecks = checks + ? await this.checkRegistry.getAll(checks) + : await this.checkRegistry.list(); + const factIds = techInsightChecks.flatMap(it => it.factIds); + const facts = await this.repository.getLatestFactsByIds(factIds, entity); + techInsightChecks.forEach(techInsightCheck => { + const rule = techInsightCheck.rule; + rule.name = techInsightCheck.id; + engine.addRule({ ...techInsightCheck.rule, event: noopEvent }); + }); + const factValues = Object.values(facts).reduce( + (acc, it) => ({ ...acc, ...it.facts }), + {}, + ); + + try { + const results = await engine.run(factValues); + return await this.ruleEngineResultsToCheckResponse( + results, + techInsightChecks, + Object.values(facts), + ); + } catch (e) { + throw new Error(`Failed to run rules engine, ${e.message}`); + } + } + + async validate( + check: TechInsightJsonRuleCheck, + ): Promise { + const ajv = new Ajv({ verbose: true }); + const validator = ajv.compile(validationSchema); + const isValidToSchema = validator(check.rule); + if (check.type !== JSON_RULE_ENGINE_CHECK_TYPE) { + const msg = `Only ${JSON_RULE_ENGINE_CHECK_TYPE} checks can be registered to this fact checker`; + this.logger.warn(msg); + return { + valid: false, + message: msg, + }; + } + if (!isValidToSchema) { + const msg = 'Failed to to validate conditions against JSON schema'; + this.logger.warn( + 'Failed to to validate conditions against JSON schema', + validator.errors, + ); + return { + valid: false, + message: msg, + errors: validator.errors ? validator.errors : undefined, + }; + } + + const existingSchemas = await this.repository.getLatestSchemas( + check.factIds, + ); + const references = this.retrieveIndividualFactReferences( + check.rule.conditions, + ); + const results = references.map(ref => ({ + ref, + result: existingSchemas.some(schema => schema.hasOwnProperty(ref)), + })); + const failedReferences = results.filter(it => !it.result); + failedReferences.forEach(it => { + this.logger.warn( + `Validation failed for check ${check.name}. Reference to value ${ + it.ref + } does not exists in referred fact schemas: ${check.factIds.join(',')}`, + ); + }); + const valid = failedReferences.length === 0; + return { + valid, + ...(!valid + ? { + message: `Check is referencing missing values from fact schemas: ${failedReferences + .map(it => it.ref) + .join(',')}`, + } + : {}), + }; + } + + getChecks(): Promise { + return this.checkRegistry.list(); + } + + private retrieveIndividualFactReferences( + condition: TopLevelCondition | { fact: string }, + ): string[] { + let results: string[] = []; + if ('all' in condition) { + results = results.concat( + condition.all.flatMap(con => + this.retrieveIndividualFactReferences(con), + ), + ); + } else if ('any' in condition) { + results = results.concat( + condition.any.flatMap(con => + this.retrieveIndividualFactReferences(con), + ), + ); + } else { + results.push(condition.fact); + } + return results; + } + + private async ruleEngineResultsToCheckResponse( + results: EngineResult, + techInsightChecks: TechInsightJsonRuleCheck[], + facts: FlatTechInsightFact[], + ) { + return await Promise.all( + [ + ...(results.results && results.results), + ...(results.failureResults && results.failureResults), + ].map(async result => { + const techInsightCheck = techInsightChecks.find( + check => check.id === result.name, + ); + if (!techInsightCheck) { + // This should never happen, we just constructed these based on each other + throw new Error( + `Failed to determine tech insight check with id ${result.name}. Discrepancy between ran rule engine and configured checks.`, + ); + } + const factResponse = await this.constructFactInformationResponse( + facts, + techInsightCheck, + ); + return { + facts: factResponse, + result: result.result, + check: JsonRulesEngineFactChecker.constructCheckResponse( + techInsightCheck, + result, + ), + }; + }), + ); + } + + private static constructCheckResponse( + techInsightCheck: TechInsightJsonRuleCheck, + result: any, + ) { + const returnable = { + id: techInsightCheck.id, + type: techInsightCheck.type, + name: techInsightCheck.name, + description: techInsightCheck.description, + factIds: techInsightCheck.factIds, + metadata: result.result + ? techInsightCheck.successMetadata + : techInsightCheck.failureMetadata, + rule: { conditions: {} }, + }; + + if ('toJSON' in result) { + // Results from json-rules-engine serialize "wrong" since the objects are creating their own serialization implementations. + // 'toJSON' should always be present in the result object but it is missing from the types. + // Parsing the stringified representation into a plain object here to be able to serialize it later + // along with other items present in the returned response. + const rule = JSON.parse(result.toJSON()); + return { ...returnable, rule: pick(rule, ['conditions']) }; + } + return returnable; + } + + private async constructFactInformationResponse( + facts: FlatTechInsightFact[], + techInsightCheck: TechInsightJsonRuleCheck, + ): Promise { + const factSchemas = await this.repository.getLatestSchemas( + techInsightCheck.factIds, + ); + const schemas = factSchemas.reduce( + (acc, schema) => ({ ...acc, ...schema }), + {}, + ); + const individualFacts = this.retrieveIndividualFactReferences( + techInsightCheck.rule.conditions, + ); + const factValues = facts + .filter(factContainer => + techInsightCheck.factIds.includes(factContainer.id), + ) + .reduce( + (acc, factContainer) => ({ + ...acc, + ...pick(factContainer.facts, individualFacts), + }), + {}, + ); + return Object.entries(factValues).reduce((acc, [key, value]) => { + return { + ...acc, + [key]: { + value, + ...schemas[key], + }, + }; + }, {}); + } +} + +/** + * @public + * + * Constructor options for JsonRulesEngineFactCheckerFactory + * + * Implementation of checkRegistry is optional. + * If there is a need to use persistent storage for checks, it is recommended to inject a storage implementation here. + * Otherwise an in-memory option is instantiated and used. + */ +export type JsonRulesEngineFactCheckerFactoryOptions = { + checks: TechInsightJsonRuleCheck[]; + logger: Logger; + checkRegistry?: TechInsightCheckRegistry; +}; + +/** + * @public + * + * Factory to construct JsonRulesEngineFactChecker + * Can be constructed with optional implementation of CheckInsightCheckRegistry if needed. + * Otherwise defaults to using in-memory CheckRegistry + */ +export class JsonRulesEngineFactCheckerFactory { + private readonly checks: TechInsightJsonRuleCheck[]; + private readonly logger: Logger; + private readonly checkRegistry?: TechInsightCheckRegistry; + + constructor({ + checks, + logger, + checkRegistry, + }: JsonRulesEngineFactCheckerFactoryOptions) { + this.logger = logger; + this.checks = checks; + this.checkRegistry = checkRegistry; + } + + /** + * @param repository - Implementation of TechInsightsStore. Used by the returned JsonRulesEngineFactChecker + * to retrieve fact and fact schema data + * @returns JsonRulesEngineFactChecker implementation + */ + construct(repository: TechInsightsStore) { + return new JsonRulesEngineFactChecker({ + checks: this.checks, + logger: this.logger, + checkRegistry: this.checkRegistry, + repository, + }); + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json b/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json new file mode 100644 index 0000000000..9020ba0735 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/service/validation-schema.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/CacheControl/json-rules-engine/rule-schema.json", + "type": "object", + "title": "Fact Checks", + "description": "Checks contain a set of conditions and a single event. When the engine is run, each check condition is evaluated and results returned", + "required": ["conditions"], + "properties": { + "conditions": { + "$ref": "#/definitions/conditions" + }, + "priority": { + "$id": "#/properties/priority", + "anyOf": [ + { + "type": "integer", + "minimum": 1 + } + ], + "title": "Priority", + "description": "Dictates when check should be run, relative to other check. Higher priority checks are run before lower priority checks. Checks with the same priority are run in parallel. Priority must be a positive, non-zero integer.", + "default": 1, + "examples": [1] + } + }, + "definitions": { + "conditions": { + "type": "object", + "title": "Conditions", + "description": "Check conditions are a combination of facts, operators, and values that determine whether the check is a success or a failure. The simplest form of a condition consists of a fact, an operator, and a value. When the engine runs, the operator is used to compare the fact against the value. Each check's conditions must have either an all or an any operator at its root, containing an array of conditions. The all operator specifies that all conditions contained within must be truthy for the check to be considered a success. The any operator only requires one condition to be truthy for the check to succeed.", + "default": {}, + "examples": [ + { + "all": [ + { + "value": true, + "fact": "displayMessage", + "operator": "equal" + } + ] + } + ], + "oneOf": [ + { + "required": ["any"] + }, + { + "required": ["all"] + } + ], + "properties": { + "any": { + "$ref": "#/definitions/conditionArray" + }, + "all": { + "$ref": "#/definitions/conditionArray" + } + } + }, + "conditionArray": { + "type": "array", + "title": "Condition Array", + "description": "An array of conditions with a possible recursive inclusion of another condition array.", + "default": [], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/conditions" + }, + { + "$ref": "#/definitions/condition" + } + ] + } + }, + "condition": { + "type": "object", + "title": "Condition", + "description": "Check conditions are a combination of facts, operators, and values that determine whether the check is a success or a failure. The simplest form of a condition consists of a fact, an operator, and a value. When the engine runs, the operator is used to compare the fact against the value.", + "default": { + "fact": "my-fact", + "operator": "lessThanInclusive", + "value": 1 + }, + "examples": [ + { + "fact": "gameDuration", + "operator": "equal", + "value": 40.0 + }, + { + "value": 5.0, + "fact": "personalFoulCount", + "operator": "greaterThanInclusive" + }, + { + "fact": "product-price", + "operator": "greaterThan", + "path": "$.price", + "value": 100.0, + "params": { + "productId": "widget" + } + } + ], + "required": ["fact", "operator", "value"], + "properties": { + "fact": { + "type": "string", + "title": "Fact", + "description": "Facts are methods or constants registered with the engine prior to runtime and referenced within check conditions. Each fact method should be a pure function that may return a either computed value, or promise that resolves to a computed value. As check conditions are evaluated during runtime, they retrieve fact values dynamically and use the condition operator to compare the fact result with the condition value.", + "default": "", + "examples": ["gameDuration"] + }, + "operator": { + "type": "string", + "anyOf": [ + { + "const": "equal", + "title": "fact must equal value" + }, + { + "const": "notEqual", + "title": "fact must not equal value" + }, + { + "const": "lessThan", + "title": "fact must be less than value" + }, + { + "const": "lessThanInclusive", + "title": "fact must be less than or equal to value" + }, + { + "const": "greaterThan", + "title": "fact must be greater than value" + }, + { + "const": "greaterThanInclusive", + "title": "fact must be greater than or equal to value" + }, + { + "const": "in", + "title": "fact must be included in value (an array)" + }, + { + "const": "notIn", + "title": "fact must not be included in value (an array)" + }, + { + "const": "contains", + "title": "fact (an array) must include value" + }, + { + "const": "doesNotContain", + "title": "fact (an array) must not include value" + } + ], + "title": "Operator", + "description": "The operator compares the value returned by the fact to what is stored in the value property. If the result is truthy, the condition passes.", + "default": "", + "examples": ["equal"] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "title": "Value", + "description": "The value the fact should be compared to.", + "default": 0, + "examples": [40] + }, + "params": { + "type": "object", + "title": "Event Params", + "description": "Optional helper params to make available to the event processor.", + "default": {}, + "examples": [ + { + "customProperty": "customValue" + } + ] + }, + "path": { + "type": "string", + "title": "Path", + "description": "For more complex data structures, writing a separate fact handler for each object property quickly becomes verbose and unwieldy. To address this, a path property may be provided to traverse fact data using json-path syntax. Json-path support is provided by jsonpath-plus", + "default": "", + "examples": ["$.price"] + } + } + } + } +} diff --git a/plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts b/plugins/tech-insights-backend-module-jsonfc/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/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. + */ + +export {}; diff --git a/plugins/tech-insights-backend-module-jsonfc/src/types.ts b/plugins/tech-insights-backend-module-jsonfc/src/types.ts new file mode 100644 index 0000000000..592fee248d --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/src/types.ts @@ -0,0 +1,73 @@ +/* + * 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 { TopLevelCondition } from 'json-rules-engine'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; +import { + BooleanCheckResult, + CheckResponse, +} from '@backstage/plugin-tech-insights-common'; + +/** + * @public + */ +export type Rule = { + conditions: TopLevelCondition; + name?: string; + priority?: number; +}; + +/** + * @public + */ +export interface TechInsightJsonRuleCheck extends TechInsightCheck { + rule: Rule; +} + +/** + * @public + */ +export type CheckCondition = { + operator: string; + fact: string; + factValue: any; + factResult: any; + result: boolean; +}; + +/** + * @public + */ +export type ResponseTopLevelCondition = + | { all: CheckCondition[] } + | { any: CheckCondition[] }; + +/** + * @public + */ +export interface JsonRuleCheckResponse extends CheckResponse { + rule: { + conditions: ResponseTopLevelCondition & { + priority: number; + }; + }; +} + +/** + * @public + */ +export interface JsonRuleBooleanCheckResult extends BooleanCheckResult { + check: JsonRuleCheckResponse; +} diff --git a/plugins/tech-insights-backend/.eslintrc.js b/plugins/tech-insights-backend/.eslintrc.js new file mode 100644 index 0000000000..a126c438a4 --- /dev/null +++ b/plugins/tech-insights-backend/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'jest/expect-expect': [ + 'error', + { + assertFunctionNames: ['expect', 'request.**.expect'], + }, + ], + }, +}; diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md new file mode 100644 index 0000000000..e2cc1c0252 --- /dev/null +++ b/plugins/tech-insights-backend/README.md @@ -0,0 +1,230 @@ +# Tech Insights Backend + +This is the backend for the default Backstage Tech Insights feature. +This provides the API for the frontend tech insights, scorecards and fact visualization functionality, +as well as a framework to run fact retrievers and store fact values in to a data store. + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/techInsights.ts`. An example content for `techInsights.ts` could be something like this. + +```ts +import { + createRouter, + DefaultTechInsightsBuilder, +} from '@backstage/plugin-tech-insights-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, + discovery, + database, +}: PluginEnvironment): Promise { + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [], // Fact retrievers registrations you want tech insights to use + }); + + return await createRouter({ + ...(await builder.build()), + logger, + config, + }); +} +``` + +With the `techInsights.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: + +```diff ++import techInsights from './plugins/techInsights'; + + async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const techInsightsEnv = useHotMemoize(module, () => createEnv('tech_insights')); + + const apiRouter = Router(); ++ apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); + ... + apiRouter.use(notFoundHandler()); + } +``` + +### Adding fact retrievers + +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: + +```ts +import { createFactRetrieverRegistration } from './createFactRetriever'; + +const myFactRetriever = { + /** + * snip + */ +}; + +const myFactRetrieverRegistration = createFactRetrieverRegistration( + '1 * 3 * * ', // On the first minute of the third day of the month + myFactRetriever, +); +``` + +Then you can modify the example `techInsights.ts` file shown above like this: + +```diff +const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, +- factRetrievers: [], ++ factRetrievers: [myFactRetrieverRegistration], +}); +``` + +#### Running fact retrievers in a multi-instance installation + +Current logic on running scheduled fact retrievers is intended to be executed in a single instance. Running on multi-instane environment there might be some additional data accumulation when multiple fact retrievers would retrieve and persist their facts. To mitigate this it is recommended to mark a single instance to be a specific fact retriever instance. One way to do this is by using environment variables to indicate if the retrievers should be registered. This can be done for example like the code snippet below + +```diff +const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, +- factRetrievers: [], ++ factRetrievers: process.env.MAIN_FACT_RETRIEVER_INSTANCE ? [myFactRetrieverRegistration] : [], +}); +``` + +Where the instance dedicated to handling retrieval of facts would have environment variable `MAIN_FACT_RETRIEVER_INSTANCE` set to true. + +### Creating Fact Retrievers + +A Fact Retriever consist of four required and one optional parts: + +1. `id` - unique identifier of a fact retriever +2. `version`: A semver string indicating the current version of the schema and the handler +3. `schema` - A versioned schema defining the shape of data a fact retriever returns +4. `handler` - An asynchronous function handling the logic of retrieving and returning facts for an entity +5. `entityFilter` - (Optional) EntityFilter object defining the entity kinds, types and/or names this fact retriever handles + +An example implementation of a FactRetriever could for example be as follows: + +```ts +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 + entityFilter: [{ kind: 'component' }], // EntityFilter to be used in the future (creating checks, graphs etc.) to figure out which entities this fact retrieves data for. + schema: { + // Name/identifier of an individual fact that this retriever returns + examplenumberfact: { + type: 'integer', // Type of the fact + description: 'A fact of a number', // Description of the fact + }, + }, + handler: async ctx => { + // Handler function that retrieves the fact + const { discovery, config, logger } = ctx; + const catalogClient = new CatalogClient({ + discoveryApi: discovery, + }); + const entities = await catalogClient.getEntities({ + filter: [{ kind: 'component' }], + }); + /** + * snip: Do complex logic to retrieve facts from external system or calculate fact values + */ + + // Respond with an array of entity/fact values + return entities.items.map(it => { + return { + // Entity information that this fact relates to + entity: { + namespace: it.metadata.namespace, + kind: it.kind, + name: it.metadata.name, + }, + + // All facts that this retriever returns + facts: { + examplenumberfact: 2, // + }, + // (optional) timestamp to use as a Luxon DateTime object + }; + }); + }, +}; +``` + +### Adding a fact checker + +This module comes with a possibility to additionally add a fact checker and expose fact checking endpoints from the API. To be able to enable this feature you need to add a FactCheckerFactory implementation to be part of the `DefaultTechInsightsBuilder` constructor call. + +There is a default FactChecker implementation provided in module `@backstage/plugin-tech-insights-backend-module-jsonfc`. This implementation uses `json-rules-engine` as the underlying functionality to run checks. If you want to implement your own FactChecker, for example to be able to handle other than `boolean` result types, you can do so by implementing `FactCheckerFactory` and `FactChecker` interfaces from `@backstage/plugin-tech-insights-common` package. + +To add the default FactChecker into your Tech Insights you need to install the module into your backend application: + +```bash +# From your Backstage root directory +cd packages/backend +yarn add @backstage/plugin-tech-insights-backend-module-jsonfc +``` + +and modify the `techInsights.ts` file to contain a reference to the FactChecker implementation. + +```diff ++import { JsonRulesEngineFactCheckerFactory } from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + ++const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ ++ checks: [], ++ logger, ++}), + + const builder = new DefaultTechInsightsBuilder({ + logger, + config, + database, + discovery, + factRetrievers: [myFactRetrieverRegistration], ++ factCheckerFactory: myFactCheckerFactory + }); +``` + +To be able to run checks, you need to additionally add individual checks into your FactChecker implementation. For examples how to add these, you can check the documentation of the individual implementation of the FactChecker + +#### Modifying check persistence + +The default FactChecker implementation comes with an in-memory storage to store checks. You can inject an additional data store by adding an implementation of `TechInsightCheckRegistry` into the constructor options when creating a `JsonRulesEngineFactCheckerFactory`. That can be done as follows: + +```diff +const myTechInsightCheckRegistry: TechInsightCheckRegistry = // snip +const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ + checks: [], + logger, ++ checkRegistry: myTechInsightCheckRegistry +}), + +``` diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md new file mode 100644 index 0000000000..826789dd1d --- /dev/null +++ b/plugins/tech-insights-backend/api-report.md @@ -0,0 +1,82 @@ +## API Report File for "@backstage/plugin-tech-insights-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Config } from '@backstage/config'; +import express from 'express'; +import { FactChecker } from '@backstage/plugin-tech-insights-node'; +import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; +import { FactRetriever } from '@backstage/plugin-tech-insights-node'; +import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; +import { Logger as Logger_2 } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; + +// @public +export const buildTechInsightsContext: < + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>( + options: TechInsightsOptions, +) => Promise>; + +// @public +export function createFactRetrieverRegistration( + cadence: string, + factRetriever: FactRetriever, +): FactRetrieverRegistration; + +// @public +export function createRouter< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>(options: RouterOptions): Promise; + +// @public +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + +// @public +export interface RouterOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + config: Config; + factChecker?: FactChecker; + logger: Logger_2; + persistenceContext: PersistenceContext; +} + +// @public (undocumented) +export type TechInsightsContext< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> = { + factChecker?: FactChecker; + persistenceContext: PersistenceContext; +}; + +// @public (undocumented) +export interface TechInsightsOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + // (undocumented) + config: Config; + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + factCheckerFactory?: FactCheckerFactory; + factRetrievers: FactRetrieverRegistration[]; + // (undocumented) + logger: Logger_2; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js new file mode 100644 index 0000000000..6094daa4b7 --- /dev/null +++ b/plugins/tech-insights-backend/migrations/202109061111_fact_schemas.js @@ -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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('fact_schemas', table => { + table.comment( + 'The table for tech insight fact schemas. Containing a versioned data model definition for a collection of facts.', + ); + table + .text('id') + .notNullable() + .comment('Identifier of the fact retriever plugin/package'); + table + .string('version') + .notNullable() + .comment('SemVer string defining the version of schema.'); + table + .string('entityFilter') + .nullable() + .comment( + 'A serialized entity filter object used to determine which entities this schema is applicable to.', + ); + table + .text('schema') + .notNullable() + .comment( + 'Fact schema defining the values/types what this version of the fact would contain.', + ); + table.primary(['id', 'version']); + table.index('id', 'fact_schema_id_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('fact_schemas', table => { + table.dropIndex([], 'fact_schema_id_idx'); + }); + await knex.schema.dropTable('fact_schemas'); +}; diff --git a/plugins/tech-insights-backend/migrations/202109061212_facts.js b/plugins/tech-insights-backend/migrations/202109061212_facts.js new file mode 100644 index 0000000000..0bdcdc0da1 --- /dev/null +++ b/plugins/tech-insights-backend/migrations/202109061212_facts.js @@ -0,0 +1,72 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('facts', table => { + table.comment( + 'The table for tech insight fact collections. Contains facts for individual fact retriever namespace/ref.', + ); + table + .text('id') + .notNullable() + .comment('Unique identifier of the fact retriever plugin/package'); + table + .string('version') + .notNullable() + .comment( + 'SemVer string defining the version of schema this fact is based on.', + ); + table + .dateTime('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this entry was created'); + table + .text('entity') + .notNullable() + .comment('Identifier of the entity these facts relate to'); + table + .text('facts') + .notNullable() + .comment( + 'Values of the fact collection stored as key-value pairs in JSON format.', + ); + + table + .foreign(['id', 'version']) + .references(['id', 'version']) + .inTable('fact_schemas'); + + table.index(['id', 'entity'], 'fact_id_entity_idx'); + table.index('id', 'fact_id_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('facts', table => { + table.dropIndex([], 'fact_id_idx'); + table.dropIndex([], 'fact_id_entity_idx'); + }); + await knex.schema.dropTable('facts'); +}; diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json new file mode 100644 index 0000000000..678bec25c4 --- /dev/null +++ b/plugins/tech-insights-backend/package.json @@ -0,0 +1,66 @@ +{ + "name": "@backstage/plugin-tech-insights-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-insights-backend" + }, + "keywords": [ + "backstage", + "tech-insights", + "reporting" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.0", + "@backstage/catalog-client": "^0.3.16", + "@backstage/catalog-model": "^0.9.0", + "@backstage/config": "^0.1.8", + "@backstage/errors": "^0.1.1", + "@backstage/plugin-tech-insights-common": "^0.1.0", + "@backstage/plugin-tech-insights-node": "^0.1.0", + "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "knex": "^0.95.1", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "node-cron": "^3.0.0", + "semver": "^7.3.5", + "uuid": "^8.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.8", + "@backstage/cli": "^0.8.0", + "@types/supertest": "^2.0.8", + "@types/node-cron": "^3.0.0", + "@types/semver": "^7.3.8", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/tech-insights-backend/src/index.ts b/plugins/tech-insights-backend/src/index.ts new file mode 100644 index 0000000000..5e8e690e8d --- /dev/null +++ b/plugins/tech-insights-backend/src/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './service/router'; +export type { RouterOptions } from './service/router'; + +export { buildTechInsightsContext } from './service/techInsightsContextBuilder'; +export type { + TechInsightsOptions, + TechInsightsContext, +} from './service/techInsightsContextBuilder'; + +export type { PersistenceContext } from './service/persistence/persistenceContext'; +export { createFactRetrieverRegistration } from './service/fact/createFactRetriever'; diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts new file mode 100644 index 0000000000..4dc002465b --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { + FactRetriever, + FactRetrieverRegistration, + FactSchemaDefinition, + TechInsightFact, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-node'; +import { FactRetrieverRegistry } from './FactRetrieverRegistry'; +import { FactRetrieverEngine } from './FactRetrieverEngine'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { schedule } from 'node-cron'; + +jest.mock('node-cron', () => { + const original = jest.requireActual('node-cron'); + return { + ...original, + schedule: jest.fn(), + }; +}); + +const testFactRetriever: FactRetriever = { + id: 'test-factretriever', + version: '0.0.1', + entityFilter: [{ kind: 'component' }], + schema: { + testnumberfact: { + type: 'integer', + description: '', + }, + }, + handler: async () => { + return [ + { + entity: { + namespace: 'a', + kind: 'a', + name: 'a', + }, + facts: { + testnumberfact: 1, + }, + }, + ]; + }, +}; +const cadence = '1 * * * *'; +describe('FactRetrieverEngine', () => { + let engine: FactRetrieverEngine; + let factSchemaAssertionCallback: ( + factSchemaDefinition: FactSchemaDefinition, + ) => void; + let factInsertionAssertionCallback: (facts: TechInsightFact[]) => void; + + const mockRepository: TechInsightsStore = { + insertFacts: (facts: TechInsightFact[]) => { + factInsertionAssertionCallback(facts); + return Promise.resolve(); + }, + insertFactSchema: (def: FactSchemaDefinition) => { + factSchemaAssertionCallback(def); + return Promise.resolve(); + }, + } as unknown as TechInsightsStore; + + const mockFactRetrieverRegistry: FactRetrieverRegistry = { + listRetrievers(): FactRetriever[] { + return [testFactRetriever]; + }, + listRegistrations(): FactRetrieverRegistration[] { + return [{ factRetriever: testFactRetriever, cadence }]; + }, + } as unknown as FactRetrieverRegistry; + + const defaultEngineConfig = { + factRetrieverContext: { + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), + discovery: { + getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + }, + }, + factRetrieverRegistry: mockFactRetrieverRegistry, + repository: mockRepository, + }; + + it('Should update fact retriever schemas on initialization', async () => { + factSchemaAssertionCallback = ({ id, schema, version, entityFilter }) => { + expect(id).toEqual('test-factretriever'); + expect(version).toEqual('0.0.1'); + expect(entityFilter).toEqual([{ kind: 'component' }]); + expect(schema).toEqual({ + testnumberfact: { + type: 'integer', + description: '', + }, + }); + }; + + engine = await FactRetrieverEngine.create(defaultEngineConfig); + }); + it('Should insert facts when scheduled step is run', async () => { + (schedule as jest.Mock).mockImplementation( + (cronCadence: string, retrieverAction: Function) => { + return { + cadence: cronCadence, + triggerScheduledJobNow: retrieverAction, + }; + }, + ); + + factSchemaAssertionCallback = () => {}; + factInsertionAssertionCallback = facts => { + expect(facts).toHaveLength(1); + expect(facts[0]).toEqual({ + ref: 'test-factretriever', + entity: { + namespace: 'a', + kind: 'a', + name: 'a', + }, + facts: { + testnumberfact: 1, + }, + }); + }; + engine = await FactRetrieverEngine.create(defaultEngineConfig); + engine.schedule(); + const job: any = engine.getJob('test-factretriever'); + job.triggerScheduledJobNow(); + expect(job.cadence!!).toEqual(cadence); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts new file mode 100644 index 0000000000..aa2207cca8 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -0,0 +1,135 @@ +/* + * 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 { + FactRetriever, + FactRetrieverContext, + TechInsightsStore, +} from '@backstage/plugin-tech-insights-node'; +import { FactRetrieverRegistry } from './FactRetrieverRegistry'; +import { schedule, validate, ScheduledTask } from 'node-cron'; +import { Logger } from 'winston'; + +function randomDailyCron() { + const rand = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1) + min); + return `${rand(0, 59)} ${rand(0, 23)} * * *`; +} + +function duration(startTimestamp: [number, number]): string { + const delta = process.hrtime(startTimestamp); + const seconds = delta[0] + delta[1] / 1e9; + return `${seconds.toFixed(1)}s`; +} + +export class FactRetrieverEngine { + private scheduledJobs = new Map(); + + constructor( + private readonly repository: TechInsightsStore, + private readonly factRetrieverRegistry: FactRetrieverRegistry, + private readonly factRetrieverContext: FactRetrieverContext, + private readonly logger: Logger, + private readonly defaultCadence?: string, + ) {} + + static async create({ + repository, + factRetrieverRegistry, + factRetrieverContext, + defaultCadence, + }: { + repository: TechInsightsStore; + factRetrieverRegistry: FactRetrieverRegistry; + factRetrieverContext: FactRetrieverContext; + defaultCadence?: string; + }) { + await Promise.all( + factRetrieverRegistry + .listRetrievers() + .map(it => repository.insertFactSchema(it)), + ); + + return new FactRetrieverEngine( + repository, + factRetrieverRegistry, + factRetrieverContext, + factRetrieverContext.logger, + defaultCadence, + ); + } + + schedule() { + const registrations = this.factRetrieverRegistry.listRegistrations(); + const newRegs: string[] = []; + registrations.forEach(registration => { + const { factRetriever, cadence } = registration; + if (!this.scheduledJobs.has(factRetriever.id)) { + const cronExpression = + cadence || this.defaultCadence || randomDailyCron(); + if (!validate(cronExpression)) { + this.logger.warn( + `Validation failed for cron expression ${cronExpression} when trying to schedule fact retriever ${factRetriever.id}`, + ); + return; + } + const job = schedule( + cronExpression, + this.createFactRetrieverHandler(factRetriever), + ); + this.scheduledJobs.set(factRetriever.id, job); + newRegs.push(factRetriever.id); + } + }); + this.logger.info( + `Scheduled ${newRegs.length} fact retrievers to Fact Retriever Engine.`, + ); + } + + getJob(ref: string) { + return this.scheduledJobs.get(ref); + } + + private createFactRetrieverHandler(factRetriever: FactRetriever) { + return async () => { + const startTimestamp = process.hrtime(); + this.logger.info( + `Retrieving facts for fact retriever ${factRetriever.id}`, + ); + const facts = await factRetriever.handler(this.factRetrieverContext); + if (this.logger.isDebugEnabled()) { + this.logger.debug( + `Retrieved ${facts.length} facts for fact retriever ${ + factRetriever.id + } in ${duration(startTimestamp)}`, + ); + } + + try { + await this.repository.insertFacts(factRetriever.id, facts); + this.logger.info( + `Stored ${facts.length} facts for fact retriever ${ + factRetriever.id + } in ${duration(startTimestamp)}`, + ); + } catch (e) { + this.logger.warn( + `Failed to insert facts for fact retriever ${factRetriever.id}`, + e, + ); + } + }; + } +} diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts new file mode 100644 index 0000000000..719f8bf504 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverRegistry.ts @@ -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 { + FactRetriever, + FactRetrieverRegistration, + FactSchema, +} from '@backstage/plugin-tech-insights-node'; +import { ConflictError, NotFoundError } from '@backstage/errors'; + +export class FactRetrieverRegistry { + private readonly retrievers = new Map(); + + constructor(retrievers: FactRetrieverRegistration[]) { + retrievers.forEach(it => { + this.register(it); + }); + } + + register(registration: FactRetrieverRegistration) { + if (this.retrievers.has(registration.factRetriever.id)) { + throw new ConflictError( + `Tech insight fact retriever with identifier '${registration.factRetriever.id}' has already been registered`, + ); + } + this.retrievers.set(registration.factRetriever.id, registration); + } + + get(retrieverReference: string): FactRetriever { + const registration = this.retrievers.get(retrieverReference); + if (!registration) { + throw new NotFoundError( + `Tech insight fact retriever with identifier '${retrieverReference}' is not registered.`, + ); + } + return registration.factRetriever; + } + + listRetrievers(): FactRetriever[] { + return [...this.retrievers.values()].map(it => it.factRetriever); + } + + listRegistrations(): FactRetrieverRegistration[] { + return [...this.retrievers.values()]; + } + + getSchemas(): FactSchema[] { + return this.listRetrievers().map(it => it.schema); + } +} diff --git a/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts new file mode 100644 index 0000000000..655736400a --- /dev/null +++ b/plugins/tech-insights-backend/src/service/fact/createFactRetriever.ts @@ -0,0 +1,52 @@ +/* + * 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 { + FactRetriever, + FactRetrieverRegistration, +} from '@backstage/plugin-tech-insights-node'; + +/** + * @public + * + * A helper function to construct fact retriever registrations. + * + * @param cadence - cron expression to indicate when the fact retriever should be triggered + * @param factRetriever - Implementation of fact retriever consisting of at least id, version, schema and handler + * + * + * @remarks + * + * Cron expressions help: + * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ second (optional) + # โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ minute + # โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ hour + # โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ day of month + # โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€ month + # โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€ day of week + # โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + # โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + # * * * * * * + * + */ +export function createFactRetrieverRegistration( + cadence: string, + factRetriever: FactRetriever, +): FactRetrieverRegistration { + return { + cadence, + factRetriever, + }; +} diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts new file mode 100644 index 0000000000..43d00e2425 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.test.ts @@ -0,0 +1,266 @@ +/* + * 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 { DateTime, Duration } from 'luxon'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { getVoidLogger } from '@backstage/backend-common'; +import { initializePersistenceContext } from './persistenceContext'; + +const factSchemas = [ + { + id: 'test-fact', + version: '0.0.1-test', + entityFilter: JSON.stringify([{ kind: 'component' }]), + schema: JSON.stringify({ + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + }), + }, +]; +const additionalFactSchemas = [ + { + id: 'test-fact', + version: '1.2.1-test', + entityFilter: JSON.stringify([{ kind: 'component' }]), + schema: JSON.stringify({ + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }), + }, + { + id: 'test-fact', + version: '1.1.1-test', + entityFilter: JSON.stringify([{ kind: 'component' }]), + schema: JSON.stringify({ + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }), + }, +]; + +const secondSchema = { + id: 'second-test-fact', + version: '0.0.1-test', + entityFilter: JSON.stringify([{ kind: 'service' }]), + schema: JSON.stringify({ + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }), +}; + +const now = DateTime.now().toISO(); +const shortlyInTheFuture = DateTime.now() + .plus(Duration.fromMillis(555)) + .toISO(); +const farInTheFuture = DateTime.now() + .plus(Duration.fromMillis(555666777)) + .toISO(); + +const facts = [ + { + timestamp: now, + id: 'test-fact', + version: '0.0.1-test', + entity: 'a:a/a', + facts: JSON.stringify({ + testNumberFact: 1, + }), + }, + { + timestamp: shortlyInTheFuture, + id: 'test-fact', + version: '0.0.1-test', + entity: 'a:a/a', + facts: JSON.stringify({ + testNumberFact: 2, + }), + }, +]; + +const additionalFacts = [ + { + timestamp: farInTheFuture, + id: 'test-fact', + version: '0.0.1-test', + entity: 'a:a/a', + facts: JSON.stringify({ + testNumberFact: 3, + }), + }, +]; + +describe('Tech Insights database', () => { + let store: TechInsightsStore; + let testDbClient: Knex; + beforeAll(async () => { + testDbClient = await TestDatabases.create().init('SQLITE_3'); + + store = ( + await initializePersistenceContext(testDbClient, { + logger: getVoidLogger(), + }) + ).techInsightsStore; + + await testDbClient.batchInsert('fact_schemas', factSchemas); + await testDbClient.batchInsert('facts', facts); + }); + + const baseAssertionFact = { + id: 'test-fact', + entity: { namespace: 'a', kind: 'a', name: 'a' }, + timestamp: DateTime.fromISO(shortlyInTheFuture), + version: '0.0.1-test', + facts: { testNumberFact: 2 }, + }; + + it('should be able to return latest schema', async () => { + const schemas = await store.getLatestSchemas(); + expect(schemas[0]).toMatchObject({ + id: 'test-fact', + version: '0.0.1-test', + entityFilter: [{ kind: 'component' }], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + }); + }); + + it('should return last schema based on semver', async () => { + await testDbClient.batchInsert('fact_schemas', additionalFactSchemas); + + const schemas = await store.getLatestSchemas(); + expect(schemas[0]).toMatchObject({ + id: 'test-fact', + version: '1.2.1-test', + entityFilter: [{ kind: 'component' }], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }); + }); + + it('should return multiple schemas if those exists', async () => { + await testDbClient.batchInsert('fact_schemas', [ + { + ...secondSchema, + id: 'second', + }, + ]); + + const schemas = await store.getLatestSchemas(); + expect(schemas).toHaveLength(2); + expect(schemas[0]).toMatchObject({ + id: 'test-fact', + version: '1.2.1-test', + entityFilter: [{ kind: 'component' }], + testNumberFact: { + type: 'integer', + description: 'Test fact with a number type', + }, + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }); + expect(schemas[1]).toMatchObject({ + id: 'second', + version: '0.0.1-test', + entityFilter: [{ kind: 'service' }], + testStringFact: { + type: 'string', + description: 'Test fact with a string type', + }, + }); + }); + + it('should return latest facts only for the correct id', async () => { + const returnedFact = await store.getLatestFactsByIds( + ['test-fact'], + 'a:a/a', + ); + expect(returnedFact['test-fact']).toMatchObject(baseAssertionFact); + }); + + it('should return latest facts for multiple ids', async () => { + await testDbClient.batchInsert('fact_schemas', [secondSchema]); + await testDbClient.batchInsert( + 'facts', + additionalFacts.map(fact => ({ + ...fact, + id: 'second-test-fact', + timestamp: farInTheFuture, + })), + ); + const returnedFacts = await store.getLatestFactsByIds( + ['test-fact', 'second-test-fact'], + 'a:a/a', + ); + + expect(returnedFacts['test-fact']).toMatchObject({ + ...baseAssertionFact, + }); + expect(returnedFacts['second-test-fact']).toMatchObject({ + ...baseAssertionFact, + id: 'second-test-fact', + timestamp: DateTime.fromISO(farInTheFuture), + facts: { testNumberFact: 3 }, + }); + }); + + it('should return facts correctly between time range', async () => { + await testDbClient.batchInsert('facts', additionalFacts); + const returnedFacts = await store.getFactsBetweenTimestampsByIds( + ['test-fact'], + 'a:a/a', + DateTime.fromISO(now), + DateTime.fromISO(shortlyInTheFuture).plus(Duration.fromMillis(10)), + ); + expect(returnedFacts['test-fact']).toHaveLength(2); + + expect(returnedFacts['test-fact'][0]).toMatchObject({ + ...baseAssertionFact, + timestamp: DateTime.fromISO(now), + facts: { testNumberFact: 1 }, + }); + expect(returnedFacts['test-fact'][1]).toMatchObject({ + ...baseAssertionFact, + }); + expect(returnedFacts['test-fact']).not.toContainEqual({ + ...baseAssertionFact, + timestamp: DateTime.fromISO(farInTheFuture), + facts: { testNumberFact: 3 }, + }); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts new file mode 100644 index 0000000000..31a2997af5 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -0,0 +1,190 @@ +/* + * 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 { Knex } from 'knex'; +import { + FactSchema, + TechInsightFact, + FlatTechInsightFact, + TechInsightsStore, + FactSchemaDefinition, +} from '@backstage/plugin-tech-insights-node'; +import { rsort } from 'semver'; +import { groupBy, omit } from 'lodash'; +import { DateTime } from 'luxon'; +import { Logger } from 'winston'; +import { parseEntityName, stringifyEntityRef } from '@backstage/catalog-model'; + +export type RawDbFactRow = { + id: string; + version: string; + timestamp: Date | string; + entity: string; + facts: string; +}; + +type RawDbFactSchemaRow = { + id: string; + version: string; + schema: string; + entityFilter?: string; +}; + +export class TechInsightsDatabase implements TechInsightsStore { + private readonly CHUNK_SIZE = 50; + + constructor(private readonly db: Knex, private readonly logger: Logger) {} + + async getLatestSchemas(ids?: string[]): Promise { + const queryBuilder = this.db('fact_schemas'); + if (ids) { + queryBuilder.whereIn('id', ids); + } + const existingSchemas = await queryBuilder.orderBy('id', 'desc').select(); + + const groupedSchemas = groupBy(existingSchemas, 'id'); + return Object.values(groupedSchemas) + .map(schemas => { + const sorted = rsort(schemas.map(it => it.version)); + return schemas.find(it => it.version === sorted[0])!!; + }) + .map((it: RawDbFactSchemaRow) => ({ + ...omit(it, 'schema'), + ...JSON.parse(it.schema), + entityFilter: it.entityFilter ? JSON.parse(it.entityFilter) : null, + })); + } + + async insertFactSchema(schemaDefinition: FactSchemaDefinition) { + const { id, version, schema, entityFilter } = schemaDefinition; + const existingSchemas = await this.db('fact_schemas') + .where({ id }) + .and.where({ version }) + .select(); + + if (!existingSchemas || existingSchemas.length === 0) { + await this.db('fact_schemas').insert({ + id, + version, + entityFilter: entityFilter ? JSON.stringify(entityFilter) : undefined, + schema: JSON.stringify(schema), + }); + } + } + + async insertFacts(id: string, facts: TechInsightFact[]): Promise { + if (facts.length === 0) return; + const currentSchema = await this.getLatestSchema(id); + const factRows = facts.map(it => { + return { + id, + version: currentSchema.version, + entity: stringifyEntityRef(it.entity), + facts: JSON.stringify(it.facts), + ...(it.timestamp && { timestamp: it.timestamp.toJSDate() }), + }; + }); + await this.db.transaction(async tx => { + await tx.batchInsert('facts', factRows, this.CHUNK_SIZE); + }); + } + + async getLatestFactsByIds( + ids: string[], + entityTriplet: string, + ): Promise<{ [factId: string]: FlatTechInsightFact }> { + const results = await this.db('facts') + .where({ entity: entityTriplet }) + .and.whereIn('id', ids) + .join( + this.db('facts') + .max('timestamp') + .column('id as subId') + .groupBy('id') + .as('subQ'), + 'facts.id', + 'subQ.subId', + ); + return this.dbFactRowsToTechInsightFacts(results); + } + + async getFactsBetweenTimestampsByIds( + ids: string[], + entityTriplet: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ + [factId: string]: FlatTechInsightFact[]; + }> { + const results = await this.db('facts') + .where({ entity: entityTriplet }) + .and.whereIn('id', ids) + .and.whereBetween('timestamp', [ + startDateTime.toISO(), + endDateTime.toISO(), + ]); + + return groupBy( + results.map(it => { + const { namespace, kind, name } = parseEntityName(it.entity); + const timestamp = + typeof it.timestamp === 'string' + ? DateTime.fromISO(it.timestamp) + : DateTime.fromJSDate(it.timestamp); + return { + id: it.id, + entity: { namespace, kind, name }, + timestamp, + version: it.version, + facts: JSON.parse(it.facts), + }; + }), + 'id', + ); + } + + private async getLatestSchema(id: string): Promise { + const existingSchemas = await this.db('fact_schemas') + .where({ id }) + .orderBy('id', 'desc') + .select(); + if (existingSchemas.length < 1) { + this.logger.warn(`No schema found for ${id}. `); + throw new Error(`No schema found for ${id}. `); + } + const sorted = rsort(existingSchemas.map(it => it.version)); + return existingSchemas.find(it => it.version === sorted[0])!!; + } + + private dbFactRowsToTechInsightFacts(rows: RawDbFactRow[]) { + return rows.reduce((acc, it) => { + const { namespace, kind, name } = parseEntityName(it.entity); + const timestamp = + typeof it.timestamp === 'string' + ? DateTime.fromISO(it.timestamp) + : DateTime.fromJSDate(it.timestamp); + return { + ...acc, + [it.id]: { + id: it.id, + entity: { namespace, kind, name }, + timestamp, + version: it.version, + facts: JSON.parse(it.facts), + }, + }; + }, {}); + } +} diff --git a/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts new file mode 100644 index 0000000000..b0fb65349f --- /dev/null +++ b/plugins/tech-insights-backend/src/service/persistence/persistenceContext.ts @@ -0,0 +1,59 @@ +/* + * 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, resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { Logger } from 'winston'; +import { TechInsightsDatabase } from './TechInsightsDatabase'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-tech-insights-backend', + 'migrations', +); + +/** + * A Container for persistence related components in TechInsights + * + * @public + */ +export type PersistenceContext = { + techInsightsStore: TechInsightsStore; +}; + +export type CreateDatabaseOptions = { + logger: Logger; +}; + +const defaultOptions: CreateDatabaseOptions = { + logger: getVoidLogger(), +}; + +/** + * A factory method to construct persistence context for running implementation. + * + * @public + */ +export const initializePersistenceContext = async ( + knex: Knex, + options: CreateDatabaseOptions = defaultOptions, +): Promise => { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return { + techInsightsStore: new TechInsightsDatabase(knex, options.logger), + }; +}; diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts new file mode 100644 index 0000000000..0b7d3b7c45 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { buildTechInsightsContext } from './techInsightsContextBuilder'; +import { createRouter } from './router'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import request from 'supertest'; +import express from 'express'; +import { PersistenceContext } from './persistence/persistenceContext'; +import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { DateTime } from 'luxon'; +import { Knex } from 'knex'; + +describe('Tech Insights router tests', () => { + let app: express.Express; + + const latestFactsByIdsMock = jest.fn(); + const factsBetweenTimestampsByIdsMock = jest.fn(); + const latestSchemasMock = jest.fn(); + + const mockPersistenceContext: PersistenceContext = { + techInsightsStore: { + getLatestFactsByIds: latestFactsByIdsMock, + getFactsBetweenTimestampsByIds: factsBetweenTimestampsByIdsMock, + getLatestSchemas: latestSchemasMock, + } as unknown as TechInsightsStore, + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + beforeAll(async () => { + const techInsightsContext = await buildTechInsightsContext({ + database: { + getClient: () => { + return Promise.resolve({ + migrate: { + latest: () => {}, + }, + }) as unknown as Promise; + }, + }, + logger: getVoidLogger(), + factRetrievers: [], + config: ConfigReader.fromConfigs([]), + discovery: { + getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), + }, + }); + + const router = await createRouter({ + logger: getVoidLogger(), + config: ConfigReader.fromConfigs([]), + ...techInsightsContext, + persistenceContext: mockPersistenceContext, + }); + + app = express().use(router); + }); + + it('should be able to retrieve latest schemas', async () => { + await request(app).get('/fact-schemas').expect(200); + expect(latestSchemasMock).toHaveBeenCalled(); + }); + + it('should not contain check endpoints when checker not present', async () => { + await request(app).get('/checks').expect(404); + await request(app).post('/checks/a/a/a').expect(404); + }); + + it('should be able to parse id request params for fact retrieval', async () => { + await request(app) + .get('/facts/latest') + .query({ + entity: 'a:a/a', + ids: ['firstId', 'secondId'], + }) + .expect(200); + expect(latestFactsByIdsMock).toHaveBeenCalledWith( + ['firstId', 'secondId'], + 'a:a/a', + ); + }); + + it('should be able to parse datetime request params for fact retrieval', async () => { + await request(app) + .get('/facts/range') + .query({ + entity: 'a:a/a', + ids: ['firstId', 'secondId'], + startDatetime: '2021-12-12T12:12:12', + endDatetime: '2022-11-11T11:11:11', + }) + .expect(200); + expect(factsBetweenTimestampsByIdsMock).toHaveBeenCalledWith( + ['firstId', 'secondId'], + 'a:a/a', + DateTime.fromISO('2021-12-12T12:12:12.000+00:00'), + DateTime.fromISO('2022-11-11T11:11:11.000+00:00'), + ); + }); + + it('should respond gracefully on parsing errors', async () => { + await request(app) + .get('/facts/range') + .query({ + entity: 'a:a/a', + ids: ['firstId', 'secondId'], + startDatetime: '2021-12-1222T12:12:12', + endDatetime: '2022-1122-11T11:11:11', + }) + .expect(422); + expect(latestFactsByIdsMock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts new file mode 100644 index 0000000000..a8951318ea --- /dev/null +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -0,0 +1,162 @@ +/* + * 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 express from 'express'; +import Router from 'express-promise-router'; +import { Config } from '@backstage/config'; +import { + FactChecker, + TechInsightCheck, +} from '@backstage/plugin-tech-insights-node'; + +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Logger } from 'winston'; +import { DateTime } from 'luxon'; +import { PersistenceContext } from './persistence/persistenceContext'; +import { + EntityRef, + parseEntityName, + stringifyEntityRef, +} from '@backstage/catalog-model'; + +/** + * @public + * + * RouterOptions to construct TechInsights endpoints + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + */ +export interface RouterOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * Optional FactChecker implementation. If omitted, endpoints are not constructed + */ + factChecker?: FactChecker; + + /** + * TechInsights PersistenceContext. Should contain an implementation of TechInsightsStore + */ + persistenceContext: PersistenceContext; + + /** + * Backstage config object + */ + config: Config; + + /** + * Implementation of Winston logger + */ + logger: Logger; +} + +/** + * @public + * + * Constructs a tech-insights router. + * + * Exposes endpoints to handle facts + * Exposes optional endpoints to handle checks if a FactChecker implementation is passed in + * + * @param options - RouterOptions object + */ +export async function createRouter< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>(options: RouterOptions): Promise { + const router = Router(); + router.use(express.json()); + const { persistenceContext, factChecker, logger } = options; + const { techInsightsStore } = persistenceContext; + + if (factChecker) { + logger.info('Fact checker configured. Enabling fact checking endpoints.'); + router.get('/checks', async (_req, res) => { + return res.send(await factChecker.getChecks()); + }); + + 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); + return res.send(checkResult); + } catch (e) { + return res.status(500).json({ message: e.message }).send(); + } + }); + } else { + logger.info( + 'Starting tech insights module without fact checking endpoints.', + ); + } + + router.get('/fact-schemas', async (req, res) => { + const ids = req.query.ids as string[]; + return res.send(await techInsightsStore.getLatestSchemas(ids)); + }); + + /** + * /facts/latest?entity=component:default/mycomponent&ids[]=factRetrieverId1&ids[]=factRetrieverId2 + */ + router.get('/facts/latest', async (req, res) => { + const { entity } = req.query; + const { namespace, kind, name } = parseEntityName(entity as EntityRef); + const ids = req.query.ids as string[]; + return res.send( + await techInsightsStore.getLatestFactsByIds( + ids, + stringifyEntityRef({ namespace, kind, name }), + ), + ); + }); + + /** + * /facts/latest?entity=component:default/mycomponent&startDateTime=2021-12-24T01:23:45&endDateTime=2021-12-31T23:59:59&ids[]=factRetrieverId1&ids[]=factRetrieverId2 + */ + router.get('/facts/range', async (req, res) => { + const { entity } = req.query; + const { namespace, kind, name } = parseEntityName(entity as EntityRef); + + const ids = req.query.ids as string[]; + const startDatetime = DateTime.fromISO(req.query.startDatetime as string); + const endDatetime = DateTime.fromISO(req.query.endDatetime as string); + if (!startDatetime.isValid || !endDatetime.isValid) { + return res.status(422).send({ + message: 'Failed to parse datetime from request', + field: !startDatetime.isValid ? 'startDateTime' : 'endDateTime', + value: !startDatetime.isValid ? startDatetime : endDatetime, + }); + } + const entityTriplet = stringifyEntityRef({ namespace, kind, name }); + return res.send( + await techInsightsStore.getFactsBetweenTimestampsByIds( + ids, + entityTriplet, + startDatetime, + endDatetime, + ), + ); + }); + return router; +} diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts new file mode 100644 index 0000000000..75e24b7c06 --- /dev/null +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -0,0 +1,138 @@ +/* + * 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 { FactRetrieverEngine } from './fact/FactRetrieverEngine'; +import { Logger } from 'winston'; +import { FactRetrieverRegistry } from './fact/FactRetrieverRegistry'; +import { Config } from '@backstage/config'; +import { + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { + FactChecker, + FactCheckerFactory, + FactRetrieverRegistration, + TechInsightCheck, +} from '@backstage/plugin-tech-insights-node'; +import { + initializePersistenceContext, + PersistenceContext, +} from './persistence/persistenceContext'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; + +/** + * @public + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * + * Configuration options to initialize TechInsightsBuilder. Generic types params are needed if FactCheckerFactory + * is included for FactChecker creation. + */ +export interface TechInsightsOptions< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * A collection of FactRetrieverRegistrations. + * Used to register FactRetrievers and their schemas and schedule an execution loop for them. + */ + factRetrievers: FactRetrieverRegistration[]; + + /** + * Optional factory exposing a `construct` method to initialize a FactChecker implementation + */ + factCheckerFactory?: FactCheckerFactory; + + logger: Logger; + config: Config; + discovery: PluginEndpointDiscovery; + database: PluginDatabaseManager; +} + +/** + * @public + * @typeParam CheckType - Type of the check for the fact checker this builder returns + * @typeParam CheckResultType - Type of the check result for the fact checker this builder returns + * + * A container for exported implementations related to TechInsights. + * FactChecker is present if an optional FactCheckerFactory is included in the build stage. + */ +export type TechInsightsContext< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> = { + factChecker?: FactChecker; + persistenceContext: PersistenceContext; +}; + +/** + * @public + * + * Constructs needed persistence context, fact retriever engine + * and optionally fact checker implementations to be used in the tech insights module. + * + * @param options - Needed options to construct TechInsightsContext + * @returns TechInsightsContext with persistence implementations and optionally an implementation of a FactChecker + */ +export const buildTechInsightsContext = async < + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +>( + options: TechInsightsOptions, +): Promise> => { + const { + factRetrievers, + factCheckerFactory, + config, + discovery, + database, + logger, + } = options; + + const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); + + const persistenceContext = await initializePersistenceContext( + await database.getClient(), + { logger }, + ); + + const factRetrieverEngine = await FactRetrieverEngine.create({ + repository: persistenceContext.techInsightsStore, + factRetrieverRegistry, + factRetrieverContext: { + config, + discovery, + logger, + }, + }); + + factRetrieverEngine.schedule(); + + if (factCheckerFactory) { + const factChecker = factCheckerFactory.construct( + persistenceContext.techInsightsStore, + ); + return { + persistenceContext, + factChecker, + }; + } + + return { + persistenceContext, + }; +}; diff --git a/plugins/tech-insights-backend/src/setupTests.ts b/plugins/tech-insights-backend/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-backend/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. + */ + +export {}; diff --git a/plugins/tech-insights-common/.eslintrc.js b/plugins/tech-insights-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-common/README.md b/plugins/tech-insights-common/README.md new file mode 100644 index 0000000000..af14ef0e0d --- /dev/null +++ b/plugins/tech-insights-common/README.md @@ -0,0 +1,3 @@ +# Tech Insights Common + +Common types and functionalities for tech insights shared in an isomorphic manner between BE and FE implementations. diff --git a/plugins/tech-insights-common/api-report.md b/plugins/tech-insights-common/api-report.md new file mode 100644 index 0000000000..ddb38cfa85 --- /dev/null +++ b/plugins/tech-insights-common/api-report.md @@ -0,0 +1,43 @@ +## API Report File for "@backstage/plugin-tech-insights-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { DateTime } from 'luxon'; + +// @public +export interface BooleanCheckResult extends CheckResult { + // (undocumented) + result: boolean; +} + +// @public +export interface CheckResponse { + description: string; + factIds: string[]; + id: string; + metadata?: Record; + name: string; + type: string; +} + +// @public +export type CheckResult = { + facts: FactResponse; + check: CheckResponse; +}; + +// @public +export type FactResponse = { + [id: string]: { + id: string; + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + description: string; + value: number | string | boolean | DateTime | []; + since?: string; + metadata?: Record; + }; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json new file mode 100644 index 0000000000..5e02c9e4b0 --- /dev/null +++ b/plugins/tech-insights-common/package.json @@ -0,0 +1,42 @@ +{ + "name": "@backstage/plugin-tech-insights-common", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-insights-common" + }, + "keywords": [ + "backstage", + "tech-insights" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@types/luxon": "^2.0.5", + "luxon": "^2.0.2" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-common/src/index.test.ts b/plugins/tech-insights-common/src/index.test.ts new file mode 100644 index 0000000000..0c54118f9a --- /dev/null +++ b/plugins/tech-insights-common/src/index.test.ts @@ -0,0 +1,24 @@ +/* + * 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 * as anything from './'; + +describe('tech-insights-common', () => { + // Types only + it('should exist', () => { + expect(anything).toBeTruthy(); + }); +}); diff --git a/plugins/tech-insights-common/src/index.ts b/plugins/tech-insights-common/src/index.ts new file mode 100644 index 0000000000..4bfc660748 --- /dev/null +++ b/plugins/tech-insights-common/src/index.ts @@ -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 { DateTime } from 'luxon'; + +/** + * @public + * + * Response type for checks. + */ +export interface CheckResponse { + /** + * Identifier of the Check + */ + id: string; + /** + * Type identifier for the check. + * Can be used to determine storage options, logical routing to correct FactChecker implementation + * or to help frontend render correct component types based on this + */ + type: string; + /** + * Human readable name of the Check + */ + name: string; + /** + * Description of the Check + */ + description: string; + + /** + * A collection of references to fact rows used to run this checks against + */ + factIds: string[]; + + /** + * Metadata related to a check. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; +} + +/** + * @public + * + * Individual fact response type. + * Keyed by the name of the fact + */ +export type FactResponse = { + [id: string]: { + /** + * Reference and unique identifier of the fact row + */ + id: string; + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values + */ + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + + /** + * Description of the individual fact + */ + description: string; + + /** + * Actual value of the fact + */ + value: number | string | boolean | DateTime | []; + + /** + * An optional SemVer version identifying when this fact was added to the FactSchema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + */ + metadata?: Record; + }; +}; + +/** + * Generic CheckResult + * + * Contains information about the facts used to calculate the check result + * and information about the check itself. Both may include metadata to be able to display additional information. + * A collection of these should be parseable by the frontend to display scorecards + * + * @public + */ +export type CheckResult = { + facts: FactResponse; + check: CheckResponse; +}; + +/** + * CheckResult of type Boolean. + * + * @public + */ +export interface BooleanCheckResult extends CheckResult { + result: boolean; +} diff --git a/plugins/tech-insights-node/.eslintrc.js b/plugins/tech-insights-node/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/tech-insights-node/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/tech-insights-node/README.md b/plugins/tech-insights-node/README.md new file mode 100644 index 0000000000..4065611837 --- /dev/null +++ b/plugins/tech-insights-node/README.md @@ -0,0 +1,3 @@ +# Tech Insights Node + +Common types and functionalities for tech insights backend implementations to be shared between tech-insights-backend and individual implementations of FactRetrievers, FactCheckers and their persistence options. diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md new file mode 100644 index 0000000000..caf4b4cf0e --- /dev/null +++ b/plugins/tech-insights-node/api-report.md @@ -0,0 +1,149 @@ +## API Report File for "@backstage/plugin-tech-insights-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CheckResult } from '@backstage/plugin-tech-insights-common'; +import { Config } from '@backstage/config'; +import { DateTime } from 'luxon'; +import { Logger as Logger_2 } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public +export type CheckValidationResponse = { + valid: boolean; + message?: string; + errors?: unknown[]; +}; + +// @public +export interface FactChecker< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + getChecks(): Promise; + runChecks(entity: string, checks?: string[]): Promise; + validate(check: CheckType): Promise; +} + +// @public +export interface FactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + // (undocumented) + construct( + repository: TechInsightsStore, + ): FactChecker; +} + +// @public +export interface FactRetriever { + entityFilter?: + | Record[] + | Record; + handler: (ctx: FactRetrieverContext) => Promise; + id: string; + schema: FactSchema; + version: string; +} + +// @public +export type FactRetrieverContext = { + config: Config; + discovery: PluginEndpointDiscovery; + logger: Logger_2; +}; + +// @public +export type FactRetrieverRegistration = { + factRetriever: FactRetriever; + cadence?: string; +}; + +// @public +export type FactSchema = { + [name: string]: { + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + description: string; + since?: string; + metadata?: Record; + }; +}; + +// @public +export type FactSchemaDefinition = Omit; + +// @public +export type FlatTechInsightFact = TechInsightFact & { + id: string; +}; + +// @public +export interface TechInsightCheck { + description: string; + factIds: string[]; + failureMetadata?: Record; + id: string; + name: string; + successMetadata?: Record; + type: string; +} + +// @public +export interface TechInsightCheckRegistry { + // (undocumented) + get(checkId: string): Promise; + // (undocumented) + getAll(checks: string[]): Promise; + // (undocumented) + list(): Promise; + // (undocumented) + register(check: CheckType): Promise; +} + +// @public +export type TechInsightFact = { + entity: { + namespace: string; + kind: string; + name: string; + }; + facts: Record< + string, + | number + | string + | boolean + | DateTime + | number[] + | string[] + | boolean[] + | DateTime[] + >; + timestamp?: DateTime; +}; + +// @public +export interface TechInsightsStore { + getFactsBetweenTimestampsByIds( + ids: string[], + entity: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ + [factRef: string]: FlatTechInsightFact[]; + }>; + // (undocumented) + getLatestFactsByIds( + ids: string[], + entity: string, + ): Promise<{ + [factRef: string]: FlatTechInsightFact; + }>; + getLatestSchemas(ids?: string[]): Promise; + insertFacts(id: string, facts: TechInsightFact[]): Promise; + insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json new file mode 100644 index 0000000000..4a7cf3589b --- /dev/null +++ b/plugins/tech-insights-node/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-tech-insights-node", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-insights-node" + }, + "keywords": [ + "backstage", + "tech-insights" + ], + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.0", + "@backstage/config": "^0.1.8", + "@backstage/plugin-tech-insights-common": "^0.1.0 ", + "@types/luxon": "^2.0.5", + "luxon": "^2.0.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/tech-insights-node/src/checks.ts b/plugins/tech-insights-node/src/checks.ts new file mode 100644 index 0000000000..e5f10aff6d --- /dev/null +++ b/plugins/tech-insights-node/src/checks.ts @@ -0,0 +1,153 @@ +/* + * 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 { TechInsightsStore } from './persistence'; +import { CheckResult } from '@backstage/plugin-tech-insights-common'; + +/** + * A factory wrapper to construct FactChecker implementations. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * @typeParam CheckResultType - Implementation specific result of a check. Can extend CheckResult with additional information + */ +export interface FactCheckerFactory< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * @param repository - TechInsightsStore + * @returns an implementation of a FactChecker for generic types defined in the factory + */ + construct( + repository: TechInsightsStore, + ): FactChecker; +} + +/** + * FactChecker interface + * + * A generic interface that can be implemented to create checkers for specific check and check return types. + * This is used especially when creating Scorecards and displaying results of rules when run against facts. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * @typeParam CheckResultType - Implementation specific result of a check. Can extend CheckResult with additional information + */ +export interface FactChecker< + CheckType extends TechInsightCheck, + CheckResultType extends CheckResult, +> { + /** + * Runs checks against an entity. + * + * @param entity - A reference to an entity to run checks against. In a format namespace/kind/name + * @param checks - A collection of checks to run against provided entity + * @returns - A collection containing check/fact information and the actual results of the check + */ + runChecks(entity: string, checks?: string[]): Promise; + + /** + * Retrieves all available checks that can be used to run checks against. + * The implementation can be just a piping through to CheckRegistry implementation if such is in use. + * + * @returns - A collection of checks + */ + getChecks(): Promise; + + /** + * Validates if check is valid and can be run with the current implementation + * + * @param check - The check to be validated + * @returns - Validation result + */ + validate(check: CheckType): Promise; +} + +/** + * Registry containing checks for tech insights. + * + * @public + * @typeParam CheckType - Implementation specific Check. Can extend TechInsightCheck with additional information + * + */ +export interface TechInsightCheckRegistry { + register(check: CheckType): Promise; + get(checkId: string): Promise; + getAll(checks: string[]): Promise; + list(): Promise; +} + +/** + * Generic definition of a check for Tech Insights + * + * @public + */ +export interface TechInsightCheck { + /** + * Unique identifier of the check + * + * Used to identify which checks to use when running checks. + */ + id: string; + + /** + * Type identifier for the check. + * Can be used to determine storage options, logical routing to correct FactChecker implementation + * or to help frontend render correct component types based on this + */ + type: string; + + /** + * Human readable name of the check, may be displayed in the UI + */ + name: string; + + /** + * Human readable description of the check, may be displayed in the UI + */ + description: string; + + /** + * A collection of string referencing fact rows that a check will be run against. + * + * References the fact container, aka fact retriever itself which may or may not contain multiple individual facts and values + */ + factIds: string[]; + + /** + * Metadata to be returned in case a check has been successfully evaluated + * Can contain links, description texts or other actionable items + */ + successMetadata?: Record; + + /** + * Metadata to be returned in case a check evaluation has ended in failure + * Can contain links, description texts or other actionable items + */ + failureMetadata?: Record; +} + +/** + * Validation response from CheckValidator + * + * May contain additional data for display purposes + * @public + */ +export type CheckValidationResponse = { + valid: boolean; + message?: string; + errors?: unknown[]; +}; diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts new file mode 100644 index 0000000000..a52a91f2f4 --- /dev/null +++ b/plugins/tech-insights-node/src/facts.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 { DateTime } from 'luxon'; +import { Config } from '@backstage/config'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Logger } from 'winston'; + +/** + * A container for facts. The shape of the fact records needs to correspond to the FactSchema with same `ref` value. + * Each container contains a reference to an entity which can be a Backstage entity or a generic construct + * outside of backstage with same shape. + * + * Container may contain multiple individual facts and their values + * + * @public + */ +export type TechInsightFact = { + /** + * Entity reference that this fact relates to + */ + entity: { + namespace: string; + kind: string; + name: string; + }; + + /** + * A collection of fact values as key value pairs. + * + * Key indicates fact name as it is defined in FactSchema + */ + facts: Record< + string, + | number + | string + | boolean + | DateTime + | number[] + | string[] + | boolean[] + | DateTime[] + >; + + /** + * Optional timestamp value which can be used to override retrieval time of the fact row. + * Otherwise when stored into data storage, defaults to current time + */ + timestamp?: DateTime; +}; + +/** + + * Response type used when returning from database and API. + * Adds a field for ref for easier usage + * + * @public + */ +export type FlatTechInsightFact = TechInsightFact & { + /** + * Reference and unique identifier of the fact row + */ + id: string; +}; + +/** + * @public + * + * A record type to specify individual fact shapes + * + * Used as part of a schema to validate, identify and generically construct usage implementations + * of individual fact values in the system. + */ +export type FactSchema = { + /** + * Name of the fact + */ + [name: string]: { + /** + * Type of the individual fact value + * + * Numbers are split into integers and floating point values. + * `set` indicates a collection of values + */ + type: 'integer' | 'float' | 'string' | 'boolean' | 'datetime' | 'set'; + + /** + * A description of this individual fact value + */ + description: string; + + /** + * Optional semver string to indicate when this specific fact definition was added to the schema + */ + since?: string; + + /** + * Metadata related to an individual fact. + * Can contain links, additional description texts and other actionable data. + * + * Currently loosely typed, but in the future when patterns emerge, key shapes can be defined + * + * examples: + * ``` + * \{ + * link: 'https://sonarqube.mycompany.com/fix-these-issues', + * suggestion: 'To affect this value, you can do x, y, z', + * minValue: 0 + * \} + * ``` + */ + metadata?: Record; + }; +}; + +/** + * @public + * + * FactRetrieverContext injected into individual handler methods of FactRetriever implementations. + * The context can be used to construct logic to retrieve entities, contact integration points + * and fetch and calculate fact values from external sources. + */ +export type FactRetrieverContext = { + config: Config; + discovery: PluginEndpointDiscovery; + logger: Logger; +}; + +/** + * @public + * + * FactRetriever interface + * + * A component specifying + */ +export interface FactRetriever { + /** + * A unique identifier of the retriever. + * Used to identify and store individual facts returned from this retriever + * and schemas defined by this retriever. + */ + id: string; + + /** + * Semver string indicating the version of this fact retriever + * This version is used to determine if the schema this fact retriever matches the data this fact retriever provides. + * + * Should be incremented on changes to returned data from the handler or if the schema changes. + */ + version: string; + + /** + * Handler function that needs to be implemented to retrieve fact values for entities. + * + * @param ctx - FactRetrieverContext which can be used to retrieve config and contact integrations + * @returns - A collection of TechInsightFacts grouped by entities. + */ + handler: (ctx: FactRetrieverContext) => Promise; + + /** + * A fact schema defining the shape of data returned from the handler method for each entity + */ + schema: FactSchema; + + /** + * An optional object/array of objects of entity filters to indicate if this fact retriever is valid for an entity type. + * If omitted, the retriever should apply to all entities. + * + * Should be defined for example: + * \{ field: 'kind', values: \['component'\] \} + * \{ field: 'metadata.name', values: \['component-1', 'component-2'\] \} + */ + entityFilter?: + | Record[] + | Record; +} + +/** + * @public + * + * A flat serializable structure for Facts. + * Containing information about fact schema, version, id, and entity filters + */ +export type FactSchemaDefinition = Omit; + +/** + * @public + * + * Registration of a fact retriever + * Used to add and schedule individual fact retrievers to the fact retriever engine. + */ +export type FactRetrieverRegistration = { + /** + * Actual FactRetriever implementation + */ + factRetriever: FactRetriever; + + /** + * Cron expression to indicate when the retriever should be triggered. + * Defaults to a random point in time every 24h + * + */ + cadence?: string; +}; diff --git a/plugins/tech-insights-node/src/index.test.ts b/plugins/tech-insights-node/src/index.test.ts new file mode 100644 index 0000000000..5e5f8c9d77 --- /dev/null +++ b/plugins/tech-insights-node/src/index.test.ts @@ -0,0 +1,24 @@ +/* + * 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 * as anything from './'; + +describe('tech-insights-node', () => { + // Types only + it('should exist', () => { + expect(anything).toBeTruthy(); + }); +}); diff --git a/plugins/tech-insights-node/src/index.ts b/plugins/tech-insights-node/src/index.ts new file mode 100644 index 0000000000..70ef27e159 --- /dev/null +++ b/plugins/tech-insights-node/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 './checks'; +export * from './facts'; +export * from './persistence'; diff --git a/plugins/tech-insights-node/src/persistence.ts b/plugins/tech-insights-node/src/persistence.ts new file mode 100644 index 0000000000..444e961008 --- /dev/null +++ b/plugins/tech-insights-node/src/persistence.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 { + FactSchema, + TechInsightFact, + FlatTechInsightFact, + FactSchemaDefinition, +} from './facts'; +import { DateTime } from 'luxon'; + +/** + * TechInsights Database + * + * @public + */ +export interface TechInsightsStore { + /** + * Stores fact containers as rows into data store. + * Individual items in array correspond to a fact schema based on reference and entity based on entity identifier. + * + * Each row may contain multiple individual facts and values + * + * @param id - Unique identifier of the fact retriever these facts relate to + * @param facts - A collection of TechInsightFacts + */ + insertFacts(id: string, facts: TechInsightFact[]): Promise; + + /** + * @param ids - A collection of fact row identifiers + * @param entity - A string identifying an entity. In a format namespace/kind/name + * + * @returns - An object keyed by a fact reference and containing an individual TechInsightFact + */ + getLatestFactsByIds( + ids: string[], + entity: string, + ): Promise<{ [factRef: string]: FlatTechInsightFact }>; + + /** + * Retrieves fact values identified by fact row references for an individual entity. + * + * @param ids - A collection of fact row identifiers + * @param entity - A string identifying an entity. In a format namespace/kind/name + * @param startDateTime - DateTime object indicating start of the time frame + * @param endDateTime - DateTime object indicating start of the time frame + * + * @returns - An object keyed by a fact reference and containing a collection of TechInsightFacts matching the time frame + */ + getFactsBetweenTimestampsByIds( + ids: string[], + entity: string, + startDateTime: DateTime, + endDateTime: DateTime, + ): Promise<{ [factRef: string]: FlatTechInsightFact[] }>; + + /** + * Stores versioned fact schemas into data store + * + * @param schemaDefinition - FactSchemaDefinition containing id, version, schema and entityTypes. + */ + insertFactSchema(schemaDefinition: FactSchemaDefinition): Promise; + + /** + * Retrieves latest versions (as defined by semver) of fact schemas from the data store. + * + * @param ids - Collection of ids to return. If omitted, all Schemas should be returned. + * @returns - A collection of schemas + */ + getLatestSchemas(ids?: string[]): Promise; +} diff --git a/plugins/tech-insights-node/src/setupTests.ts b/plugins/tech-insights-node/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/tech-insights-node/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. + */ + +export {}; diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index bf146c4c2a..839ad1b399 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,9 +31,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -45,10 +45,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -58,7 +58,7 @@ "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 221a681203..7331ea0147 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-techdocs-backend +## 0.10.7 + +### Patch Changes + +- b45607a2ec: Make techdocs s3 publisher credentials config schema optional. + +## 0.10.6 + +### Patch Changes + +- 106a5dc3ad: Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + - @backstage/techdocs-common@0.10.5 + ## 0.10.5 ### Patch Changes diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index ac31474fa9..0023a4b757 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -83,12 +83,12 @@ export interface Config { * User access key id * @visibility secret */ - accessKeyId: string; + accessKeyId?: string; /** * User secret access key * @visibility secret */ - secretAccessKey: string; + secretAccessKey?: string; /** * ARN of role to be assumed * @visibility backend diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index c4fec93c10..c71833948b 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.5", + "version": "0.10.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.7", + "@backstage/backend-common": "^0.9.8", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.7", - "@backstage/search-common": "^0.2.0", - "@backstage/techdocs-common": "^0.10.4", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/search-common": "^0.2.1", + "@backstage/techdocs-common": "^0.10.5", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", @@ -51,10 +51,10 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.18", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@types/dockerode": "^3.2.1", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 006cfdd073..21be228636 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { msw } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -90,7 +90,7 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { let collator: DefaultTechDocsCollator; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), @@ -138,6 +138,8 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { componentType: entity!.spec!.type, lifecycle: entity!.spec!.lifecycle, owner: '', + kind: entity.kind, + name: entity.metadata.name, }); }); }); @@ -148,7 +150,7 @@ describe('DefaultTechDocsCollator', () => { let collator: DefaultTechDocsCollator; const worker = setupServer(); - msw.setupDefaultHandlers(worker); + setupRequestMockHandlers(worker); beforeEach(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), @@ -183,6 +185,8 @@ describe('DefaultTechDocsCollator', () => { componentType: entity!.spec!.type, lifecycle: entity!.spec!.lifecycle, owner: '', + kind: entity.kind, + name: entity.metadata.name, }); }); }); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index c0a96b7862..121ef6e46d 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -130,7 +130,9 @@ export class DefaultTechDocsCollator implements DocumentCollator { path: doc.location, }), path: doc.location, - ...entityInfo, + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, entityTitle: entity.metadata.title, componentType: entity.spec?.type?.toString() || 'other', lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index f3b94c8579..be100b9dea 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-techdocs +## 0.12.5 + +### Patch Changes + +- fe5738fe1c: Lazy load `LazyLog` as it is rarely used. +- 53c9ad7e04: Update font weight for headings in TechDocs +- Updated dependencies + - @backstage/core-components@0.7.3 + - @backstage/theme@0.2.13 + - @backstage/plugin-search@0.4.17 + - @backstage/core-plugin-api@0.1.13 + - @backstage/plugin-catalog-react@0.6.3 + +## 0.12.4 + +### Patch Changes + +- a9a8c6f7c5: Reader will now scroll to the top of the page when navigating between pages +- 106a5dc3ad: Restore original casing for `kind`, `namespace` and `name` in `DefaultTechDocsCollator`. +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/theme@0.2.12 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/core-components@0.7.2 + - @backstage/integration-react@0.1.13 + - @backstage/plugin-catalog-react@0.6.2 + - @backstage/catalog-model@0.9.6 + - @backstage/plugin-search@0.4.16 + - @backstage/core-plugin-api@0.1.12 + ## 0.12.3 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 0aecd7d17c..79841a0621 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.3", + "version": "0.12.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,17 +32,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.5", - "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/errors": "^0.1.3", - "@backstage/integration": "^0.6.8", - "@backstage/integration-react": "^0.1.12", + "@backstage/catalog-model": "^0.9.6", + "@backstage/config": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/errors": "^0.1.4", + "@backstage/integration": "^0.6.9", + "@backstage/integration-react": "^0.1.13", "@backstage/plugin-catalog": "^0.7.2", - "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/plugin-search": "^0.4.15", - "@backstage/theme": "^0.2.11", + "@backstage/plugin-catalog-react": "^0.6.3", + "@backstage/plugin-search": "^0.4.17", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -61,10 +61,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -74,7 +74,7 @@ "@types/node": "^14.14.32", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist", diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 9bb0a7bbad..4e12ac18d1 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -198,6 +198,7 @@ export const useTechDocsReaderDom = (): Element | null => { .md-footer-nav__link { width: 20rem;} .md-content { margin-left: 20rem; max-width: calc(100% - 20rem * 2 - 3rem); } .md-typeset { font-size: 1rem; } + .md-typeset h1, .md-typeset h2, .md-typeset h3 { font-weight: bold; } .md-nav { font-size: 1rem; } .md-grid { max-width: 90vw; margin: 0 } .md-typeset table:not([class]) { diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx index d1bb99b76c..fa82527282 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx @@ -23,9 +23,9 @@ import { // 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', () => { +jest.mock('react-lazylog/build/LazyLog', () => { return { - LazyLog: ({ text }: { text: string }) => { + default: ({ text }: { text: string }) => { return

{text}

; }, }; @@ -46,18 +46,20 @@ describe('', () => { }); describe('', () => { - it('should render with empty log', () => { + it('should render with empty log', async () => { const onClose = jest.fn(); const rendered = render( , ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); - expect(rendered.getByText(/Waiting for logs.../i)).toBeInTheDocument(); + expect( + await rendered.findByText(/Waiting for logs.../i), + ).toBeInTheDocument(); expect(onClose).toBeCalledTimes(0); }); - it('should render with empty logs', () => { + it('should render logs', async () => { const onClose = jest.fn(); const rendered = render( ', () => { />, ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); - expect( - rendered.queryByText(/Waiting for logs.../i), - ).not.toBeInTheDocument(); - expect(rendered.getByText(/Line 1/i)).toBeInTheDocument(); - expect(rendered.getByText(/Line 2/i)).toBeInTheDocument(); + expect(await rendered.findByText(/Line 1/i)).toBeInTheDocument(); + expect(await rendered.findByText(/Line 2/i)).toBeInTheDocument(); expect(onClose).toBeCalledTimes(0); }); @@ -80,7 +79,7 @@ describe('', () => { const rendered = render( , ); - rendered.getByRole('button').click(); + rendered.getByTitle('Close the drawer').click(); expect(onClose).toBeCalledTimes(1); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx index 4283932fb3..8c739a9bcf 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Progress } from '@backstage/core-components'; import { Button, createStyles, @@ -25,9 +26,9 @@ import { Typography, } from '@material-ui/core'; import Close from '@material-ui/icons/Close'; -import * as React from 'react'; -import { useState } from 'react'; -import { LazyLog } from 'react-lazylog'; +import React, { Suspense, useState } from 'react'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -83,15 +84,17 @@ export const TechDocsBuildLogsDrawerContent = ({ - + }> + + ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx index c6f017e92d..602a94e758 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { buildInitialFilters, TechDocsSearch } from './TechDocsSearch'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { searchApiRef } from '@backstage/plugin-search'; +import { wrapInTestApp } from '@backstage/test-utils'; import { act, fireEvent, @@ -22,9 +23,8 @@ import { waitFor, within, } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { searchApiRef } from '@backstage/plugin-search'; +import React from 'react'; +import { TechDocsSearch } from './TechDocsSearch'; const entityId = { name: 'test', @@ -89,7 +89,7 @@ describe('', () => { await singleResult; expect(querySpy).toBeCalledWith({ filters: { - kind: 'testable', + kind: 'Testable', name: 'test', namespace: 'testspace', }, @@ -108,7 +108,7 @@ describe('', () => { await waitFor(() => expect(querySpy).toBeCalledWith({ filters: { - kind: 'testable', + kind: 'Testable', name: 'test', namespace: 'testspace', }, @@ -120,23 +120,3 @@ describe('', () => { }); }); }); - -describe('buildInitialFilters', () => { - const filterEnt = { - name: 'Test', - kind: 'TestKind', - namespace: 'TeStNaMeSpAcE', - }; - it('should use filters as is when legacy path', () => { - const filters = buildInitialFilters(true, filterEnt); - expect(filters).toStrictEqual(filterEnt); - }); - it('should lowercase all filters for new approach', () => { - const filters = buildInitialFilters(false, filterEnt); - expect(filters).toStrictEqual({ - name: 'test', - kind: 'testkind', - namespace: 'testnamespace', - }); - }); -}); diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.tsx index 64229e6620..5bc1947ed6 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import React, { ChangeEvent, useEffect, useState } from 'react'; +import { EntityName } from '@backstage/catalog-model'; +import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; import { CircularProgress, Grid, @@ -22,21 +23,15 @@ import { InputAdornment, TextField, } from '@material-ui/core'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; -import { DocsResultListItem } from '../../components/DocsResultListItem'; import SearchIcon from '@material-ui/icons/Search'; -import { useDebounce } from 'react-use'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { ChangeEvent, useEffect, useState } from 'react'; import { useNavigate } from 'react-router'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useDebounce } from 'react-use'; +import { DocsResultListItem } from '../../components/DocsResultListItem'; -type EntityId = { - name: string; - namespace: string; - kind: string; -}; type TechDocsSearchProps = { - entityId: EntityId; + entityId: EntityName; debounceTime?: number; }; @@ -54,17 +49,6 @@ type TechDocsSearchResult = { document: TechDocsDoc; }; -export const buildInitialFilters = ( - legacyPaths: boolean, - entityId: EntityId, -) => { - return legacyPaths - ? entityId - : Object.entries(entityId).reduce((acc, [key, value]) => { - return { ...acc, [key]: value?.toLocaleLowerCase('en-US') }; - }, {}); -}; - const TechDocsSearchBar = ({ entityId, debounceTime = 150, @@ -176,15 +160,11 @@ const TechDocsSearchBar = ({ }; export const TechDocsSearch = (props: TechDocsSearchProps) => { - const configApi = useApi(configApiRef); - const legacyPaths = configApi.getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ); const initialState = { term: '', types: ['techdocs'], pageCursor: '', - filters: buildInitialFilters(legacyPaths || false, props.entityId), + filters: props.entityId, }; return ( diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 67c990cf13..c3f0e469aa 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@backstage/cli": "^0.8.0", "@types/supertest": "^2.0.8", - "msw": "^0.29.0", + "msw": "^0.35.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/todo/package.json b/plugins/todo/package.json index d20ff43470..5e32d84497 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.5", - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.1", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -41,17 +41,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index bc836a7251..dc63957cb9 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -43,18 +43,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@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", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index c012c8e059..bbab66e455 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-components": "^0.7.3", + "@backstage/core-plugin-api": "^0.1.13", "@backstage/errors": "^0.1.3", - "@backstage/theme": "^0.2.11", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/core-app-api": "^0.1.18", + "@backstage/cli": "^0.8.2", + "@backstage/core-app-api": "^0.1.20", "@backstage/dev-utils": "^0.2.12", - "@backstage/test-utils": "^0.1.19", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -47,7 +47,7 @@ "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts new file mode 100644 index 0000000000..f1015f8139 --- /dev/null +++ b/scripts/snyk-github-issue-sync.ts @@ -0,0 +1,200 @@ +/* + * 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. + */ +/* eslint-disable import/no-extraneous-dependencies */ +import { Octokit } from '@octokit/rest'; +import minimist from 'minimist'; +// Generated by GitHub workflow .github/workflows/snyk-github-issue-creator +import synkJsonOutput from '../snyk.json'; + +type Vulnerability = { + description: string; + packages: { + name: string; + target: string; + }[]; + snykId: string; +}; + +const argv = minimist(process.argv.slice(2)); + +const GH_OWNER = 'backstage'; +const GH_REPO = 'backstage'; +const SNYK_GH_LABEL = 'snyk-vulnerability'; +const SNYK_ID_REGEX = /\[([A-Z0-9-:]+)]/i; + +const isDryRun = 'dryrun' in argv; + +if (!process.env.GITHUB_TOKEN) { + console.error('GITHUB_TOKEN is not set. Please provide a Github token'); + process.exit(1); +} + +const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, +}); + +if (isDryRun) { + console.log( + 'โš ๏ธ Running in dryrun mode, no issues will be updated on Github โš ๏ธ', + ); +} + +const fetchSnykGithubIssueMap = async (): Promise> => { + const snykGithubIssueMap: Record = {}; + + const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, { + owner: GH_OWNER, + repo: GH_REPO, + per_page: 100, + state: 'open', + labels: SNYK_GH_LABEL, + }); + + for await (const { data: issues } of iterator) { + for (const issue of issues) { + // Gets the Vulnerability ID from square braces + const match = SNYK_ID_REGEX.exec(issue.title); + + if (match && match[1]) { + snykGithubIssueMap[match[1]] = issue.number; + } else { + console.log(`Unmatched Snyk ID for ${issue.title}`); + } + } + } + + return snykGithubIssueMap; +}; + +const generateIssueBody = (vulnerability: Vulnerability) => ` +## Affecting Packages/Plugins + +${Array.from(vulnerability.packages) + .map(({ name, target }) => `* [${name}](${target})`) + .join('\n')} + +${vulnerability.description} +`; + +const createGithubIssue = async (vulnerability: Vulnerability) => { + console.log( + `Create Github Issue for Snyk Vulnerability ${vulnerability.snykId}`, + ); + + vulnerability.packages.forEach(({ name, target }) => { + console.log(`- ${name} [${target}]`); + }); + + if (!isDryRun) { + await octokit.issues.create({ + owner: GH_OWNER, + repo: GH_REPO, + title: `Snyk vulnerability [${vulnerability.snykId}]`, + labels: [SNYK_GH_LABEL, 'help wanted'], + body: generateIssueBody(vulnerability), + }); + } +}; + +const updateGithubIssue = async ( + githubIssueId: number, + vulnerability: Vulnerability, +) => { + console.log( + `Update Github Issue #${githubIssueId} for Snky Vulnerability ${vulnerability.snykId}`, + ); + + if (!isDryRun) { + await octokit.issues.update({ + owner: GH_OWNER, + repo: GH_REPO, + issue_number: githubIssueId, + body: generateIssueBody(vulnerability), + }); + } +}; + +const closeGithubIssue = async (githubIssueId: number) => { + console.log(`Closing Github Issue #${githubIssueId}`); + + if (!isDryRun) { + await octokit.issues.update({ + owner: GH_OWNER, + repo: GH_REPO, + issue_number: githubIssueId, + state: 'closed', + }); + } +}; + +async function main() { + const snykGithubIssueMap = await fetchSnykGithubIssueMap(); + const vulnerabilityStore: Record = {}; + + // Group the Snyk vulnerabilities, and link back to the affecting packages. + synkJsonOutput.forEach( + ({ projectName, displayTargetFile, vulnerabilities }) => { + vulnerabilities.forEach( + ({ id, description }: { id: string; description: string }) => { + if (id !== undefined && description !== undefined) { + if (vulnerabilityStore[id]) { + if ( + !vulnerabilityStore[id].packages.some( + ({ name }) => name === projectName, + ) + ) { + vulnerabilityStore[id].packages.push({ + name: projectName, + target: displayTargetFile, + }); + } + } else { + vulnerabilityStore[id] = { + description, + snykId: id, + packages: [ + { + name: projectName, + target: displayTargetFile, + }, + ], + }; + } + } + }, + ); + }, + ); + + for (const [id, vulnerability] of Object.entries(vulnerabilityStore)) { + if (snykGithubIssueMap[id]) { + await updateGithubIssue(snykGithubIssueMap[id], vulnerability); + } else { + await createGithubIssue(vulnerability); + } + } + + for (const [snykId, githubIssueId] of Object.entries(snykGithubIssueMap)) { + if (!vulnerabilityStore[snykId]) { + await closeGithubIssue(githubIssueId); + } + } +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/yarn.lock b/yarn.lock index c905fbea6f..e18f6fedb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -266,13 +266,20 @@ dependencies: tslib "^2.0.0" -"@azure/msal-common@^4.0.0", "@azure/msal-common@^4.4.0": +"@azure/msal-common@^4.0.0": version "4.4.0" resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.4.0.tgz#818526042f78838ebc332fb735e7de64d8bccb45" integrity sha512-Qrs33Ctt2KM7NxArFPIUKc8UbIcm7zYxJFdJeQ9k7HKBhVk3e88CUz1Mw33cS/Jr+YA1H02OAzHg++bJ+4SFyQ== dependencies: debug "^4.1.1" +"@azure/msal-common@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-5.0.1.tgz#234030b340cc63575e84190b96a8a336b69c7698" + integrity sha512-CmPR3XM9+CGUu7V/+bAwDxyN6XqWJJhVLmv7utT3sbgay4l5roVXsD1t4wURTs8PwzxmmnJOrhvvGhoDxUW69g== + dependencies: + debug "^4.1.1" + "@azure/msal-node@1.0.0-beta.6": version "1.0.0-beta.6" resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz#da6bc3a3a861057c85586055960e069f162548ee" @@ -284,12 +291,12 @@ uuid "^8.3.0" "@azure/msal-node@^1.1.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.2.0.tgz#d08a5fb3391436715cb37c6eb44816013bdfa11e" - integrity sha512-79o5n483vslc7Qegh9+0BsxODRmlk6YYjVdl9jvwmAuF+i+oylq57e7RVhTVocKCbLCIMOKARI14JyKdDbW0WA== + version "1.3.2" + resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.3.2.tgz#17665397c04b9cad57b42eb6e21d5d6f35d9b83a" + integrity sha512-aKU2lVRKhZa1IJ/Za/Ir6qlythQ3FHz0g0px3SbM4iC1otyr3ANS4mIn/6fmkpZDIHc8eAgJh2KMep1Yn2zpig== dependencies: - "@azure/msal-common" "^4.4.0" - axios "^0.21.1" + "@azure/msal-common" "^5.0.1" + axios "^0.21.4" jsonwebtoken "^8.5.1" uuid "^8.3.0" @@ -2071,7 +2078,7 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.15.4" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -2165,7 +2172,7 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" -"@backstage/catalog-client@^0.3.18": +"@backstage/catalog-client@^0.3.16", "@backstage/catalog-client@^0.3.18": version "0.3.19" resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.3.19.tgz#109b0fde1cc2d9a306635f3775fc8f6174172b14" integrity sha512-0ydcC/1ISNgR8SggOUnMNNHtsRwrKN5GX+AXgTVdZk4qpz1MqG1+fzrNld9V7KVvXdsbGDZAl/b5a7/FJEpCqg== @@ -2175,24 +2182,6 @@ "@backstage/errors" "^0.1.2" cross-fetch "^3.0.6" -"@backstage/core-api@^0.2.23": - version "0.2.23" - resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.23.tgz#c0ec13407ff7c78d376eb18e9d8e1490d54d995b" - integrity sha512-859IGJ5LpcFaqZOenJNM9eFBKd5lrdBjYst8I0srLCaZkBCshTbUT615G3zoDMDiXZNSm+h4V82kMT4eES9wDw== - dependencies: - "@backstage/config" "^0.1.4" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/theme" "^0.2.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - 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" @@ -2371,52 +2360,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/core@*": - version "0.7.14" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.14.tgz#863844fe40bb6a29bcc2d297e42055633b0e886f" - integrity sha512-W7EMspBXrp1GPALK6+qdJjsvkqcaYGFyoh8/bRAXABIkJpGQGiy4xUZUKDoMhd+OdVHv/mzyyv3fH2yc32J07w== - dependencies: - "@backstage/config" "^0.1.5" - "@backstage/core-api" "^0.2.23" - "@backstage/errors" "^0.1.1" - "@backstage/theme" "^0.2.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@testing-library/react-hooks" "^3.4.2" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - "@types/react-text-truncate" "^0.14.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - history "^5.0.0" - immer "^9.0.1" - lodash "^4.17.15" - material-table "^1.69.1" - 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 "^6.15.4" - react-markdown "^5.0.2" - 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 "^1.0.0" - 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" @@ -2666,6 +2609,18 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@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" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -2719,7 +2674,7 @@ enabled "2.0.x" kuler "^2.0.0" -"@date-io/core@1.x", "@date-io/core@^1.1.0", "@date-io/core@^1.3.13": +"@date-io/core@1.x", "@date-io/core@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== @@ -2729,18 +2684,6 @@ resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.7.tgz#0fe1fa0ef02c827919e23c2802a4b25589ac522d" integrity sha512-EG/1qDiQvd12RoNJ6H+sZcHVswC/3uMx/ySvfaJ24vB30rLjkgHggEXbgMbfgki7wMuiQ/zXI8QlmF1k3kWRGQ== -"@date-io/core@^2.10.11": - version "2.10.11" - resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.11.tgz#b1a3d57730f3eaaab54d5658be4a71727297e357" - integrity sha512-keXQnwH0LM8wyvu+j5Z2KGK56D+eItjy7DnwuWl/oV+DM2UEYl0z5WhdPMpfswSyt/kjuPOzcVF/7u/skMLaoA== - -"@date-io/date-fns@1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.1.0.tgz#91cd7703042513a6a0056a344b25cd74a4df643e" - integrity sha512-FMRhYWfoGiIXdN4xWAArpkdEbqsg2Fr+6Yda7Np2eVWCNx6gSMYsHIM51IIcI+3762ajYbhoEYjHYXVFNZIk1g== - dependencies: - "@date-io/core" "^1.1.0" - "@date-io/date-fns@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" @@ -2748,12 +2691,12 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@2.x": - version "2.10.11" - resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.10.11.tgz#d0981b9fdf5e5f17f8ce59265a3ac6c335565fac" - integrity sha512-SS6SIkp0Y9GFwpQycCTUAyW3OZTW05CWI1DJu10hUzcg8SmjJfhjs7hQY3TOeW+JT6VtXGTVGwbWPUBJsNkhZg== +"@date-io/luxon@1.x": + version "1.3.13" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" + integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== dependencies: - "@date-io/core" "^2.10.11" + "@date-io/core" "^1.3.13" "@discoveryjs/json-ext@^0.5.3": version "0.5.5" @@ -2769,7 +2712,7 @@ find-my-way "^2.2.2" into-stream "^5.1.1" -"@elastic/elasticsearch@^7.13.0": +"@elastic/elasticsearch@7.13.0": version "7.13.0" resolved "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.13.0.tgz#6dcf511dfa91187e22c81e54f41f4bd0fd96b4d6" integrity sha512-WgwLWo2p9P2tdqzBGX9fHeG8p5IOTXprXNTECQG2mJ7z9n93N5AFBJpEw4d35tWWeCWi9jI13A2wzQZH7XZ/xw== @@ -3025,16 +2968,14 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" -"@graphiql/toolkit@^0.2.0": - version "0.2.2" - resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.2.2.tgz#193d570afcf686c9ee61c92054c1782b9f3c1255" - integrity sha512-kDgYhqnS4p4LqSo1KvLd3tbX8Hhdj0ZrgQuGsosjjEnahiPYmmylxUL1p9lj6348OsypcTlCncGpEjeb9S3TiQ== +"@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== dependencies: - "@n1ru4l/push-pull-async-iterable-iterator" "^2.1.4" - graphql-ws "^4.3.2" + "@n1ru4l/push-pull-async-iterable-iterator" "^3.0.0" + graphql-ws "^4.9.0" meros "^1.1.4" - optionalDependencies: - subscriptions-transport-ws "^0.9.18" "@graphql-codegen/cli@^1.21.3": version "1.21.6" @@ -4504,18 +4445,6 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@material-ui/pickers@3.2.2": - version "3.2.2" - resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.2.tgz#9b7a67f5a8f21f8183853ebc1848e5d8dd680d4c" - integrity sha512-on/J1yyKeJ4CkLnItpf/jPDKMZVWvHDklkh5FS7wkZ0s1OPoqTsPubLWfA7eND6xREnVRyLFzVTlE3VlWYdQWw== - dependencies: - "@babel/runtime" "^7.2.0" - "@types/styled-jsx" "^2.2.8" - clsx "^1.0.2" - react-transition-group "^4.0.0" - rifm "^0.7.0" - tslib "^1.9.3" - "@material-ui/pickers@^3.2.10", "@material-ui/pickers@^3.3.10": version "3.3.10" resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485" @@ -4611,19 +4540,28 @@ resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@microsoft/api-documenter@^7.13.47": - version "7.13.47" - resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.47.tgz#0b7726634232b37f76c0e5e8353cdbb5b52d4ece" - integrity sha512-jk78Pf8cKL2WZf6CkKUUtwegdsTA1Jf0MfIzD50qpG7T257HLrqCi1t70ZA85VpRLR8oSeNHMayqNTWkdku9iA== +"@microsoft/api-documenter@^7.13.68": + version "7.13.68" + resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.68.tgz#c1e144764cac0684adefe78fd848d78c3f374681" + integrity sha512-cRjwK1TDyGxFGgCsRG8G0Yi3Z4akvfWgw1pWAxKFbm7ajlQQGZcHPnb+n4lKlSeQ5g/cxc7hcdw54Mvisne9Bg== dependencies: - "@microsoft/api-extractor-model" "7.13.5" + "@microsoft/api-extractor-model" "7.13.16" "@microsoft/tsdoc" "0.13.2" - "@rushstack/node-core-library" "3.40.0" - "@rushstack/ts-command-line" "4.9.0" + "@rushstack/node-core-library" "3.43.2" + "@rushstack/ts-command-line" "4.10.4" colors "~1.2.1" js-yaml "~3.13.1" resolve "~1.17.0" +"@microsoft/api-extractor-model@7.13.16": + version "7.13.16" + resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.16.tgz#1d67541ebbcea32672c5fdd9392dc1579b2fc23a" + integrity sha512-ttdxVXsTWL5dd26W1YNLe3LgDsE0EE273aZlcLe58W0opymBybCYU1Mn+OHQM8BuErrdvdN8LdpWAAbkiOEN/Q== + dependencies: + "@microsoft/tsdoc" "0.13.2" + "@microsoft/tsdoc-config" "~0.15.2" + "@rushstack/node-core-library" "3.43.2" + "@microsoft/api-extractor-model@7.13.5", "@microsoft/api-extractor-model@^7.13.5": version "7.13.5" resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.5.tgz#7836a81ba47b9a654062ed0361e4eee69afae51e" @@ -4657,9 +4595,9 @@ integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== "@microsoft/microsoft-graph-types@^2.6.0": - version "2.6.0" - resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.6.0.tgz#d1f97049362ea5c0ed79199875f24707f9832b29" - integrity sha512-wK26kqUdQJLpgPh2Fk5jcRuFtnGLjNrXCWKM78zXnvAg+On246WYLz6+7amILJDC5rpaZTGXf4eynXoJFuNBTw== + version "2.8.0" + resolved "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.8.0.tgz#c3b538f99028e8609c5ebf95a494318a8f3d9201" + integrity sha512-NDgLn9IhYD/+nCeeGAi1JM7xTFqaM6rkXfLfiC1xvXy48BGBUrAf8fNFq5fkzBvGY8HfjzdPIkrJkfvLL+rzDQ== "@microsoft/tsdoc-config@~0.15.2": version "0.15.2" @@ -4684,7 +4622,7 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@mswjs/cookies@^0.1.5": +"@mswjs/cookies@^0.1.6": version "0.1.6" resolved "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" integrity sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA== @@ -4692,21 +4630,22 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" -"@mswjs/interceptors@^0.10.0": - version "0.10.0" - resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.10.0.tgz#f5aad03c2c0591d164e3ed178b21942f1c2f8061" - integrity sha512-/M0GGpid5q2EDI+Keas1sLYF3VZFXHDE5gCmX/jHdp+OJFruVNca3PUk7A8KnGdPpuycZogdPsmRBSOXwjyA7A== +"@mswjs/interceptors@^0.12.6": + version "0.12.7" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" + integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== dependencies: "@open-draft/until" "^1.0.3" - debug "^4.3.0" + "@xmldom/xmldom" "^0.7.2" + debug "^4.3.2" headers-utils "^3.0.2" + outvariant "^1.2.0" strict-event-emitter "^0.2.0" - xmldom "^0.6.0" -"@n1ru4l/push-pull-async-iterable-iterator@^2.1.4": - version "2.1.4" - resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.4.tgz#a90225474352f9f159bff979905f707b9c6bcf04" - integrity sha512-qLIvoOUJ+zritv+BlzcBMePKNjKQzH9Rb2i9W98YXxf/M62Lye8qH0peyiU8yJ1tL0kfulWi31BoK10E6BKJeA== +"@n1ru4l/push-pull-async-iterable-iterator@^3.0.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== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -4784,22 +4723,7 @@ "@gar/promisify" "^1.0.1" semver "^7.3.5" -"@npmcli/git@^2.0.1": - version "2.0.6" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2" - integrity sha512-a1MnTfeRPBaKbFY07fd+6HugY1WAkKJzdiJvlRub/9o5xz2F/JtPacZZapx5zRJUQFIzSL677vmTSxEcDMrDbg== - dependencies: - "@npmcli/promise-spawn" "^1.1.0" - lru-cache "^6.0.0" - mkdirp "^1.0.3" - npm-pick-manifest "^6.0.0" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.2" - unique-filename "^1.1.1" - which "^2.0.2" - -"@npmcli/git@^2.1.0": +"@npmcli/git@^2.0.1", "@npmcli/git@^2.1.0": version "2.1.0" resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== @@ -4872,7 +4796,7 @@ dependencies: json-parse-even-better-errors "^2.3.1" -"@npmcli/promise-spawn@^1.1.0", "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== @@ -4961,6 +4885,19 @@ before-after-hook "^2.1.0" universal-user-agent "^6.0.0" +"@octokit/core@^3.5.1": + version "3.5.1" + resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^6.0.1": version "6.0.3" resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" @@ -4995,6 +4932,11 @@ "@octokit/types" "^6.12.2" btoa-lite "^1.0.0" +"@octokit/openapi-types@^11.2.0": + version "11.2.0" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== + "@octokit/openapi-types@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.3.2.tgz#065ce49b338043ec7f741316ce06afd4d459d944" @@ -5005,6 +4947,13 @@ resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== +"@octokit/plugin-paginate-rest@^2.16.8": + version "2.17.0" + resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== + dependencies: + "@octokit/types" "^6.34.0" + "@octokit/plugin-paginate-rest@^2.6.2": version "2.7.0" resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3" @@ -5017,6 +4966,11 @@ resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + "@octokit/plugin-rest-endpoint-methods@5.3.1": version "5.3.1" resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.1.tgz#deddce769b4ec3179170709ab42e4e9e6195aaa9" @@ -5025,6 +4979,14 @@ "@octokit/types" "^6.16.2" deprecation "^2.3.1" +"@octokit/plugin-rest-endpoint-methods@^5.12.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== + dependencies: + "@octokit/types" "^6.34.0" + deprecation "^2.3.1" + "@octokit/request-error@^2.0.0", "@octokit/request-error@^2.0.2", "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" @@ -5056,6 +5018,16 @@ "@octokit/plugin-request-log" "^1.0.2" "@octokit/plugin-rest-endpoint-methods" "5.3.1" +"@octokit/rest@^18.12.0": + version "18.12.0" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881" + integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== + dependencies: + "@octokit/core" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.8" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^5.12.0" + "@octokit/types@^5.0.0", "@octokit/types@^5.0.1": version "5.5.0" resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" @@ -5070,6 +5042,13 @@ dependencies: "@octokit/openapi-types" "^7.3.2" +"@octokit/types@^6.34.0": + version "6.34.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== + dependencies: + "@octokit/openapi-types" "^11.2.0" + "@octokit/webhooks-methods@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz#1108b9ea661ca6c81e4a8bfa63a09eb27d5bc2db" @@ -5211,9 +5190,9 @@ react-lifecycles-compat "^3.0.4" "@rjsf/core@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.1.0.tgz#7667364a4532e25c164abbc2adabe39157ffec4c" - integrity sha512-EwM2juiQxEdXzFy9rIIsDr6/e+FYeR1cKx0/no8ASEuXZ1p+/nf/CxKGobzD6UhpRip7JK9PhhuHFEFBIjHFJA== + version "3.2.0" + resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.2.0.tgz#189ff7fb132cb223463792d75fb1e04e9dd5d9f9" + integrity sha512-Cgonq9bBWlpE0yeeIv7rAIDuJvO1wAvp9G7tT/UvhODAyb4tDb1q8J/8ubWuRRiEyzYUkuxN0W8uPIzQsJgtTQ== dependencies: "@types/json-schema" "^7.0.7" ajv "^6.7.0" @@ -5254,15 +5233,16 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-github-insights@^1.1.23": - version "1.1.23" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.23.tgz#bcf7424df5a659df39d5aa75f5fffd8e02c9437e" - integrity sha512-3HL9nEtlaE+gFmV0xkrtZGbWHbS26z+NHzEk2KIyV8oqReQ+57PRLTVcXCjFA+djkKfP2hpu5s7zp72aVb0v+w== + version "1.2.2" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.2.2.tgz#09d958ed15adbb598afda34187f45deedec2264d" + integrity sha512-ybomCU/3NIQJYahp/E78wl+JuzpwkRkhyeCcySLA45GaCz/mEqAGSs4xDZancpV2ls6nxV3mKUw/DL9afmMGAw== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.3.0" + "@backstage/core-components" "^0.7.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.4.0" + "@backstage/integration-react" "^0.1.10" + "@backstage/plugin-catalog-react" "^0.6.0" "@backstage/theme" "^0.2.7" "@date-io/core" "2.10.7" "@material-ui/core" "^4.11.0" @@ -5396,6 +5376,21 @@ timsort "~0.3.0" z-schema "~3.18.3" +"@rushstack/node-core-library@3.43.2": + version "3.43.2" + resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.43.2.tgz#f067371a94fd92ed8f9d9aa8201c5e9e17a19f0f" + integrity sha512-b7AEhSf6CvZgvuDcWMFDeKx2mQSn9AVnMQVyxNxFeHCtLz3gJicqCOlw2GOXM8HKh6PInLdil/NVCDcstwSrIw== + dependencies: + "@types/node" "12.20.24" + colors "~1.2.1" + fs-extra "~7.0.1" + import-lazy "~4.0.0" + jju "~1.4.0" + resolve "~1.17.0" + semver "~7.3.0" + timsort "~0.3.0" + z-schema "~3.18.3" + "@rushstack/rig-package@0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.0.tgz#334ad2846797861361b3445d4cc9ae9164b1885c" @@ -5404,6 +5399,16 @@ resolve "~1.17.0" strip-json-comments "~3.1.1" +"@rushstack/ts-command-line@4.10.4": + version "4.10.4" + resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.4.tgz#05142b74e5cb207d3dd9b935c82f80d7fcb68042" + integrity sha512-4T5ao4UgDb6LmiRj4GumvG3VT/p6RSMgl7TN7S58ifaAGN2GeTNBajFCDdJs9QQP0d/4tA5p0SFzT7Ps5Byirg== + dependencies: + "@types/argparse" "1.0.38" + argparse "~1.0.9" + colors "~1.2.1" + string-argv "~0.3.1" + "@rushstack/ts-command-line@4.9.0": version "4.9.0" resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.9.0.tgz#781ba42cff73cae097b6d5241b6441e7cc2fe6e0" @@ -5490,20 +5495,20 @@ resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@spotify/eslint-config-base@^9.0.0": - version "9.0.2" - resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-9.0.2.tgz#a4830f610f40de935de795d3def486c3e5064ee4" - integrity sha512-HS+eLpVMbUUBXZ9ZlPcd6yCje/OVNhdcGDeU+DoK+C9AFfzbRtuZ1EJ8ewzj58ENrhlfJPuxCS68bypA/esyjw== +"@spotify/eslint-config-base@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-12.0.0.tgz#0b1e41bb436d5c1c20714703629514d64c3c0f06" + integrity sha512-5Uud/TmzakqmdUNCZpD8JFQRa2VG3dVd3DanSMpU/nVdu6K5LyX8EMU3Tz1vGP18Wih8iAu/sBSJhntNzw7e6w== -"@spotify/eslint-config-react@^10.0.0": - version "10.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-10.0.0.tgz#6f83ada05f79b49c1f9def5b8815e3231ed24969" - integrity sha512-MozX6W3aMp7EQPliuUQYI58Ni5vh65mItXMG0CgZBj0v1ZEeZVM5XS/nqhsCaIHYXskmZM2O1qqLFaEg5PqGdg== +"@spotify/eslint-config-react@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-12.0.0.tgz#5b8d4bc3b81a8ec2824648f482f1f6c3cf711893" + integrity sha512-lNHZRtJesNA273OJHBVUGAg2JYyVDZ+bsT7h3OwnX1HYgejJ3YcKPSziPM8TGFAN8DruH3tHFfaM63uAIA1+uw== -"@spotify/eslint-config-typescript@^10.0.0": - version "10.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-10.0.0.tgz#4df7074f3f4ef31d76c617e55d335f9a36cfed5b" - integrity sha512-qR4WOU3gJrpz26O8BlNbXas4Yj93NeVH7yvULVYO2j9bCAEZJu2sfl1BGfOy4qAsYGutZhJtNwMqK0Rl4DcFHQ== +"@spotify/eslint-config-typescript@^12.0.0": + version "12.0.0" + 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" @@ -5511,18 +5516,18 @@ integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ== "@storybook/addon-a11y@^6.3.4": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.7.tgz#a802455f2d932eda07314e3d44a96c94bbd22b3d" - integrity sha512-Z5Lhxm8r5CkPW9FYf6zmAk9c7IhUeUQZxKZeEWGZdOvcjQ32rtg4IYvO2SHgWNrEKBdxxFm3pMiyK3wylQLfsQ== + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.12.tgz#2f930fc84fc275a4ed43a716fc09cc12caf4e110" + integrity sha512-q1NdRHFJV6sLEEJw0hatCc5ZIthELqM/AWdrEWDyhcJNyiq7Tq4nKqQBMTQSYwHiUAmxVgw7i4oa1vM2M51/3g== dependencies: - "@storybook/addons" "6.3.7" - "@storybook/api" "6.3.7" - "@storybook/channels" "6.3.7" - "@storybook/client-api" "6.3.7" - "@storybook/client-logger" "6.3.7" - "@storybook/components" "6.3.7" - "@storybook/core-events" "6.3.7" - "@storybook/theming" "6.3.7" + "@storybook/addons" "6.3.12" + "@storybook/api" "6.3.12" + "@storybook/channels" "6.3.12" + "@storybook/client-api" "6.3.12" + "@storybook/client-logger" "6.3.12" + "@storybook/components" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/theming" "6.3.12" axe-core "^4.2.0" core-js "^3.8.2" global "^4.4.0" @@ -5593,7 +5598,7 @@ react-syntax-highlighter "^13.5.3" regenerator-runtime "^0.13.7" -"@storybook/addons@6.3.11", "@storybook/addons@^6.1.11": +"@storybook/addons@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.11.tgz#4b5e88793bcce7ef823340e9010a96e35e3284cc" integrity sha512-2Y03lOwzWDRB/glISa/4luBMM5uyYhkIBixbZF9miIb2SCWRlNmom5NCnKsR18Wu6g7zI7os3aAMfKr24aSofQ== @@ -5608,6 +5613,21 @@ global "^4.4.0" regenerator-runtime "^0.13.7" +"@storybook/addons@6.3.12", "@storybook/addons@^6.1.11": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz#8773dcc113c5086dfff722388b7b65580e43b65b" + integrity sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg== + dependencies: + "@storybook/api" "6.3.12" + "@storybook/channels" "6.3.12" + "@storybook/client-logger" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/router" "6.3.12" + "@storybook/theming" "6.3.12" + core-js "^3.8.2" + global "^4.4.0" + regenerator-runtime "^0.13.7" + "@storybook/addons@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.7.tgz#7c6b8d11b65f67b1884f6140437fe996dc39537a" @@ -5649,6 +5669,32 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.12.tgz#2845c20464d5348d676d09665e8ab527825ed7b5" + integrity sha512-LScRXUeCWEW/OP+jiooNMQICVdusv7azTmULxtm72fhkXFRiQs2CdRNTiqNg46JLLC9z95f1W+pGK66X6HiiQA== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/channels" "6.3.12" + "@storybook/client-logger" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.3.12" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.3.12" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + store2 "^2.12.0" + telejson "^5.3.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/api@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.7.tgz#88b8a51422cd0739c91bde0b1d65fb6d8a8485d0" @@ -5764,6 +5810,19 @@ qs "^6.10.0" telejson "^5.3.2" +"@storybook/channel-postmessage@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.12.tgz#3ff9412ac0f445e3b8b44dd414e783a5a47ff7c1" + integrity sha512-Ou/2Ga3JRTZ/4sSv7ikMgUgLTeZMsXXWLXuscz4oaYhmOqAU9CrJw0G1NitwBgK/+qC83lEFSLujHkWcoQDOKg== + dependencies: + "@storybook/channels" "6.3.12" + "@storybook/client-logger" "6.3.12" + "@storybook/core-events" "6.3.12" + core-js "^3.8.2" + global "^4.4.0" + qs "^6.10.0" + telejson "^5.3.2" + "@storybook/channel-postmessage@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.7.tgz#bd4edf84a29aa2cd4a22d26115c60194d289a840" @@ -5786,6 +5845,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.12.tgz#aa0d793895a8b211f0ad3459c61c1bcafd0093c7" + integrity sha512-l4sA+g1PdUV8YCbgs47fIKREdEQAKNdQIZw0b7BfTvY9t0x5yfBywgQhYON/lIeiNGz2OlIuD+VUtqYfCtNSyw== + dependencies: + core-js "^3.8.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/channels@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.7.tgz#85ed5925522b802d959810f78d37aacde7fea66e" @@ -5819,6 +5887,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.12.tgz#a0c6d72a871d1cb02b4b98675472839061e39b5b" + integrity sha512-xnW+lKKK2T774z+rOr9Wopt1aYTStfb86PSs9p3Fpnc2Btcftln+C3NtiHZl8Ccqft8Mz/chLGgewRui6tNI8g== + dependencies: + "@storybook/addons" "6.3.12" + "@storybook/channel-postmessage" "6.3.12" + "@storybook/channels" "6.3.12" + "@storybook/client-logger" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/csf" "0.0.1" + "@types/qs" "^6.9.5" + "@types/webpack-env" "^1.16.0" + core-js "^3.8.2" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + regenerator-runtime "^0.13.7" + stable "^0.1.8" + store2 "^2.12.0" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-api@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.7.tgz#cb1dca05467d777bd09aadbbdd1dd22ca537ce14" @@ -5851,6 +5943,14 @@ core-js "^3.8.2" global "^4.4.0" +"@storybook/client-logger@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.12.tgz#6585c98923b49fcb25dbceeeb96ef2a83e28e0f4" + integrity sha512-zNDsamZvHnuqLznDdP9dUeGgQ9TyFh4ray3t1VGO7ZqWVZ2xtVCCXjDvMnOXI2ifMpX5UsrOvshIPeE9fMBmiQ== + dependencies: + core-js "^3.8.2" + global "^4.4.0" + "@storybook/client-logger@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.7.tgz#ff17b7494e7e9e23089b0d5c5364c371c726bdd1" @@ -5889,6 +5989,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/components@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.12.tgz#0c7967c60354c84afa20dfab4753105e49b1927d" + integrity sha512-kdQt8toUjynYAxDLrJzuG7YSNL6as1wJoyzNUaCfG06YPhvIAlKo7le9tS2mThVFN5e9nbKrW3N1V1sp6ypZXQ== + dependencies: + "@popperjs/core" "^2.6.0" + "@storybook/client-logger" "6.3.12" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.3.12" + "@types/color-convert" "^2.0.0" + "@types/overlayscrollbars" "^1.12.0" + "@types/react-syntax-highlighter" "11.0.5" + color-convert "^2.0.1" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + markdown-to-jsx "^7.1.3" + memoizerific "^1.11.3" + overlayscrollbars "^1.13.1" + polished "^4.0.5" + prop-types "^15.7.2" + react-colorful "^5.1.2" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.3" + react-textarea-autosize "^8.3.0" + regenerator-runtime "^0.13.7" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/components@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.7.tgz#42b1ca6d24e388e02eab82aa9ed3365db2266ecc" @@ -6003,6 +6133,13 @@ dependencies: core-js "^3.8.2" +"@storybook/core-events@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.12.tgz#73f6271d485ef2576234e578bb07705b92805290" + integrity sha512-SXfD7xUUMazaeFkB92qOTUV8Y/RghE4SkEYe5slAdjeocSaH7Nz2WV0rqNEgChg0AQc+JUI66no8L9g0+lw4Gw== + dependencies: + core-js "^3.8.2" + "@storybook/core-events@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.7.tgz#c5bc7cae7dc295de73b6b9f671ecbe582582e9bd" @@ -6197,6 +6334,22 @@ qs "^6.10.0" ts-dedent "^2.0.0" +"@storybook/router@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.12.tgz#0d572ec795f588ca886f39cb9b27b94ff3683f84" + integrity sha512-G/pNGCnrJRetCwyEZulHPT+YOcqEj/vkPVDTUfii2qgqukup6K0cjwgd7IukAURnAnnzTi1gmgFuEKUi8GE/KA== + dependencies: + "@reach/router" "^1.3.4" + "@storybook/client-logger" "6.3.12" + "@types/reach__router" "^1.3.7" + core-js "^3.8.2" + fast-deep-equal "^3.1.3" + global "^4.4.0" + lodash "^4.17.20" + memoizerific "^1.11.3" + qs "^6.10.0" + ts-dedent "^2.0.0" + "@storybook/router@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.7.tgz#1714a99a58a7b9f08b6fcfe2b678dad6ca896736" @@ -6255,6 +6408,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.3.12": + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.12.tgz#5bddf9bd90a60709b5ab238ecdb7d9055dd7862e" + integrity sha512-wOJdTEa/VFyFB2UyoqyYGaZdym6EN7RALuQOAMT6zHA282FBmKw8nL5DETHEbctpnHdcrMC/391teK4nNSrdOA== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.27" + "@storybook/client-logger" "6.3.12" + core-js "^3.8.2" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.27" + global "^4.4.0" + memoizerific "^1.11.3" + polished "^4.0.5" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/theming@6.3.7": version "6.3.7" resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.7.tgz#6daf9a21b26ed607f3c28a82acd90c0248e76d8b" @@ -6573,6 +6744,11 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1" integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA== +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/accepts@*", "@types/accepts@^1.3.5": version "1.3.5" resolved "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" @@ -6755,10 +6931,10 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== "@types/cookiejar@*": version "2.1.1" @@ -6798,9 +6974,9 @@ postcss "5 - 7" "@types/d3-color@*": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.0.tgz#febdfadade56e215a4c3f612fe3000d92999f5d5" - integrity sha512-Bs0maTeU47rdZT+n42iQ0C4gnbnJlIDJkqHFtIsDx2tPPITDeoSdIrm+00UYXzegzArYC2GsG80eHNMwz08IAw== + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" + integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== "@types/d3-force@^2.1.1": version "2.1.1" @@ -6808,9 +6984,9 @@ integrity sha512-3r+CQv2K/uDTAVg0DGxsbBjV02vgOxb8RhPIv3gd6cp3pdPAZ7wEXpDjUZSoqycAQLSDOxG/AZ54Vx6YXZSbmQ== "@types/d3-interpolate@*": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.0.tgz#325029216dc722c1c68c33ccda759f1209d35823" - integrity sha512-Wt1v2zTlEN8dSx8hhx6MoOhWQgTkz0Ukj7owAEIOF2QtI0e219paFX9rf/SLOr/UExWb1TcUzatU8zWwFby6gg== + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== dependencies: "@types/d3-color" "*" @@ -6824,10 +7000,10 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== -"@types/d3-selection@*", "@types/d3-selection@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.0.tgz#59df94a8e47ed1050a337d4ffb4d4d213aa590a8" - integrity sha512-EF0lWZ4tg7oDFg4YQFlbOU3936e3a9UmoQ2IXlBy1+cv2c2Pv7knhKUzGlH5Hq2sF/KeDTH1amiRPey2rrLMQA== +"@types/d3-selection@*", "@types/d3-selection@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz#e57b01ab69b18b380f68db97b76ceefe62f17191" + integrity sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA== "@types/d3-shape@^1": version "1.3.5" @@ -6843,10 +7019,10 @@ dependencies: "@types/d3-path" "*" -"@types/d3-zoom@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.1.tgz#6f0d993042124947314053c937784e24d8b003cf" - integrity sha512-FuiGLfaHmp84b9wsj0dG03E/aJl5k98OLnJ2/5p7bQOHEpWqR+z5WCoWYMAbdGxaca7VNd9tCT5i6AJnpauNTQ== +"@types/d3-zoom@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" + integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== dependencies: "@types/d3-interpolate" "*" "@types/d3-selection" "*" @@ -6877,9 +7053,9 @@ "@types/ssh2" "*" "@types/dockerode@^3.2.1": - version "3.2.7" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.7.tgz#0bc1f583b0e1edc4ca0c6179ef251382df35dcb3" - integrity sha512-Y8hMRQTwsOjz4qm6yilZKKjB/Y7+2EOiY3RPN1Xtu63wEUEDVv+3Ou+sgiisPE9+pVe3bmwhnF+E1Iwj/o4J6w== + version "3.3.0" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz#d6afcc1de31e211ee900da3a48ef4f1a496cf05a" + integrity sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg== dependencies: "@types/docker-modem" "*" "@types/node" "*" @@ -6907,21 +7083,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "0.0.44" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" - integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== +"@types/estree@*", "@types/estree@^0.0.50": + version "0.0.50" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== - "@types/expect@^1.20.4": version "1.20.4" resolved "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" @@ -7088,6 +7259,14 @@ "@types/through" "*" rxjs "^6.4.0" +"@types/inquirer@^7.3.3": + version "7.3.3" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" + integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -7143,9 +7322,9 @@ pretty-format "^26.0.0" "@types/jquery@^3.3.34": - version "3.5.5" - resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903" - integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w== + version "3.5.8" + resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz#83bfbcdf4e625c5471590f92703c06aadb052a09" + integrity sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw== dependencies: "@types/sizzle" "*" @@ -7184,7 +7363,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@*", "@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== @@ -7282,6 +7461,11 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.4.tgz#f7b5a86ccd843c0ccaddfaedd9ee1081bc1cde3b" integrity sha512-l3xuhmyF2kBldy15SeY6d6HbK2BacEcSK1qTF1ISPtPHr29JH0C1fndz9ExXLKpGl0J6pZi+dGp1i5xesMt60Q== +"@types/luxon@^2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.5.tgz#29d3b095d55ee50df8f4cf109b16009334d9828e" + integrity sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw== + "@types/markdown-to-jsx@^6.11.3": version "6.11.3" resolved "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -7324,9 +7508,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/minimist@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" - integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + version "1.2.2" + resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/minipass@*": version "2.2.0" @@ -7354,6 +7538,18 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-cron@^2.0.4": + version "2.0.5" + resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-2.0.5.tgz#e244709a86d32453c5a702ced35b53db683fbc8e" + integrity sha512-rQ4kduTmgW11tbtx0/RsoybYHHPu4Vxw5v5ZS5qUKNerlEAI8r8P1F5UUZ2o2HTvzG759sbFxuRuqWxU8zc+EQ== + dependencies: + "@types/tz-offset" "*" + +"@types/node-cron@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.0.tgz#f946cefb5c05c64f460090f6be97bd50460c8898" + integrity sha512-RNBIyVwa/1v2r8/SqK8tadH2sJlFRAo5Ghac/cOcCv4Kp94m0I03UmAh9WVhCqS9ZdB84dF3x47p9aTw8E4c4A== + "@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.7": version "2.5.8" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" @@ -7362,7 +7558,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": version "14.17.8" resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== @@ -7372,6 +7568,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@12.20.24": + version "12.20.24" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" + integrity sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ== + "@types/node@^12.7.1": version "12.12.58" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" @@ -7382,6 +7583,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== +"@types/node@^16.9.2": + version "16.11.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -7393,9 +7599,9 @@ integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== "@types/nunjucks@^3.1.4": - version "3.1.5" - resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.1.5.tgz#e0782333b75b4ff9ac9213aa160ca8c5132eaf57" - integrity sha512-0zEdmQNNvQ+xyV9kqQvAV93UVroTwhE78toVUDT0GBnGcW2jQBZnB4al9qq2LqI5qHOqROy/DvvAY/UwrbvV1A== + version "3.2.0" + resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.0.tgz#2303629c91cdd39de103af979046d4a8590351e4" + integrity sha512-1FM36Hm3EdidJmWlZZafkg/kZME0UZ/0vQ46JE8R7R0JqQafah0r+d4i6d/MJg5DnKxEeAOAeifEVkzo7fEvGg== "@types/oauth@*": version "0.9.1" @@ -7496,6 +7702,13 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/puppeteer@^5.4.4": + version "5.4.4" + resolved "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0" + integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q== + dependencies: + "@types/node" "*" + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -7506,11 +7719,6 @@ resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== -"@types/raf@^3.4.0": - version "3.4.0" - resolved "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" - integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== - "@types/range-parser@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" @@ -7604,7 +7812,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.9": +"@types/react@*", "@types/react@>=16.9.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -7689,6 +7897,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" integrity sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA== +"@types/semver@^7.3.8": + version "7.3.8" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59" + integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now== + "@types/serve-static@*": version "1.13.9" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" @@ -7860,6 +8073,11 @@ dependencies: "@types/node" "*" +"@types/tz-offset@*": + version "0.0.0" + resolved "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.0.tgz#d58f1cebd794148d245420f8f0660305d320e565" + integrity sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" @@ -8000,6 +8218,13 @@ resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== + dependencies: + "@types/node" "*" + "@types/yup@^0.29.13": version "0.29.13" resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz#21b137ba60841307a3c8a1050d3bf4e63ad561e9" @@ -8386,7 +8611,7 @@ dependencies: tslib "^1.9.3" -"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5": +"@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.2", "@xmldom/xmldom@^0.7.5": version "0.7.5" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== @@ -8424,7 +8649,7 @@ a-sync-waterfall@^1.0.0: resolved "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== -abab@^2.0.0, abab@^2.0.3: +abab@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== @@ -8454,14 +8679,6 @@ accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-globals@^4.1.0: - version "4.3.4" - resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -8480,22 +8697,17 @@ acorn-jsx@^5.3.1: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-walk@^6.0.1: - version "6.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" - integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== - acorn-walk@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== -acorn@^5.5.3: - version "5.7.4" - resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^6.0.1, acorn@^6.4.1: +acorn@^6.4.1: version "6.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== @@ -8670,6 +8882,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1: dependencies: type-fest "^0.11.0" +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + ansi-html@0.0.7, ansi-html@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -8818,10 +9035,10 @@ apollo-server-caching@^0.7.0: dependencies: lru-cache "^6.0.0" -apollo-server-core@^2.21.0, apollo-server-core@^2.25.2: - version "2.25.2" - resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.25.2.tgz#ff65da5e512d9b5ca54c8e5e8c78ee28b5987247" - integrity sha512-lrohEjde2TmmDTO7FlOs8x5QQbAS0Sd3/t0TaK2TWaodfzi92QAvIsq321Mol6p6oEqmjm8POIDHW1EuJd7XMA== +apollo-server-core@^2.25.3: + version "2.25.3" + resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.25.3.tgz#1a649fd14b3928f5b6e65f0002b380fcfde56862" + integrity sha512-Midow3uZoJ9TjFNeCNSiWElTVZlvmB7G7tG6PPoxIR9Px90/v16Q6EzunDIO0rTJHRC3+yCwZkwtf8w2AcP0sA== dependencies: "@apollographql/apollo-tools" "^0.5.0" "@apollographql/graphql-playground-html" "1.6.27" @@ -8862,10 +9079,10 @@ apollo-server-errors@^2.5.0: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz#5d1024117c7496a2979e3e34908b5685fe112b68" integrity sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA== -apollo-server-express@^2.16.1, apollo-server-express@^2.21.0: - version "2.25.2" - resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.25.2.tgz#58cd819694ff4c2dec6945a95c5dff6aa2719ef6" - integrity sha512-A2gF2e85vvDugPlajbhr0A14cDFDIGX0mteNOJ8P3Z3cIM0D4hwrWxJidI+SzobefDIyIHu1dynFedJVhV0euQ== +apollo-server-express@^2.16.1, apollo-server-express@^2.25.3: + version "2.25.3" + resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.25.3.tgz#33fe0dae27fa71c8710e714efd93451bf2eb105f" + integrity sha512-tTFYn0oKH2qqLwVj7Ez2+MiKleXACODiGh5IxsB7VuYCPMAi9Yl8iUSlwTjQUvgCWfReZjnf0vFL2k5YhDlrtQ== dependencies: "@apollographql/graphql-playground-html" "1.6.27" "@types/accepts" "^1.3.5" @@ -8874,7 +9091,7 @@ apollo-server-express@^2.16.1, apollo-server-express@^2.21.0: "@types/express" "^4.17.12" "@types/express-serve-static-core" "^4.17.21" accepts "^1.3.5" - apollo-server-core "^2.25.2" + apollo-server-core "^2.25.3" apollo-server-types "^0.9.0" body-parser "^1.18.3" cors "^2.8.5" @@ -8902,12 +9119,12 @@ apollo-server-types@^0.9.0: apollo-server-env "^3.1.0" apollo-server@^2.16.1: - version "2.21.0" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.21.0.tgz#4e62131885b4a8a26bb8b5e77177bd0d4d210852" - integrity sha512-OqngjOSB0MEH6VKGWHcrqt4y39HlhYh9CrMvn4PhadTt53IPYRmBglk5qSRA8xMorGqy60iKrOReqj5YfCjTOg== + version "2.25.3" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.25.3.tgz#2e5db9ce5217389625ac5014551dcbdeeedcd1d8" + integrity sha512-+eUY2//DLkU7RkJLn6CTl1P89/ZMHuUQnWqv8La2iJ2hLT7Me+nMx+hgHl3LqlT/qDstQ8qA45T85FuCayplmQ== dependencies: - apollo-server-core "^2.21.0" - apollo-server-express "^2.21.0" + apollo-server-core "^2.25.3" + apollo-server-express "^2.25.3" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.8" @@ -9061,11 +9278,6 @@ array-differ@^3.0.0: resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - array-filter@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" @@ -9228,11 +9440,6 @@ async-each@^1.0.1: resolved "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async-lock@^1.1.0: version "1.2.4" resolved "https://registry.npmjs.org/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91" @@ -9400,7 +9607,7 @@ axios-cached-dns-resolve@0.5.2: pino "^5.12.2" pino-pretty "^2.6.0" -axios@^0.21.1: +axios@^0.21.1, axios@^0.21.4: version "0.21.4" resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -9663,7 +9870,7 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@6.26.0, babel-runtime@^6.26.0: +babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -9711,16 +9918,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-arraybuffer@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" - integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== - -base64-arraybuffer@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c" - integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA== - base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -9778,6 +9975,11 @@ before-after-hook@^2.1.0: resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + better-opn@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz#94a55b4695dc79288f31d7d0e5f658320759f7c6" @@ -9890,7 +10092,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -10159,7 +10361,7 @@ buffer@4.9.2, buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.5.0, buffer@^5.7.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.7.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -10480,20 +10682,6 @@ canvas@^2.6.1: nan "^2.14.0" simple-get "^3.0.3" -canvg@^3.0.6: - version "3.0.9" - resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.9.tgz#9ba095f158b94b97ca2c9c1c40785b11dc08df6d" - integrity sha512-rDXcnRPuz4QHoCilMeoTxql+fvGqNAxp+qV/KHD8rOiJSAfVjFclbdUNHD2Uqfthr+VMg17bD2bVuk6F07oLGw== - dependencies: - "@babel/runtime" "^7.12.5" - "@types/raf" "^3.4.0" - core-js "^3.8.3" - raf "^3.4.1" - regenerator-runtime "^0.13.7" - rgbcolor "^1.0.1" - stackblur-canvas "^2.0.0" - svg-pathdata "^6.0.3" - capital-case@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" @@ -10760,11 +10948,6 @@ classnames@*, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1: resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== -classnames@2.2.6: - version "2.2.6" - resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== - clean-css@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -10920,7 +11103,7 @@ clone-stats@^1.0.0: resolved "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= -clone@2.x, clone@^2.1.1: +clone@2.x, clone@^2.1.1, clone@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= @@ -10975,18 +11158,18 @@ 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.0: - version "1.0.0" - resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.0.0.tgz#ba8db60dc42b87768d643b3d19bf088f43dc5380" - integrity sha512-6LnSeRldL7psIBfjDr4xXKxCqPVYfQE4Yj04p2VpIyAIpc4MVE4VOjzvILgnmAW8X93ou5/s5gQXvB4huDwTUQ== +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== dependencies: - graphql-language-service-interface "^2.8.2" - graphql-language-service-parser "^1.9.0" + graphql-language-service-interface "^2.9.0" + graphql-language-service-parser "^1.10.0" -codemirror@^5.54.0: - version "5.59.4" - resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz#bfc11c8ce32b04818e8d661bbd790a94f4b3a6f3" - integrity sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg== +codemirror@^5.58.2: + version "5.63.3" + resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.63.3.tgz#97042a242027fe0c87c09b36bc01931d37b76527" + integrity sha512-1C+LELr+5grgJYqwZKqxrcbPsHFHapVaVAloBsFBASbpLnQqLw1U8yXJ3gT5D+rhxIiSpo+kTqN+hQ+9ialIXw== codeowners-utils@^1.0.2: version "1.0.2" @@ -11082,16 +11265,16 @@ colorette@1.2.1: resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -colorette@^1.2.1, colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -colorette@^1.4.0: +colorette@^1.2.1, colorette@^1.2.2, colorette@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== +colorette@^2.0.10: + version "2.0.16" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + colors@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -11150,7 +11333,7 @@ commander@7.1.0: resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== -commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.7.1, commander@~2.20.3: +commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.7.1: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -11535,35 +11718,20 @@ core-js-compat@^3.9.0, core-js-compat@^3.9.1: browserslist "^4.16.6" semver "7.0.0" -core-js-pure@^3.10.2, core-js-pure@^3.6.5: - version "3.15.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.1.tgz#8356f6576fa2aa8e54ca6fe02620968ff010eed7" - integrity sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA== - -core-js-pure@^3.16.0: +core-js-pure@^3.10.2, core-js-pure@^3.16.0, core-js-pure@^3.6.5, core-js-pure@^3.8.2: version "3.16.2" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8" integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw== -core-js-pure@^3.8.2: - version "3.16.0" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.0.tgz#218e07add3f1844e53fab195c47871fc5ba18de8" - integrity sha512-wzlhZNepF/QA9yvx3ePDgNGudU5KDB8lu/TRPKelYA/QtSnkS/cLl2W+TIdEX1FAFcBr0YpY7tPDlcmXJ7AyiQ== - core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: version "2.6.12" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: - version "3.15.0" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044" - integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw== - -core-js@^3.6.0, core-js@^3.8.3: - version "3.18.3" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509" - integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw== + version "3.19.0" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz#9e40098a9bc326c7e81b486abbd5e12b9d275176" + integrity sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -11816,13 +11984,6 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-line-break@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz#3dc74c2ed5eb64211480281932475790243e7338" - integrity sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w== - dependencies: - base64-arraybuffer "^0.2.0" - css-loader@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" @@ -11863,16 +12024,6 @@ css-select-base-adapter@^0.1.1: resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - css-select@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" @@ -11883,6 +12034,17 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== + dependencies: + boolbase "^1.0.0" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" + nth-check "^2.0.0" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -11912,16 +12074,16 @@ css-vendor@^2.0.8: "@babel/runtime" "^7.8.3" is-in-browser "^1.0.2" -css-what@2.1: - version "2.1.3" - resolved "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - css-what@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== +css-what@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== + css.escape@1.5.1, css.escape@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -12021,22 +12183,15 @@ csso@^4.0.2: dependencies: css-tree "1.0.0-alpha.37" -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssstyle@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" - integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== - dependencies: - cssom "0.3.x" +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.2.0: version "2.3.0" @@ -12157,11 +12312,21 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== +"d3-color@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" + integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== + "d3-dispatch@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + d3-drag@2: version "2.0.0" resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" @@ -12170,11 +12335,24 @@ d3-drag@2: d3-dispatch "1 - 2" d3-selection "2" +"d3-drag@2 - 3": + version "3.0.0" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + "d3-ease@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + d3-force@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz#f20ccbf1e6c9e80add1926f09b51f686a8bc0937" @@ -12203,6 +12381,13 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 2" +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" @@ -12213,6 +12398,11 @@ d3-path@1: resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== +"d3-path@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" + integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== + "d3-quadtree@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz#edbad045cef88701f6fee3aee8e93fb332d30f9d" @@ -12235,6 +12425,11 @@ d3-selection@2, d3-selection@^2.0.0: resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + d3-shape@^1.2.0: version "1.3.7" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" @@ -12249,6 +12444,13 @@ d3-shape@^2.0.0: dependencies: d3-path "1 - 2" +d3-shape@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.0.1.tgz#9ccdfb28fd9b0d12f2d8aec234cd5c4a9ea27931" + integrity sha512-HNZNEQoDhuCrDWEc/BMbF/hKtzMZVoe64TvisFLDp2Iyj0UShB/E6/lBsLlJTfBMbYgftHj90cXJ0SEitlE6Xw== + dependencies: + d3-path "1 - 3" + d3-time-format@2: version "2.3.0" resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" @@ -12266,6 +12468,11 @@ d3-time@1: resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + d3-transition@2: version "2.0.0" resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" @@ -12277,6 +12484,17 @@ d3-transition@2: d3-interpolate "1 - 2" d3-timer "1 - 2" +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + d3-zoom@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz#f04d0afd05518becce879d04709c47ecd93fba54" @@ -12288,6 +12506,17 @@ d3-zoom@^2.0.0: d3-selection "2" d3-transition "2" +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -12326,15 +12555,6 @@ dashify@^2.0.0: resolved "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz#fff270ca2868ca427fee571de35691d6e437a648" integrity sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A== -data-urls@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -12354,11 +12574,6 @@ date-and-time@^1.0.0: resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-1.0.0.tgz#0062394bdf6f44e961f0db00511cb19cdf3cc0a5" integrity sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw== -date-fns@2.0.0-alpha.27: - version "2.0.0-alpha.27" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" - integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== - date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" @@ -12384,7 +12599,7 @@ dayjs@^1.10.4: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== -debounce@1.2.0, debounce@^1.2.0: +debounce@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== @@ -12396,7 +12611,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.2, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@4.3.2, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -12686,6 +12901,11 @@ detect-port@^1.3.0: address "^1.0.1" debug "^2.6.0" +devtools-protocol@0.0.901419: + version "0.0.901419" + resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" + integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -12821,7 +13041,7 @@ dom-accessibility-api@^0.5.4, dom-accessibility-api@^0.5.6: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== -dom-converter@^0.2: +dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -12870,7 +13090,7 @@ domain-browser@^1.1.1: resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -12880,12 +13100,10 @@ domelementtype@^2.0.1, domelementtype@^2.1.0: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" +domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== domexception@^2.0.1: version "2.0.1" @@ -12894,13 +13112,6 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - domhandler@^3.0, domhandler@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" @@ -12915,20 +13126,19 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" -dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.9: +domhandler@^4.2.0: + version "4.2.2" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" + integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== + dependencies: + domelementtype "^2.2.0" + +dompurify@^2.1.1, 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== -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -12945,6 +13155,15 @@ domutils@^2.0.0: domelementtype "^2.0.1" domhandler "^4.0.0" +domutils@^2.5.2, domutils@^2.6.0: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -13203,10 +13422,10 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enhanced-resolve@^5.8.0: - version "5.8.2" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" - integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== +enhanced-resolve@^5.8.3: + version "5.8.3" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" + integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -13223,12 +13442,17 @@ ent@^2.2.0: resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= -entities@^1.1.1, entities@^1.1.2: +entities@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0, entities@~2.0.0: +entities@^2.0.0, entities@~2.1.0: + version "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== @@ -13319,10 +13543,10 @@ es-get-iterator@^1.0.2: is-string "^1.0.5" isarray "^2.0.5" -es-module-lexer@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d" - integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw== +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== es-to-primitive@^1.2.1: version "1.2.1" @@ -13419,7 +13643,7 @@ escape-string-regexp@^5.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -escodegen@^1.14.1, escodegen@^1.9.1: +escodegen@^1.14.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -13757,6 +13981,11 @@ eventemitter2@^6.4.3: resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== +eventemitter2@^6.4.4: + version "6.4.5" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" + integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== + eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -13782,13 +14011,6 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource@^1.0.5: - version "1.0.7" - resolved "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -14076,6 +14298,17 @@ extract-files@^8.0.0: resolved "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288" integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" @@ -14101,11 +14334,6 @@ fast-decode-uri-component@^1.0.0: resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== -fast-deep-equal@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - fast-deep-equal@^3.0.0, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -14308,11 +14536,6 @@ file-uri-to-path@1.0.0: resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -filefy@0.1.10: - version "0.1.10" - resolved "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz#174677c8e2fa5bc39a3af0ed6fb492f16b8fbf42" - integrity sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ== - filelist@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" @@ -15352,18 +15575,19 @@ grapheme-splitter@^1.0.4: integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== graphiql@^1.0.0-alpha.10: - version "1.4.2" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.2.tgz#a1dc1a4d8d35f60c90d6d8a9eb62a99756e9fd9b" - integrity sha512-TQDuuU/ZqTWV1yQDpVEiKskg0IYA+Wck37DYrrFzLlpgZWRbWiyab1PyHKiRep7J540CgScBg6C/gGCymKyO3g== + version "1.4.7" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.7.tgz#6a35acf0786d7518fbb986b75bf0a3d752c19c1a" + integrity sha512-oHsBTzdWTbRJhqazbjrC6wY7YInViErAeXLqetCxdFFu2Zk5FV3V3rs7KPrCyr7kM6lW0nfXMzIfKuIgxAqx7g== dependencies: - "@graphiql/toolkit" "^0.2.0" - codemirror "^5.54.0" - codemirror-graphql "^1.0.0" + "@graphiql/toolkit" "^0.3.2" + codemirror "^5.58.2" + codemirror-graphql "^1.0.3" copy-to-clipboard "^3.2.0" dset "^3.1.0" entities "^2.0.0" - graphql-language-service "^3.1.2" - markdown-it "^10.0.0" + escape-html "^1.0.3" + graphql-language-service "^3.1.6" + markdown-it "^12.2.0" graphlib@^2.1.8: version "2.1.8" @@ -15398,20 +15622,20 @@ graphql-extensions@^0.15.0: apollo-server-env "^3.1.0" apollo-server-types "^0.9.0" -graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: - version "2.8.2" - resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" - integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== +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== dependencies: - graphql-language-service-parser "^1.9.0" - graphql-language-service-types "^1.8.0" - graphql-language-service-utils "^2.5.1" + graphql-language-service-parser "^1.10.0" + graphql-language-service-types "^1.8.3" + graphql-language-service-utils "^2.6.0" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: - version "1.9.0" - resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" - integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== +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== dependencies: graphql-language-service-types "^1.8.0" @@ -15420,21 +15644,41 @@ graphql-language-service-types@^1.8.0: 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-utils@^2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.1.tgz#832ad4b0a9da03fdded756932c27e057ccf71302" - integrity sha512-Lzz723cYrYlVN4WVzIyFGg3ogoe+QYAIBfdtDboiIILoy0FTmqbyC2TOErqbmWKqO4NK9xDA95cSRFbWiHYj0g== +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== dependencies: graphql-language-service-types "^1.8.0" nullthrows "^1.0.0" -graphql-language-service@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.1.2.tgz#6f50d5d824ea09c402cb02902b10e54b9da899d5" - integrity sha512-OiOH8mVE+uotrl3jGA2Pgt9k7rrI8lgw/8p+Cf6nwyEHbmIZj37vX9KoOWgpdFhuQlw824nNxWHSbz6k90xjWQ== +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== dependencies: - graphql-language-service-interface "^2.8.2" - graphql-language-service-types "^1.8.0" + 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-request@^3.3.0: version "3.4.0" @@ -15489,26 +15733,31 @@ graphql-type-json@^0.3.2: resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115" integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== -graphql-ws@^4.3.2: - 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-ws@^4.4.1: version "4.7.0" 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@^15.3.0, graphql@^15.4.0: +graphql@^15.3.0: version "15.5.1" resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz#f2f84415d8985e7b84731e7f3536f8bb9d383aad" integrity sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw== +graphql@^15.5.1: + version "15.6.1" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.6.1.tgz#9125bdf057553525da251e19e96dab3d3855ddfc" + integrity sha512-3i5lu0z6dRvJ48QP9kFxBkJ7h4Kso7PS8eahyTFz5Jm6CvQfLtNIE8LX9N6JLnXTuwR+sIYnXzaWp6anOg0QQw== + grouped-queue@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/grouped-queue/-/grouped-queue-2.0.0.tgz#a2c6713f2171e45db2c300a3a9d7c119d694dac8" @@ -15672,6 +15921,11 @@ hash-base@^3.0.0: inherits "^2.0.1" safe-buffer "^5.0.1" +hash-it@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/hash-it/-/hash-it-5.0.2.tgz#8cc981944964a5124f74f1065af0c0a5d182d556" + integrity sha512-csU3E/a9QEmEgPPxoShVuMcFWM329IGioEPRvYVBv3r5BFrU8pCfnk3jGEVvriAcwqd+nl6KsNhPPjg8MUzkhQ== + hash-stream-validation@^0.2.2: version "0.2.4" resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" @@ -15868,13 +16122,6 @@ html-comment-regex@^1.1.0: resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -15882,12 +16129,7 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== - -html-entities@^1.2.1: +html-entities@^1.2.0, html-entities@^1.2.1: version "1.4.0" resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== @@ -15956,26 +16198,6 @@ html-webpack-plugin@^5.3.1: pretty-error "^2.1.1" tapable "^2.0.0" -html2canvas@^1.0.0-rc.5: - version "1.3.2" - resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.3.2.tgz#951cc8388a3ce939fdac02131007ee28124afc27" - integrity sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ== - dependencies: - css-line-break "2.0.1" - text-segmentation "^1.0.2" - -htmlparser2@^3.3.0: - version "3.10.1" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - htmlparser2@^4.0: version "4.1.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -15986,6 +16208,16 @@ htmlparser2@^4.0: domutils "^2.0.0" entities "^2.0.0" +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -16111,7 +16343,7 @@ https-browserify@^1.0.0: resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -16156,7 +16388,7 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -16446,10 +16678,10 @@ inquirer@^8.0.0: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.1.0: - version "8.1.1" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.1.1.tgz#7c53d94c6d03011c7bb2a947f0dca3b98246c26a" - integrity sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w== +inquirer@^8.1.1: + version "8.2.0" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -16459,9 +16691,9 @@ inquirer@^8.1.0: figures "^3.0.0" lodash "^4.17.21" mute-stream "0.0.8" - ora "^5.3.0" + ora "^5.4.1" run-async "^2.4.0" - rxjs "^6.6.6" + rxjs "^7.2.0" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -16545,11 +16777,6 @@ is-absolute-url@^2.0.0: resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -16902,6 +17129,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -17946,38 +18178,6 @@ jscodeshift@^0.13.0: temp "^0.8.4" write-file-atomic "^2.3.0" -jsdom@11.12.0: - version "11.12.0" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - jsdom@^16.4.0: version "16.4.0" resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" @@ -18054,6 +18254,17 @@ json-pointer@^0.6.0: dependencies: foreach "^2.0.4" +json-rules-engine@^6.1.2: + version "6.1.2" + resolved "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-6.1.2.tgz#574ef455c10973fd5de07ea8414cbb72bb84d10f" + integrity sha512-+rtKuJ33HAvFywL9broh42FA9hkZNmS0l1DmgjP7nfGJ9E2i2IsfNH0BcXjyXianp/bXAyYlsSv308AfTuvBwQ== + dependencies: + clone "^2.1.2" + eventemitter2 "^6.4.4" + hash-it "^5.0.0" + jsonpath-plus "^5.0.7" + lodash.isobjectlike "^4.0.0" + json-schema-compare@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56" @@ -18194,6 +18405,11 @@ jsonpath-plus@^0.19.0: resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== +jsonpath-plus@^5.0.7: + version "5.1.0" + resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.1.0.tgz#2fc4b2e461950626c98525425a3a3518b85af6c3" + integrity sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ== + jsonpointer@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" @@ -18220,24 +18436,6 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" -jspdf-autotable@3.5.9: - version "3.5.9" - resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz#8a625ef2aead44271da95e9f649843c401536925" - integrity sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ== - -jspdf@2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz#2322f8644bc41845b3abe20db4c3ca0adeadb84c" - integrity sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ== - dependencies: - atob "^2.1.2" - btoa "^1.2.1" - optionalDependencies: - canvg "^3.0.6" - core-js "^3.6.0" - dompurify "^2.0.12" - html2canvas "^1.0.0-rc.5" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -18400,9 +18598,9 @@ keytar@^7.3.0: prebuild-install "^6.0.0" keyv-memcache@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-1.2.5.tgz#9097af5c617dc740e7300ebfd4a2efb8917429e3" - integrity sha512-iG+GHlhXyV83gmlCtMFIqNYVvv0i0towAy42NvziUR7D+k9jp81fAq0lu66H4gooOnW+ojkdigRvRpL40j1f7w== + version "1.2.7" + resolved "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-1.2.7.tgz#b8a43eeecdb11ad8f4d6d64abd4298d014c74955" + integrity sha512-lD7QaHf9M+bq9XLFZVEXJ2HNqEvO5CmtjiJjB2FHifbEQ18+YRDTfJCjo88Roc0hXtIqz/QZjYXpIYL8EWpL1g== dependencies: json-buffer "^3.0.1" memjs "^1.3.0" @@ -18421,11 +18619,6 @@ keyv@^4.0.0, keyv@^4.0.3: dependencies: json-buffer "3.0.1" -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - kind-of@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" @@ -18595,11 +18788,6 @@ leasot@^12.0.0: strip-ansi "^6.0.0" text-table "^0.2.0" -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - lerna@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -18681,13 +18869,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -linkify-it@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" - integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== - dependencies: - uc.micro "^1.0.1" - linkify-it@^3.0.1: version "3.0.2" resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" @@ -19001,6 +19182,11 @@ lodash.isobject@^3.0.2: resolved "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= +lodash.isobjectlike@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lodash.isobjectlike/-/lodash.isobjectlike-4.0.0.tgz#742c5fc65add27924d3d24191681aa9a17b2b60d" + integrity sha1-dCxfxlrdJ5JNPSQZFoGqmheytg0= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -19156,7 +19342,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.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.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== @@ -19341,13 +19527,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-age-cleaner@^0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -19385,17 +19564,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@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" - integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== - dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^2.0.0" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdown-it@^11.0.1: version "11.0.1" resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6" @@ -19407,6 +19575,17 @@ markdown-it@^11.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" + integrity sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-table@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" @@ -19432,24 +19611,6 @@ 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== -material-table@^1.69.1: - version "1.69.3" - resolved "https://registry.npmjs.org/material-table/-/material-table-1.69.3.tgz#3f282cffecf5461985965cd4a516bebb8069bf2d" - integrity sha512-UT/eQbUqfAg6XstcnM8dOQNgWmWbH5tbRmwqfGfPKQUQQXq/jb1z2spFHXPJBhEZFmk+Q95HlopiE6nAHymLMw== - dependencies: - "@date-io/date-fns" "1.1.0" - "@material-ui/pickers" "3.2.2" - classnames "2.2.6" - date-fns "2.0.0-alpha.27" - debounce "1.2.0" - fast-deep-equal "2.0.1" - filefy "0.1.10" - jspdf "2.1.0" - jspdf-autotable "3.5.9" - prop-types "15.6.2" - react-beautiful-dnd "13.0.0" - react-double-scrollbar "0.0.15" - 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" @@ -19721,14 +19882,6 @@ media-typer@0.3.0: vinyl "^2.0.1" vinyl-file "^3.0.0" -mem@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" - integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== - dependencies: - map-age-cleaner "^0.1.3" - mimic-fn "^3.1.0" - memfs@^3.1.2, memfs@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz#5de461389d596e3f23d48bb7c2afb6161f4df40e" @@ -20218,24 +20371,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.48.0, "mime-db@>= 1.43.0 < 2": - version "1.48.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== - mime-db@1.49.0: version "1.49.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== -mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.31" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== - dependencies: - mime-db "1.48.0" +"mime-db@>= 1.43.0 < 2": + version "1.48.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== -mime-types@^2.1.31: +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.32" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== @@ -20247,7 +20393,7 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.2.0, mime@^2.3.1, mime@^2.4.4, mime@^2.4.6: +mime@^2.2.0, mime@^2.4.4, mime@^2.4.6: version "2.5.2" resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== @@ -20262,11 +20408,6 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-fn@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" - integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== - mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -20351,7 +20492,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -20500,7 +20640,14 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.27.0, moment@^2.29.1: +moment-timezone@^0.5.31: + version "0.5.33" + resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" + integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.19.3, moment@^2.27.0, moment@^2.29.1: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -20560,29 +20707,30 @@ msal@^1.0.2: dependencies: tslib "^1.9.3" -msw@^0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/msw/-/msw-0.29.0.tgz#7242d575cb01db0c925241587df1fc2b79230d78" - integrity sha512-C/wz1d5uAEZRvAPAYrXG1rwLxXl0+BOs+JPrCzasoABZW3ATwS6ifSze+/DAgA93e9M86RXwvy6yDtZeZWmCFQ== +msw@^0.35.0: + version "0.35.0" + resolved "https://registry.npmjs.org/msw/-/msw-0.35.0.tgz#18a4ceb6c822ef226a30421d434413bc45030d38" + integrity sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w== dependencies: - "@mswjs/cookies" "^0.1.5" - "@mswjs/interceptors" "^0.10.0" + "@mswjs/cookies" "^0.1.6" + "@mswjs/interceptors" "^0.12.6" "@open-draft/until" "^1.0.3" - "@types/cookie" "^0.4.0" - "@types/inquirer" "^7.3.1" + "@types/cookie" "^0.4.1" + "@types/inquirer" "^7.3.3" "@types/js-levenshtein" "^1.1.0" chalk "^4.1.1" chokidar "^3.4.2" cookie "^0.4.1" - graphql "^15.4.0" + graphql "^15.5.1" headers-utils "^3.0.2" - inquirer "^8.1.0" + inquirer "^8.1.1" + is-node-process "^1.0.1" js-levenshtein "^1.1.6" node-fetch "^2.6.1" node-match-path "^0.6.3" statuses "^2.0.0" strict-event-emitter "^0.2.0" - type-fest "^1.1.3" + type-fest "^1.2.2" yargs "^17.0.1" multicast-dns-service-types@^1.1.0: @@ -20786,6 +20934,11 @@ node-abi@^2.21.0: dependencies: semver "^5.4.1" +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -20798,6 +20951,13 @@ node-cache@^5.1.2: dependencies: clone "2.x" +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + node-dir@^0.1.10, node-dir@^0.1.17: version "0.1.17" resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -20815,6 +20975,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.5" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + 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" @@ -21179,13 +21346,20 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: gauge "~2.7.3" set-blocking "~2.0.0" -nth-check@^1.0.2, nth-check@~1.0.1: +nth-check@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== dependencies: boolbase "~1.0.0" +nth-check@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + nullthrows@^1.0.0, nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -21210,7 +21384,7 @@ nunjucks@^3.2.3: asap "^2.0.3" commander "^5.1.0" -nwsapi@^2.0.7, nwsapi@^2.2.0: +nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== @@ -21340,10 +21514,10 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -octokit-plugin-create-pull-request@^3.9.3: - version "3.9.3" - resolved "https://registry.npmjs.org/octokit-plugin-create-pull-request/-/octokit-plugin-create-pull-request-3.9.3.tgz#f99f53907ac322a3494cc970514a023d7b659e2b" - integrity sha512-lTyNnCRoT4IvCQx2Cb4eFMqg8aIpsaDd59MNwf4OPnWAJM7hT6g7RW/icImvAzZLR4t5ENSLNzWarv2XqLL+Lg== +octokit-plugin-create-pull-request@^3.10.0: + version "3.10.0" + resolved "https://registry.npmjs.org/octokit-plugin-create-pull-request/-/octokit-plugin-create-pull-request-3.10.0.tgz#c9a589e0014e949eadd24a03ace10565007784d5" + integrity sha512-QU3nk62+OimV7ki+pV90cXoqqbUAQLdbqccS7/cNajdjQ2KYmaakz21FL1y78a5N0mA2P4WOs0o2+aunvbWI0w== dependencies: "@octokit/types" "^6.8.2" @@ -21473,13 +21647,6 @@ ora@^5.3.0, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -original@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -21528,6 +21695,11 @@ outdent@^0.5.0: resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== +outvariant@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.2.1.tgz#e630f6cdc1dbf398ed857e36f219de4a005ccd35" + integrity sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA== + overlayscrollbars@^1.13.1: version "1.13.1" resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz#0b840a88737f43a946b9d87875a2f9e421d0338a" @@ -21550,11 +21722,6 @@ p-cancelable@^2.0.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-each-series@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" @@ -21918,11 +22085,6 @@ parse-url@^5.0.0: parse-path "^4.0.0" protocols "^1.4.0" -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - parse5@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" @@ -22007,9 +22169,9 @@ passport-oauth2@1.2.0: uid2 "0.0.x" passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.0.tgz#5f599735e0ea40ea3027643785f81a3a9b4feb50" - integrity sha512-emXPLqLcVEcLFR/QvQXZcwLmfK8e9CqvMgmOFJxcNT3okSFMtUbRRKpY20x5euD+01uHsjjCa07DYboEeLXYiw== + version "1.6.1" + resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" + integrity sha512-ZbV43Hq9d/SBSYQ22GOiglFsjsD1YY/qdiptA+8ej+9C1dL1TVB+mBE5kDH/D4AJo50+2i8f4bx0vg4/yDDZCQ== dependencies: base64url "3.x.x" oauth "0.9.x" @@ -22404,6 +22566,13 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -22418,13 +22587,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - pkg-dir@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" @@ -22456,11 +22618,6 @@ pluralize@^8.0.0: resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - pnp-webpack-plugin@1.6.4: version "1.6.4" resolved "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" @@ -22995,12 +23152,12 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" - integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + version "2.1.2" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" + integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== dependencies: - renderkid "^2.0.1" - utila "~0.4" + lodash "^4.17.20" + renderkid "^2.0.4" pretty-format@^24.9.0: version "24.9.0" @@ -23032,10 +23189,10 @@ printj@~1.1.0: resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== -prismjs@^1.21.0, prismjs@^1.22.0, prismjs@~1.24.0: - version "1.24.1" - resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" - integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== +prismjs@^1.21.0, prismjs@^1.22.0, prismjs@~1.25.0: + version "1.25.0" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== private@^0.1.8: version "0.1.8" @@ -23057,6 +23214,11 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" + integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== + progress@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -23124,7 +23286,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompts@2.4.0, prompts@^2.0.1: +prompts@2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== @@ -23132,10 +23294,10 @@ prompts@2.4.0, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prompts@^2.4.0: - version "2.4.1" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" - integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ== +prompts@^2.0.1, prompts@^2.4.0: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" sisteransi "^1.0.5" @@ -23147,14 +23309,6 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.6.2: - version "15.6.2" - resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" - integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== - dependencies: - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -23230,6 +23384,11 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" +proxy-from-env@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -23330,6 +23489,24 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" +puppeteer@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz#a6465ff97fda0576c4ac29601406f67e6fea3dc7" + integrity sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w== + dependencies: + debug "4.3.1" + devtools-protocol "0.0.901419" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.1" + pkg-dir "4.2.0" + progress "2.0.1" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.0.0" + unbzip2-stream "1.3.3" + ws "7.4.6" + q@^1.1.2, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -23402,7 +23579,7 @@ raf-schd@^4.0.2: resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== -raf@^3.4.0, raf@^3.4.1: +raf@^3.4.0: version "3.4.1" resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -23495,7 +23672,7 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-beautiful-dnd@13.0.0, react-beautiful-dnd@^13.0.0: +react-beautiful-dnd@^13.0.0: version "13.0.0" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== @@ -24312,13 +24489,13 @@ reflect-metadata@^0.1.13: integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== refractor@^3.1.0, refractor@^3.2.0: - version "3.4.0" - resolved "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz#62bd274b06c942041f390c371b676eb67cb0a678" - integrity sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg== + version "3.5.0" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec" + integrity sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg== dependencies: hastscript "^6.0.0" parse-entities "^2.0.0" - prismjs "~1.24.0" + prismjs "~1.25.0" regenerate-unicode-properties@^8.2.0: version "8.2.0" @@ -24569,16 +24746,16 @@ remove-trailing-spaces@^1.0.6: resolved "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz#4354d22f3236374702f58ee373168f6d6887ada7" integrity sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA== -renderkid@^2.0.1: - version "2.0.3" - resolved "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" - integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== +renderkid@^2.0.4: + version "2.0.7" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" + integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== dependencies: - css-select "^1.1.0" - dom-converter "^0.2" - htmlparser2 "^3.3.0" - strip-ansi "^3.0.0" - utila "^0.4.0" + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^3.0.1" repeat-element@^1.1.2: version "1.1.3" @@ -24635,7 +24812,7 @@ request-promise-core@1.1.3: dependencies: lodash "^4.17.15" -request-promise-native@^1.0.5, request-promise-native@^1.0.8: +request-promise-native@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== @@ -24644,7 +24821,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -24848,11 +25025,6 @@ rgba-regex@^1.0.0: resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rgbcolor@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" - integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0= - rifm@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" @@ -24867,7 +25039,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -25010,7 +25182,7 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^6.6.6, rxjs@^6.6.7: +rxjs@^6.6.7: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -25969,11 +26141,6 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" -stackblur-canvas@^2.0.0: - version "2.5.0" - resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz#aa87bbed1560fdcd3138fff344fc6a1c413ebac4" - integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ== - stackframe@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71" @@ -26552,11 +26719,6 @@ svg-parser@^2.0.2: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svg-pathdata@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" - integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== - svgo@^1.0.0, svgo@^1.2.2: version "1.3.2" resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" @@ -26648,7 +26810,7 @@ symbol-observable@1.2.0, symbol-observable@^1.0.4, symbol-observable@^1.1.0, sym resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -symbol-tree@^3.2.2, symbol-tree@^3.2.4: +symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== @@ -26691,6 +26853,16 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +tar-fs@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -26948,13 +27120,6 @@ text-hex@1.0.x: resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -text-segmentation@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz#1f828fa14aa101c114ded1bda35ba7dcc17c9858" - integrity sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw== - dependencies: - utrie "^1.0.1" - text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -27179,7 +27344,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: +tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -27205,13 +27370,6 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - tr46@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" @@ -27219,6 +27377,11 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -27343,7 +27506,25 @@ ts-node@^10.0.0: source-map-support "^0.5.17" yn "3.1.1" -ts-node@^9, ts-node@^9.1.1: +ts-node@^10.2.1, ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + yn "3.1.1" + +ts-node@^9: version "9.1.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== @@ -27500,10 +27681,10 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.1.3: - version "1.2.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.2.1.tgz#232990aa513f3f5223abf54363975dfe3a121a2e" - integrity sha512-SbmIRuXhJs8KTneu77Ecylt9zuqL683tuiLYpTRil4H++eIhqCmx6ko6KAFem9dty8sOdnEiX7j4K1nRE628fQ== +type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" @@ -27544,18 +27725,18 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-json-schema@^0.50.1: - version "0.50.1" - resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz#48041eb9f6efbdf4ba88c3e3af9433601f7a2b47" - integrity sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA== +typescript-json-schema@^0.51.0: + version "0.51.0" + resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.51.0.tgz#e2abff69b8564c98c0edef2c13d55ef10fd71427" + integrity sha512-POhWbUNs2oaBti1W9k/JwS+uDsaZD9J/KQiZ/iXRQEOD0lTn9VmshIls9tn+A9X6O+smPjeEz5NEy6WTkCCzrQ== dependencies: - "@types/json-schema" "^7.0.7" - "@types/node" "^14.14.33" - glob "^7.1.6" + "@types/json-schema" "^7.0.9" + "@types/node" "^16.9.2" + glob "^7.1.7" json-stable-stringify "^1.0.1" - ts-node "^9.1.1" + ts-node "^10.2.1" typescript "~4.2.3" - yargs "^16.2.0" + yargs "^17.1.1" typescript@^4.0.3, typescript@~4.2.3: version "4.2.4" @@ -27578,12 +27759,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.8.0" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" - integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== - dependencies: - commander "~2.20.3" - source-map "~0.6.1" + version "3.14.3" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz#c0f25dfea1e8e5323eccf59610be08b6043c15cf" + integrity sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g== uid-number@0.0.6: version "0.0.6" @@ -27617,6 +27795,14 @@ unbox-primitive@^1.0.0: has-symbols "^1.0.0" which-boxed-primitive "^1.0.1" +unbzip2-stream@1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" + integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -28014,7 +28200,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3, url-parse@^1.5.3: +url-parse@^1.5.3: version "1.5.3" resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== @@ -28133,7 +28319,7 @@ util@^0.12.3: safe-buffer "^5.1.2" which-typed-array "^1.1.2" -utila@^0.4.0, utila@~0.4: +utila@~0.4: version "0.4.0" resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= @@ -28143,13 +28329,6 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -utrie@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz#e155235ebcbddc89ae09261ab6e773ce61401b2f" - integrity sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg== - dependencies: - base64-arraybuffer "^1.0.1" - uuid-browser@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" @@ -28350,7 +28529,7 @@ vscode-languageserver-types@^3.15.1: resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== -w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2: +w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== @@ -28449,10 +28628,10 @@ 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@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +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" @@ -28475,26 +28654,26 @@ webpack-dev-middleware@^3.7.3: range-parser "^1.2.1" webpack-log "^2.0.0" -webpack-dev-middleware@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz#0abe825275720e0a339978aea5f0b03b140c1584" - integrity sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw== +webpack-dev-middleware@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.1.tgz#97c948144349177856a3d2d9c612cc3fee180cf1" + integrity sha512-Kx1X+36Rn9JaZcQMrJ7qN3PMAuKmEDD9ZISjUj3Cgq4A6PtwYsC4mpaKotSRYH3iOF6HsUa8viHKS59FlyVifQ== dependencies: - colorette "^1.2.2" - mem "^8.1.1" + colorette "^2.0.10" memfs "^3.2.2" mime-types "^2.1.31" range-parser "^1.2.1" - schema-utils "^3.0.0" + schema-utils "^3.1.0" -webpack-dev-server@4.0.0-rc.0: - version "4.0.0-rc.0" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0-rc.0.tgz#56099f1e1e877d15a1fc28a928f38bbc600e33da" - integrity sha512-9S+MywBN/ecr8AbXNVUnmbFji8UTtzLR6M5Dgy6sB5Ti/73UgHn8TMhLaSBZBkY/cmSmWHDSwUXFs8lOeARpOw== +webpack-dev-server@4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.3.1.tgz#759d3337f0fbea297fbd1e433ab04ccfc000076b" + integrity sha512-qNXQCVYo1kYhH9pgLtm8LRNkXX3XzTfHSj/zqzaqYzGPca+Qjr+81wj1jgPMCHhIhso9WEQ+kX9z23iG9PzQ7w== dependencies: - ansi-html "^0.0.7" + ansi-html-community "^0.0.8" bonjour "^3.5.0" chokidar "^3.5.1" + colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^1.6.0" del "^6.0.0" @@ -28504,8 +28683,6 @@ webpack-dev-server@4.0.0-rc.0: http-proxy-middleware "^2.0.0" internal-ip "^6.2.0" ipaddr.js "^2.0.1" - is-absolute-url "^3.0.3" - killable "^1.0.1" open "^8.0.9" p-retry "^4.5.0" portfinder "^1.0.28" @@ -28516,8 +28693,8 @@ webpack-dev-server@4.0.0-rc.0: spdy "^4.0.2" strip-ansi "^7.0.0" url "^0.11.0" - webpack-dev-middleware "^5.0.0" - ws "^7.5.3" + webpack-dev-middleware "^5.2.1" + ws "^8.1.0" webpack-filter-warnings-plugin@^1.2.1: version "1.2.1" @@ -28597,9 +28774,9 @@ webpack@4: webpack-sources "^1.4.1" webpack@^5, webpack@^5.48.0: - version "5.48.0" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.48.0.tgz#06180fef9767a6fd066889559a4c4d49bee19b83" - integrity sha512-CGe+nfbHrYzbk7SKoYITCgN3LRAG0yVddjNUecz9uugo1QtYdiyrVD8nP1PhkNqPfdxC2hknmmKpP355Epyn6A== + version "5.60.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.60.0.tgz#9c26f38a57c9688b0a8c5c885e05197344eae67d" + integrity sha512-OL5GDYi2dKxnwJPSOg2tODgzDxAffN0osgWkZaBo/l3ikCxDFP+tuJT3uF7GyBE3SDBpKML/+a8EobyWAQO3DQ== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50" @@ -28610,8 +28787,8 @@ webpack@^5, webpack@^5.48.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.0" - es-module-lexer "^0.7.1" + enhanced-resolve "^5.8.3" + es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" @@ -28649,7 +28826,7 @@ websocket-extensions@>=0.1.1: resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: +whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== @@ -28666,28 +28843,18 @@ whatwg-fetch@^3.4.1: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3" integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ== -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: +whatwg-mimetype@^2.3.0: version "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@^6.4.1: - version "6.5.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== +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: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" + tr46 "~0.0.3" + webidl-conversions "^3.0.0" whatwg-url@^8.0.0, whatwg-url@^8.4.0: version "8.4.0" @@ -28930,34 +29097,20 @@ ws@7.4.5, ws@^7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== -ws@^5.2.0: - version "5.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" +ws@7.4.6: + version "7.4.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -"ws@^5.2.0 || ^6.0.0 || ^7.0.0": +"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" integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== -ws@^6.1.2: - version "6.2.1" - resolved "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -ws@^7.2.3, ws@^7.3.1: - version "7.3.1" - resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== - -ws@^7.5.3: - version "7.5.3" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +ws@^8.1.0: + version "8.2.3" + resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== xcase@^2.0.1: version "2.0.1" @@ -29040,20 +29193,15 @@ xmlchars@^2.2.0: resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" - integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== - xpath@0.0.32: version "0.0.32" resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== xss@^1.0.8: - version "1.0.9" - resolved "https://registry.npmjs.org/xss/-/xss-1.0.9.tgz#3ffd565571ff60d2e40db7f3b80b4677bec770d2" - integrity sha512-2t7FahYnGJys6DpHLhajusId7R0Pm2yTmuL0GV9+mV0ZlaLSnb2toBmppATfg5sWIhZQGlsTLoecSzya+l4EAQ== + version "1.0.10" + resolved "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2" + integrity sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw== dependencies: commander "^2.20.3" cssfilter "0.0.10" @@ -29167,6 +29315,19 @@ yargs@^17.0.0, yargs@^17.0.1: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.1.1: + version "17.2.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" + integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e" @@ -29318,23 +29479,10 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" -zombie@^6.1.4: - version "6.1.4" - resolved "https://registry.npmjs.org/zombie/-/zombie-6.1.4.tgz#9f0f53f3d9a032beb7f3fe5b382146a3475a4d47" - integrity sha512-yxNvKtyz3PP8lkr31AYh7vdbBD4is9hYXiOQKPp+k/7GiDiFQXX1Ex+peCl4ttodu/bHZcIluJ8lxMla5XefBQ== - dependencies: - babel-runtime "6.26.0" - bluebird "^3.5.1" - debug "^4.1.0" - eventsource "^1.0.5" - iconv-lite "^0.4.21" - jsdom "11.12.0" - lodash "^4.17.10" - mime "^2.3.1" - ms "^2.1.1" - request "^2.85.0" - tough-cookie "^2.3.4" - ws "^6.1.2" +zod@^3.9.5: + version "3.11.6" + resolved "https://registry.npmjs.org/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483" + integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg== zwitch@^1.0.0: version "1.0.5"