diff --git a/.changeset/chilled-pigs-destroy.md b/.changeset/chilled-pigs-destroy.md deleted file mode 100644 index 3ae2c09fbc..0000000000 --- a/.changeset/chilled-pigs-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'example-backend': patch ---- - -use node 14 for backend Dockerfile diff --git a/.changeset/dull-seals-march.md b/.changeset/dull-seals-march.md new file mode 100644 index 0000000000..714c5c5cad --- /dev/null +++ b/.changeset/dull-seals-march.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Add `BackstageRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes. diff --git a/.changeset/famous-eagles-swim.md b/.changeset/famous-eagles-swim.md new file mode 100644 index 0000000000..49f2724b5b --- /dev/null +++ b/.changeset/famous-eagles-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-register-component': patch +--- + +Register component plugin is deprecated in favor of @backstage/plugin-catalog-import diff --git a/.changeset/friendly-shoes-compare.md b/.changeset/friendly-shoes-compare.md new file mode 100644 index 0000000000..0b242511c0 --- /dev/null +++ b/.changeset/friendly-shoes-compare.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add `CatalogIndexPage` and `CatalogEntityPage`, two new extensions that replace the existing `Router` component. + +Add `EntityLayout` to replace `EntityPageLayout`, using children instead of an element property, and allowing for collection of all `RouteRef` mount points used within tabs. + +Add `EntitySwitch` to be used to select components based on entity data, along with accompanying `isKind`, `isNamespace`, and `isComponentType` filters. diff --git a/.changeset/gentle-dragons-marry.md b/.changeset/gentle-dragons-marry.md new file mode 100644 index 0000000000..d9ef443932 --- /dev/null +++ b/.changeset/gentle-dragons-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Link register API to catalog-import plugin diff --git a/.changeset/giant-rice-jump.md b/.changeset/giant-rice-jump.md deleted file mode 100644 index 20a574176e..0000000000 --- a/.changeset/giant-rice-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-api': patch ---- - -Fix for GitHub and SAML auth not properly updating session state when already logged in. diff --git a/.changeset/good-hairs-sniff.md b/.changeset/good-hairs-sniff.md deleted file mode 100644 index 540da80d60..0000000000 --- a/.changeset/good-hairs-sniff.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder': patch ---- - -Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` diff --git a/.changeset/little-lemons-hope.md b/.changeset/little-lemons-hope.md deleted file mode 100644 index e2a7f747d6..0000000000 --- a/.changeset/little-lemons-hope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -GitLab preparer uses the right token (primarily the same one as the publisher, falling back to the integrations token) diff --git a/.changeset/neat-maps-return.md b/.changeset/neat-maps-return.md new file mode 100644 index 0000000000..9ff2ebe845 --- /dev/null +++ b/.changeset/neat-maps-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-fossa': patch +--- + +Bump dependency versions of @backstage/core, cli and test-utils diff --git a/.changeset/orange-dancers-occur.md b/.changeset/orange-dancers-occur.md deleted file mode 100644 index 0283ffb634..0000000000 --- a/.changeset/orange-dancers-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': minor ---- - -Add search plugin to default template for CLI created apps diff --git a/.changeset/quiet-pants-happen.md b/.changeset/quiet-pants-happen.md deleted file mode 100644 index 165e3ae12c..0000000000 --- a/.changeset/quiet-pants-happen.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/test-utils': patch -'@backstage/plugin-graphiql': patch ---- - -Update to use new plugin extension API diff --git a/.changeset/rotten-nails-impress.md b/.changeset/rotten-nails-impress.md deleted file mode 100644 index f7f56a0ada..0000000000 --- a/.changeset/rotten-nails-impress.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-pagerduty': patch ---- - -Create AboutCard in core and use it in pagerduty and catalog plugin diff --git a/.changeset/shaggy-candles-rush.md b/.changeset/shaggy-candles-rush.md deleted file mode 100644 index 59532ef78e..0000000000 --- a/.changeset/shaggy-candles-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Start warning about usage of deprecated location types, such as `github` diff --git a/.changeset/short-dancers-explode.md b/.changeset/short-dancers-explode.md deleted file mode 100644 index 56fee63fe7..0000000000 --- a/.changeset/short-dancers-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-api': patch ---- - -Introduce new plugin extension API diff --git a/.changeset/slimy-penguins-sing.md b/.changeset/slimy-penguins-sing.md deleted file mode 100644 index 62b27a1ce6..0000000000 --- a/.changeset/slimy-penguins-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Added configuration schema for the commonly used properties diff --git a/.changeset/tasty-candles-remain.md b/.changeset/tasty-candles-remain.md deleted file mode 100644 index b6ae4faa96..0000000000 --- a/.changeset/tasty-candles-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Updated the config schema diff --git a/.changeset/violet-sloths-reply.md b/.changeset/violet-sloths-reply.md deleted file mode 100644 index ea73a8b22e..0000000000 --- a/.changeset/violet-sloths-reply.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fix for `app.listen.host` configuration not properly overriding listening host. diff --git a/.changeset/warm-moose-applaud.md b/.changeset/warm-moose-applaud.md deleted file mode 100644 index 70ae20711a..0000000000 --- a/.changeset/warm-moose-applaud.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Clearer error message when preparer or publisher type can't be determined. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2fafd3756d..92660b9ca4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,6 +11,11 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [master] + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests + paths-ignore: + - '**/*.md' + - '**/*.yml' + - '**/*.yaml' schedule: - cron: '0 8 * * 6' @@ -36,11 +41,6 @@ jobs: # a pull request then we can checkout the head. fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml new file mode 100644 index 0000000000..8ef3f10f6a --- /dev/null +++ b/.github/workflows/create-github-release.yml @@ -0,0 +1,36 @@ +# New tags and releases are created by changeset https://github.com/atlassian/changesets +name: Create a new release on GitHub when a new tag is created + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v0.4.0, v1.1.0 + +jobs: + build: + name: Create a new release on GitHub when a new tag is created + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: use node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: '12.x' + + - name: Install node dependencies + run: npm install @octokit/rest + + # GITHUB_REF is of the format refs/tags/vA.B.C + # This step extracts vA.B.C from GITHUB_REF + - name: Get the version + id: get_version + run: echo "::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}" + + # TODO/Note: This will only create a Draft release, which the maintainer can see and publish. + # If the Draft release looks good, modify the step to go ahead publish the release. (By adding the third CLI argument.) + - name: Create release on GitHub + run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} + env: + GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml index c103583052..ca402c48d5 100644 --- a/.github/workflows/master-win.yml +++ b/.github/workflows/master-win.yml @@ -51,10 +51,13 @@ jobs: - name: test run: yarn lerna -- run test + # credit: https://github.com/appleboy/discord-action/issues/3#issuecomment-731426861 - name: Discord notification if: ${{ failure() }} - uses: Ilshidur/action-discord@0.2.0 env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - with: - args: 'Windows master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' + run: | + $MESSAGE=@" + {\"content\": \"Windows master build failed https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}\"} + "@ + C:\msys64\usr\bin\curl.exe -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST $env:DISCORD_WEBHOOK --data $MESSAGE diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index c82bc8b95b..e1ddfd23f9 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -72,12 +72,8 @@ add the following ```typescript import { createRouter, - DirectoryPreparer, Preparers, Generators, - TechdocsGenerator, - CommonGitPreparer, - UrlPreparer, Publisher, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; @@ -90,30 +86,25 @@ export default async function createPlugin({ reader, }: PluginEnvironment) { // Preparers are responsible for fetching source files for documentation. - const preparers = new Preparers(); - - const directoryPreparer = new DirectoryPreparer(logger); - preparers.register('dir', directoryPreparer); - - const commonGitPreparer = new CommonGitPreparer(logger); - preparers.register('github', commonGitPreparer); - preparers.register('gitlab', commonGitPreparer); - preparers.register('azure/api', commonGitPreparer); - - const urlPreparer = new UrlPreparer(reader, logger); - preparers.register('url', urlPreparer); + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); // Generators are used for generating documentation sites. - const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger, config); - generators.register('techdocs', techdocsGenerator); + const generators = await Generators.fromConfig(config, { + logger, + }); - // Publishers are used for + // Publisher is used for // 1. Publishing generated files to storage // 2. Fetching files from storage and passing them to TechDocs frontend. - const publisher = Publisher.fromConfig(config, logger, discovery); + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); - // Docker client used by the generators. + // Docker client (conditionally) used by the generators, based on techdocs.generators config. const dockerClient = new Docker(); return await createRouter({ diff --git a/microsite/blog/2020-12-22-stability-index.md b/microsite/blog/2020-12-22-stability-index.md new file mode 100644 index 0000000000..95074a13a2 --- /dev/null +++ b/microsite/blog/2020-12-22-stability-index.md @@ -0,0 +1,42 @@ +--- +title: Announcing the Backstage Stability Index +author: Patrik Oldsberg, Spotify +authorURL: https://github.com/Rugvip +--- + +**TL;DR** Backstage is heading out of alpha and moving onto the path to stable releases and an eventual version 1.0. As the community and ecosystem continue to grow at an increasing rate, we want to provide a solid foundation for everyone building things in, with, and around Backstage. So, today we’re introducing the [Stability Index](https://backstage.io/docs/overview/stability-index) — a simple way to find out how likely (or unlikely) a specific package or plugin inside Backstage might be updated with major changes. By indicating the reliability of key features and APIs, this quick reference will help contributors and adopters better plan and coordinate their development efforts going forward. + +![Animation cycling between stability index scores](assets/2020-12-22/stability-index-hero.gif) + + + +Backstage has a great and growing community of users and contributors. With so much valuable feedback, this inevitably means that sometimes we will go back and revise design decisions that introduce breaking changes. Furthermore, as we continue to ship new features and systems, we’ll continue this cycle of iterating, gathering feedback, and iterating again before reaching a stable API. + +This rapid evolution can create uncertainty around which parts of the project are considered stable and which are not. Regardless of whether you're contributing to the project directly, setting up your own Backstage, or building plugins, you will be using various parts of the project that vary in stability. While many API surfaces of Backstage are reasonably solid and are unlikely to see any big breaking changes, others are more unstable or experimental. + +## The Stability Index + +In order to tackle the problem of uncertainty, and help align contributors, we have recently introduced a Stability Index. Inspired by a [similar concept with the same name in Node.js](https://nodejs.org/docs/latest-v4.x/api/documentation.html#documentation_stability_index), it’s a score assigned to subsets of the project, indicating the level of maturity of the API and the commitment to backwards compatibility. However, because of the current phase of the project, we have used a slightly different implementation. Rather than the score indicating a perceived stability, a higher score is instead a commitment to providing a smoother upgrade path for users, both through better documentation and backwards compatibility. Importantly, the Stability Index does not supersede [semantic versioning](https://semver.org/) (or semver), meaning we will still adhere to semver and only do breaking changes in minor releases as long as we are on 0.x. + +You can find more details about the scores on the [Stability Index](https://backstage.io/docs/overview/stability-index) page, but the following is a TL;DR of the 0–3 scores: + +- **0** — There's a changelog entry. +- **1** — There's a migration guide. +- **2** — Two weeks of deprecation. +- **3** — Three months of deprecation. + +The Stability Index has been tailored for the phase that the project is currently in, and we do not intend to keep it in place in its current form forever. It is, for example, likely that we in the future will increase the deprecation time limits. + +## Staying up to date + +In addition to the Stability Index, we have also recently adopted a weekly release cadence where we cut new releases every Thursday. Along with the two-week deprecation period of many of the API surfaces in Backstage, there is an excellent opportunity to form a routine to update your own Backstage projects either weekly or biweekly. + +In order to make the update process easier, we’ve added a new command to the Backstage CLI that allows you to easily update your project to use the latest Backstage release. The command is called `version:bump` and is executed in the root of your project using the CLI. You can see what it looks like in action below. + +![Output of the `backstage-cli` `version:bump` command](assets/2020-12-22/versions-bump.png) + +## Impact + +The Stability Index has already been active for a couple of weeks, and we have immediately seen an impact on the consideration and care taken when working in parts of the project that we have committed to keeping more stable. Both members of the core maintainer team and contributors from the community have taken care to roll out updates responsibly, with examples such as [PR #3524](https://github.com/backstage/backstage/pull/3524) and [PR #3465](https://github.com/backstage/backstage/pull/3465). + +The core team is working hard to bring more stability to broader parts of Backstage, and engineering it in a way that allows for long-term evolution. There are more learnings to be discovered as we continue evolving Backstage, but we are confident that the Stability Index will allow for a clearer path and a smoother ride for everyone involved! diff --git a/microsite/blog/assets/2020-12-22/stability-index-hero.gif b/microsite/blog/assets/2020-12-22/stability-index-hero.gif new file mode 100644 index 0000000000..6744cf977d Binary files /dev/null and b/microsite/blog/assets/2020-12-22/stability-index-hero.gif differ diff --git a/microsite/blog/assets/2020-12-22/versions-bump.png b/microsite/blog/assets/2020-12-22/versions-bump.png new file mode 100644 index 0000000000..b6069b7d74 Binary files /dev/null and b/microsite/blog/assets/2020-12-22/versions-bump.png differ diff --git a/microsite/data/plugins/aws-lambda.yaml b/microsite/data/plugins/aws-lambda.yaml index f325c0df8b..cc3d59d188 100644 --- a/microsite/data/plugins/aws-lambda.yaml +++ b/microsite/data/plugins/aws-lambda.yaml @@ -2,7 +2,7 @@ title: AWS Lambda author: roadie.io authorUrl: https://roadie.io -category: Monitoring +category: Infrastructure description: View AWS Lambda functions for your components in Backstage. documentation: https://roadie.io/backstage/plugins/aws-lambda iconUrl: https://roadie.io/images/logos/lambda.png diff --git a/microsite/data/plugins/firebase-functions.yaml b/microsite/data/plugins/firebase-functions.yaml index 8c1ea0be0a..9a1777754a 100644 --- a/microsite/data/plugins/firebase-functions.yaml +++ b/microsite/data/plugins/firebase-functions.yaml @@ -2,7 +2,7 @@ title: Firebase Functions author: roadie.io authorUrl: https://roadie.io/ -category: Monitoring +category: Infrastructure description: View Firebase Functions details for your service in Backstage. documentation: https://roadie.io/backstage/plugins/firebase-functions iconUrl: https://roadie.io/images/logos/firebase.png diff --git a/microsite/data/plugins/fossa.yaml b/microsite/data/plugins/fossa.yaml new file mode 100644 index 0000000000..c49e89c994 --- /dev/null +++ b/microsite/data/plugins/fossa.yaml @@ -0,0 +1,9 @@ +--- +title: FOSSA +author: SDA SE +authorUrl: https://sda.se/ +category: Quality +description: View FOSSA license compliance of your components in Backstage. +documentation: https://github.com/backstage/backstage/blob/master/plugins/fossa/README.md +iconUrl: https://avatars0.githubusercontent.com/u/9543448?s=400&v=4 +npmPackageName: '@backstage/plugin-fossa' diff --git a/microsite/data/plugins/pager-duty.yaml b/microsite/data/plugins/pager-duty.yaml index e8a0a1b0d9..d0006c09ec 100644 --- a/microsite/data/plugins/pager-duty.yaml +++ b/microsite/data/plugins/pager-duty.yaml @@ -1,5 +1,5 @@ --- -title: PageDuty +title: PagerDuty author: Spotify authorUrl: https://github.com/spotify category: Monitoring diff --git a/package.json b/package.json index caf19b0311..8376915729 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "docker-build": "yarn tsc && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", - "release": "changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile", + "release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile", "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn workspace storybook start", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 799895ef56..3eaf5ad111 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,23 @@ # example-app +## 0.2.8 + +### Patch Changes + +- Updated dependencies [19554f6d6] +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] +- Updated dependencies [7d72f9b09] + - @backstage/cli@0.4.3 + - @backstage/plugin-github-actions@0.2.6 + - @backstage/plugin-scaffolder@0.3.5 + - @backstage/core@0.4.2 + - @backstage/test-utils@0.1.6 + - @backstage/plugin-graphiql@0.2.3 + - @backstage/plugin-catalog@0.2.8 + - @backstage/plugin-pagerduty@0.2.4 + - @backstage/plugin-techdocs@0.5.1 + ## 0.2.7 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 63710a5c28..7541dd59a3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,39 +1,39 @@ { "name": "example-app", - "version": "0.2.7", + "version": "0.2.8", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/cli": "^0.4.2", - "@backstage/core": "^0.4.1", + "@backstage/cli": "^0.4.3", + "@backstage/core": "^0.4.2", "@backstage/plugin-api-docs": "^0.4.1", - "@backstage/plugin-catalog": "^0.2.7", + "@backstage/plugin-catalog": "^0.2.8", "@backstage/plugin-catalog-import": "^0.3.2", "@backstage/plugin-circleci": "^0.2.5", "@backstage/plugin-cloudbuild": "^0.2.5", "@backstage/plugin-cost-insights": "^0.5.2", "@backstage/plugin-explore": "^0.2.2", "@backstage/plugin-gcp-projects": "^0.2.2", - "@backstage/plugin-github-actions": "^0.2.5", + "@backstage/plugin-github-actions": "^0.2.6", "@backstage/plugin-gitops-profiles": "^0.2.2", - "@backstage/plugin-graphiql": "^0.2.2", + "@backstage/plugin-graphiql": "^0.2.3", "@backstage/plugin-org": "^0.3.2", "@backstage/plugin-jenkins": "^0.3.4", "@backstage/plugin-kubernetes": "^0.3.3", "@backstage/plugin-lighthouse": "^0.2.6", "@backstage/plugin-newrelic": "^0.2.2", - "@backstage/plugin-pagerduty": "0.2.3", + "@backstage/plugin-pagerduty": "0.2.4", "@backstage/plugin-register-component": "^0.2.5", "@backstage/plugin-rollbar": "^0.2.7", - "@backstage/plugin-scaffolder": "^0.3.4", + "@backstage/plugin-scaffolder": "^0.3.5", "@backstage/plugin-sentry": "^0.3.1", "@backstage/plugin-search": "^0.2.4", "@backstage/plugin-tech-radar": "^0.3.1", - "@backstage/plugin-techdocs": "^0.5.0", + "@backstage/plugin-techdocs": "^0.5.1", "@backstage/plugin-user-settings": "^0.2.3", "@backstage/plugin-welcome": "^0.2.3", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 3f053accfa..04cac6c2fc 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,22 @@ # example-backend +## 0.2.8 + +### Patch Changes + +- 7cfcd58ee: use node 14 for backend Dockerfile +- Updated dependencies [19554f6d6] +- Updated dependencies [33a82a713] +- Updated dependencies [5de26b9a6] +- Updated dependencies [30d6c78fb] +- Updated dependencies [5084e5039] +- Updated dependencies [a8573e53b] +- Updated dependencies [aed8f7f12] + - @backstage/plugin-scaffolder-backend@0.3.6 + - @backstage/plugin-catalog-backend@0.5.1 + - @backstage/plugin-techdocs-backend@0.5.0 + - example-app@0.2.8 + ## 0.2.7 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 718b008c7b..5c13ee94e2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.7", + "version": "0.2.8", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,18 +32,18 @@ "@backstage/config": "^0.1.2", "@backstage/plugin-app-backend": "^0.3.3", "@backstage/plugin-auth-backend": "^0.2.7", - "@backstage/plugin-catalog-backend": "^0.5.0", + "@backstage/plugin-catalog-backend": "^0.5.1", "@backstage/plugin-graphql-backend": "^0.1.4", "@backstage/plugin-kubernetes-backend": "^0.2.3", "@backstage/plugin-proxy-backend": "^0.2.3", "@backstage/plugin-rollbar-backend": "^0.1.5", - "@backstage/plugin-scaffolder-backend": "^0.3.5", - "@backstage/plugin-techdocs-backend": "^0.4.0", + "@backstage/plugin-scaffolder-backend": "^0.3.6", + "@backstage/plugin-techdocs-backend": "^0.5.0", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.7", + "example-app": "^0.2.8", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -53,7 +53,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index 4822de9c5c..afe92cc347 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -15,12 +15,8 @@ */ import { createRouter, - DirectoryPreparer, Preparers, Generators, - TechdocsGenerator, - CommonGitPreparer, - UrlPreparer, Publisher, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; @@ -33,30 +29,25 @@ export default async function createPlugin({ reader, }: PluginEnvironment) { // Preparers are responsible for fetching source files for documentation. - const preparers = new Preparers(); - - const directoryPreparer = new DirectoryPreparer(logger); - preparers.register('dir', directoryPreparer); - - const commonGitPreparer = new CommonGitPreparer(logger); - preparers.register('github', commonGitPreparer); - preparers.register('gitlab', commonGitPreparer); - preparers.register('azure/api', commonGitPreparer); - - const urlPreparer = new UrlPreparer(reader, logger); - preparers.register('url', urlPreparer); + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); // Generators are used for generating documentation sites. - const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger, config); - generators.register('techdocs', techdocsGenerator); + const generators = await Generators.fromConfig(config, { + logger, + }); - // Publishers are used for + // Publisher is used for // 1. Publishing generated files to storage // 2. Fetching files from storage and passing them to TechDocs frontend. - const publisher = Publisher.fromConfig(config, logger, discovery); + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); - // Docker client used by the generators. + // Docker client (conditionally) used by the generators, based on techdocs.generators config. const dockerClient = new Docker(); return await createRouter({ diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 8f73247301..256480bf70 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/cli +## 0.4.3 + +### Patch Changes + +- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` +- 7d72f9b09: Fix for `app.listen.host` configuration not properly overriding listening host. + ## 0.4.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index d8e1a96b46..90d2fe24f1 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.4.2", + "version": "0.4.3", "private": false, "publishConfig": { "access": "public" @@ -113,9 +113,9 @@ "devDependencies": { "@backstage/backend-common": "^0.4.1", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@types/diff": "^4.0.2", "@types/fs-extra": "^9.0.1", diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index de997bf065..2d8915e7f1 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/core-api +## 0.2.7 + +### Patch Changes + +- d681db2b5: Fix for GitHub and SAML auth not properly updating session state when already logged in. +- 1dc445e89: Introduce new plugin extension API +- Updated dependencies [1dc445e89] + - @backstage/test-utils@0.1.6 + ## 0.2.6 ### Patch Changes diff --git a/packages/core-api/package.json b/packages/core-api/package.json index fa8bd7a086..f02520d170 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.6", + "version": "0.2.7", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/core-api/src/routing/BackstageRoutes.tsx b/packages/core-api/src/routing/BackstageRoutes.tsx new file mode 100644 index 0000000000..6c042af27d --- /dev/null +++ b/packages/core-api/src/routing/BackstageRoutes.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ReactNode, Children, isValidElement, Fragment } from 'react'; +import { useRoutes } from 'react-router-dom'; + +type RouteObject = { + path: string; + element: JSX.Element; + children?: RouteObject[]; +}; + +// Similar to the same function from react-router, this collects routes from the +// children, but only the first level of routes +function createRoutesFromChildren(children: ReactNode): RouteObject[] { + return Children.toArray(children) + .flatMap(child => { + if (!isValidElement(child)) { + return []; + } + + const { children } = child.props; + + if (child.type === Fragment) { + return createRoutesFromChildren(children); + } + + let path = child.props.path as string | undefined; + + // TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone + if (path === '') { + return []; + } + path = path?.replace(/\/\*$/, '') ?? '/'; + + return [ + { + path, + element: child, + children: children && [ + { + path: '/*', + element: children, + }, + ], + }, + ]; + }) + .sort((a, b) => b.path.localeCompare(a.path)) + .map(obj => { + obj.path = obj.path === '/' ? '/' : `${obj.path}/*`; + return obj; + }); +} + +type BackstageRoutesProps = { + children: ReactNode; +}; + +export const BackstageRoutes = ( + props: BackstageRoutesProps, +): JSX.Element | null => { + const routes = createRoutesFromChildren(props.children); + return useRoutes(routes); +}; diff --git a/packages/core-api/src/routing/collectors.test.tsx b/packages/core-api/src/routing/collectors.test.tsx index 200846f175..44d53856b4 100644 --- a/packages/core-api/src/routing/collectors.test.tsx +++ b/packages/core-api/src/routing/collectors.test.tsx @@ -15,7 +15,11 @@ */ import React, { PropsWithChildren } from 'react'; -import { routePathCollector, routeParentCollector } from './collectors'; +import { + routePathCollector, + routeParentCollector, + routeObjectCollector, +} from './collectors'; import { traverseElementTree, @@ -24,21 +28,22 @@ import { } from '../extensions/traversal'; import { createRouteRef } from './RouteRef'; import { createPlugin } from '../plugin'; -import { createRoutableExtension } from '../extensions'; +import { attachComponentData, createRoutableExtension } from '../extensions'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { RouteRef } from './types'; -const mockConfig = () => ({ path: '/foo', title: 'Foo' }); const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( <>{children} ); const plugin = createPlugin({ id: 'my-plugin' }); -const ref1 = createRouteRef(mockConfig()); -const ref2 = createRouteRef(mockConfig()); -const ref3 = createRouteRef(mockConfig()); -const ref4 = createRouteRef(mockConfig()); -const ref5 = createRouteRef(mockConfig()); +const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' }); +const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' }); +const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' }); +const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' }); +const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' }); +const refOrder = [ref1, ref2, ref3, ref4, ref5]; const Extension1 = plugin.provide( createRoutableExtension({ @@ -71,6 +76,30 @@ const Extension5 = plugin.provide( }), ); +const AggregationComponent = ({ + children, +}: PropsWithChildren<{ + path: string; +}>) => <>{children}; + +attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); + +function sortedEntries(map: Map): [RouteRef, T][] { + return Array.from(map).sort( + ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), + ); +} + +function routeObj(path: string, refs: RouteRef[], children: any[] = []) { + return { + path: path, + caseSensitive: false, + element: null, + routeRefs: new Set(refs), + children: children, + }; +} + describe('discovery', () => { it('should collect routes', () => { const list = [ @@ -109,33 +138,40 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routes, routeParents, routeObjects } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routes: routePathCollector, routeParents: routeParentCollector, + routeObjects: routeObjectCollector, }, }); - expect(routes).toEqual( - new Map([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]), - ); - - expect(routeParents).toEqual( - new Map([ - [ref1, undefined], - [ref2, ref1], - [ref3, ref2], - [ref4, undefined], - [ref5, ref1], - ]), - ); + expect(sortedEntries(routes)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routeParents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, undefined], + [ref5, ref1], + ]); + expect(routeObjects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), + routeObj('/blop', [ref5]), + ], + ), + routeObj('/divsoup', [ref4]), + ]); }); it('should handle all react router Route patterns', () => { @@ -168,24 +204,154 @@ describe('discovery', () => { routeParents: routeParentCollector, }, }); - expect(routes).toEqual( - new Map([ - [ref1, '/foo'], - [ref2, '/bar/:id'], - [ref3, '/baz'], - [ref4, '/divsoup'], - [ref5, '/blop'], - ]), + expect(sortedEntries(routes)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routeParents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + }); + + it('should use the route aggregator key to bind child routes to the same path', () => { + const root = ( + + + + +
+ +
+ HELLO +
+ + + + + + + +
+
); - expect(routeParents).toEqual( - new Map([ - [ref1, undefined], - [ref2, ref1], - [ref3, undefined], - [ref4, ref3], - [ref5, ref3], - ]), + + const { routes, routeParents, routeObjects } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + }, + }); + expect(sortedEntries(routes)).toEqual([ + [ref1, '/foo'], + [ref2, '/foo'], + [ref3, '/bar'], + [ref4, '/baz'], + [ref5, '/baz'], + ]); + expect(sortedEntries(routeParents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + expect(routeObjects).toEqual([ + routeObj('/foo', [ref1, ref2]), + routeObj('/bar', [ref3], [routeObj('/baz', [ref4, ref5])]), + ]); + }); + + it('should use the route aggregator but stop when encountering explicit path', () => { + const root = ( + + + + + + + + + + + + + + ); + + const { routes, routeParents, routeObjects } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + }, + }); + expect(sortedEntries(routes)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + [ref3, '/baz'], + [ref4, '/blop'], + [ref5, '/bar'], + ]); + expect(sortedEntries(routeParents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref1], + [ref4, ref3], + [ref5, ref1], + ]); + expect(routeObjects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar', + [ref2, ref5], + [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], + ), + ], + ), + ]); + }); + + it('should stop gathering mount points after encountering explicit path', () => { + const root = ( + + + + + + + + + + + + ); + + expect(() => { + traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routePathCollector, + routeParents: routeParentCollector, + }, + }); + }).toThrow('Mounted routable extension must have a path'); }); it('should not visit the same element twice', () => { diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index e47a59aa85..37362cf8d4 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -32,25 +32,48 @@ function getMountPoint(node: ReactElement): RouteRef | undefined { export const routePathCollector = createCollector( () => new Map(), - (acc, node, parent) => { + (acc, node, parent, ctxPath: string | undefined) => { + // The context path is used during mount point gathering to assign the same path + // to all discovered mount points + let currentCtxPath = ctxPath; + if (parent?.props.element === node) { - return; + return currentCtxPath; + } + + // Start gathering mount points when we encounter a mount point gathering flag + if (getComponentData(node, 'core.gatherMountPoints')) { + const path: string | undefined = node.props?.path; + if (!path) { + throw new Error('Mount point gatherer must have a path'); + } + currentCtxPath = path; } const routeRef = getMountPoint(node); if (routeRef) { - const path: string | undefined = node.props?.path; + let path: string | undefined = node.props?.path; + // If we're gathering mount points we use the context path as out path, unless + // the element has its own path, in which case we use that instead and stop gathering + if (currentCtxPath) { + if (path) { + currentCtxPath = undefined; + } else { + path = currentCtxPath; + } + } if (!path) { throw new Error('Mounted routable extension must have a path'); } acc.set(routeRef, path); } + return currentCtxPath; }, ); export const routeParentCollector = createCollector( () => new Map(), - (acc, node, parent, parentRouteRef?: RouteRef) => { + (acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => { if (parent?.props.element === node) { return parentRouteRef; } @@ -59,8 +82,27 @@ export const routeParentCollector = createCollector( const routeRef = getMountPoint(node); if (routeRef) { - acc.set(routeRef, parentRouteRef); - nextParent = routeRef; + // "sticky" route ref is when we've encountered a mount point gatherer, and we want a + // mount points beneath it to have the same parent, regardless of internal structure + if (parentRouteRef && 'sticky' in parentRouteRef) { + acc.set(routeRef, parentRouteRef.sticky); + + // When we encounter a mount point with an explicit path, we stop gathering + // mount points withing the children and remove the sticky state + if (node.props?.path) { + nextParent = routeRef; + } else { + nextParent = parentRouteRef; + } + } else { + acc.set(routeRef, parentRouteRef); + nextParent = routeRef; + } + } + + // Mount point gatherers are marked as "sticky" + if (getComponentData(node, 'core.gatherMountPoints')) { + return { sticky: nextParent }; } return nextParent; @@ -69,9 +111,10 @@ export const routeParentCollector = createCollector( export const routeObjectCollector = createCollector( () => Array(), - (acc, node, parent, parentChildArr: BackstageRouteObject[] = acc) => { + (acc, node, parent, parentObj: BackstageRouteObject | undefined) => { + const parentChildren = parentObj?.children ?? acc; if (parent?.props.element === node) { - return parentChildArr; + return parentObj; } const path: string | undefined = node.props?.path; @@ -79,20 +122,40 @@ export const routeObjectCollector = createCollector( const routeRef = getMountPoint(node); if (routeRef) { - const children: BackstageRouteObject[] = []; - if (!path) { - throw new Error(`No path found for mount point ${routeRef}`); + if (path) { + const newObject: BackstageRouteObject = { + caseSensitive, + path, + element: null, + routeRefs: new Set([routeRef]), + children: [], + }; + parentChildren.push(newObject); + return newObject; } - parentChildArr.push({ + + parentObj?.routeRefs.add(routeRef); + } + + const isGatherer = getComponentData( + node, + 'core.gatherMountPoints', + ); + if (isGatherer) { + if (!path) { + throw new Error('Mount point gatherer must have a path'); + } + const newObject: BackstageRouteObject = { caseSensitive, path, element: null, - routeRef, - children, - }); - return children; + routeRefs: new Set(), + children: [], + }; + parentChildren.push(newObject); + return newObject; } - return parentChildArr; + return parentObj; }, ); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 55fde89de2..3a18d8b0af 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -60,8 +60,8 @@ class RouteResolver { currentRouteRef; currentRouteRef = this.routeParents.get(currentRouteRef) ) { - matchIndex = match.findIndex( - m => (m.route as BackstageRouteObject).routeRef === currentRouteRef, + matchIndex = match.findIndex(m => + (m.route as BackstageRouteObject).routeRefs.has(currentRouteRef!), ); if (matchIndex !== -1) { break; diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 3591f299f1..2bdf0aa5e8 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -21,5 +21,6 @@ export type { ConcreteRoute, MutableRouteRef, } from './types'; +export { BackstageRoutes } from './BackstageRoutes'; export { createRouteRef } from './RouteRef'; export { useRouteRef } from './hooks'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index e91ca7ea53..c6d79fd991 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -62,5 +62,5 @@ export interface BackstageRouteObject { children?: BackstageRouteObject[]; element: React.ReactNode; path: string; - routeRef: AnyRouteRef; + routeRefs: Set; } diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d4eb0a0a95..6b59bb7fbb 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core +## 0.4.2 + +### Patch Changes + +- 1dc445e89: Update to use new plugin extension API +- 342270e4d: Create AboutCard in core and use it in pagerduty and catalog plugin +- Updated dependencies [d681db2b5] +- Updated dependencies [1dc445e89] + - @backstage/core-api@0.2.7 + ## 0.4.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 3c636a0d80..71f2daceb8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.4.1", + "version": "0.4.2", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core-api": "^0.2.6", + "@backstage/core-api": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -65,8 +65,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.2", - "@backstage/test-utils": "^0.1.5", + "@backstage/cli": "^0.4.3", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 8e9b3692af..e589cab0ae 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/create-app +## 0.3.0 + +### Minor Changes + +- 0101c7a16: Add search plugin to default template for CLI created apps + +### Patch Changes + +- a8573e53b: techdocs-backend: Simplified file, removing individual preparers and generators. + techdocs-backend: UrlReader is now available to use in preparers. + + In your Backstage app, `packages/backend/plugins/techdocs.ts` file has now been simplified, + to remove registering individual preparers and generators. + + Please update the file when upgrading the version of `@backstage/plugin-techdocs-backend` package. + + ```typescript + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); + + const generators = await Generators.fromConfig(config, { + logger, + }); + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + ``` + + You should be able to remove unnecessary imports, and just do + + ```typescript + import { + createRouter, + Preparers, + Generators, + Publisher, + } from '@backstage/plugin-techdocs-backend'; + ``` + ## 0.2.5 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 52691fc2ee..23a0b1a584 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.2.5", + "version": "0.3.0", "private": false, "publishConfig": { "access": "public" @@ -39,29 +39,29 @@ "devDependencies": { "@backstage/backend-common": "^0.4.1", "@backstage/catalog-model": "^0.6.0", - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-api-docs": "^0.4.1", "@backstage/plugin-app-backend": "^0.3.3", "@backstage/plugin-auth-backend": "^0.2.7", - "@backstage/plugin-catalog": "^0.2.7", - "@backstage/plugin-catalog-backend": "^0.5.0", + "@backstage/plugin-catalog": "^0.2.8", + "@backstage/plugin-catalog-backend": "^0.5.1", "@backstage/plugin-circleci": "^0.2.5", "@backstage/plugin-explore": "^0.2.2", - "@backstage/plugin-github-actions": "^0.2.5", + "@backstage/plugin-github-actions": "^0.2.6", "@backstage/plugin-lighthouse": "^0.2.6", "@backstage/plugin-proxy-backend": "^0.2.3", "@backstage/plugin-register-component": "^0.2.5", "@backstage/plugin-rollbar-backend": "^0.1.5", - "@backstage/plugin-scaffolder": "^0.3.4", + "@backstage/plugin-scaffolder": "^0.3.5", "@backstage/plugin-search": "^0.2.4", - "@backstage/plugin-scaffolder-backend": "^0.3.5", + "@backstage/plugin-scaffolder-backend": "^0.3.6", "@backstage/plugin-tech-radar": "^0.3.1", - "@backstage/plugin-techdocs": "^0.5.0", - "@backstage/plugin-techdocs-backend": "^0.4.0", + "@backstage/plugin-techdocs": "^0.5.1", + "@backstage/plugin-techdocs-backend": "^0.5.0", "@backstage/plugin-user-settings": "^0.2.3", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts index 1bbb5ff24b..5c7ec50ae6 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -1,10 +1,7 @@ import { createRouter, - DirectoryPreparer, Preparers, Generators, - TechdocsGenerator, - CommonGitPreparer, Publisher, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; @@ -14,22 +11,28 @@ export default async function createPlugin({ logger, config, discovery, + reader, }: PluginEnvironment) { - const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger, config); + // Preparers are responsible for fetching source files for documentation. + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); - generators.register('techdocs', techdocsGenerator); + // Generators are used for generating documentation sites. + const generators = await Generators.fromConfig(config, { + logger, + }); - const preparers = new Preparers(); - const directoryPreparer = new DirectoryPreparer(logger); - const commonGitPreparer = new CommonGitPreparer(logger); - - preparers.register('dir', directoryPreparer); - preparers.register('github', commonGitPreparer); - preparers.register('gitlab', commonGitPreparer); - - const publisher = Publisher.fromConfig(config, logger, discovery); + // Publisher is used for + // 1. Publishing generated files to storage + // 2. Fetching files from storage and passing them to TechDocs frontend. + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + // Docker client (conditionally) used by the generators, based on techdocs.generators config. const dockerClient = new Docker(); return await createRouter({ diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index af3da478a5..40bac7ce7f 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/techdocs-common +## 0.3.0 + +### Minor Changes + +- a8573e53b: techdocs-backend: Simplified file, removing individual preparers and generators. + techdocs-backend: UrlReader is now available to use in preparers. + + In your Backstage app, `packages/backend/plugins/techdocs.ts` file has now been simplified, + to remove registering individual preparers and generators. + + Please update the file when upgrading the version of `@backstage/plugin-techdocs-backend` package. + + ```typescript + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); + + const generators = await Generators.fromConfig(config, { + logger, + }); + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + ``` + + You should be able to remove unnecessary imports, and just do + + ```typescript + import { + createRouter, + Preparers, + Generators, + Publisher, + } from '@backstage/plugin-techdocs-backend'; + ``` + ## 0.2.0 ### Minor Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 632dbc2b66..7144477d8a 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.2.0", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -54,7 +54,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", "@types/js-yaml": "^3.12.5", diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index f28a169942..cbfa5e7553 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -14,18 +14,32 @@ * limitations under the License. */ +import { Logger } from 'winston'; +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { TechdocsGenerator } from '.'; import { GeneratorBase, SupportedGeneratorKey, GeneratorBuilder, } from './types'; - -import { Entity } from '@backstage/catalog-model'; import { getGeneratorKey } from './helpers'; export class Generators implements GeneratorBuilder { private generatorMap = new Map(); + static async fromConfig( + config: Config, + { logger }: { logger: Logger }, + ): Promise { + const generators = new Generators(); + + const techdocsGenerator = new TechdocsGenerator(logger, config); + generators.register('techdocs', techdocsGenerator); + + return generators; + } + register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase) { this.generatorMap.set(generatorKey, generator); } diff --git a/packages/techdocs-common/src/stages/prepare/preparers.ts b/packages/techdocs-common/src/stages/prepare/preparers.ts index 52a47957e8..4a2d60eb31 100644 --- a/packages/techdocs-common/src/stages/prepare/preparers.ts +++ b/packages/techdocs-common/src/stages/prepare/preparers.ts @@ -13,14 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types'; +import { Logger } from 'winston'; +import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { DirectoryPreparer, CommonGitPreparer, UrlPreparer } from '.'; +import { PreparerBase, RemoteProtocol, PreparerBuilder } from './types'; import { parseReferenceAnnotation } from '../../helpers'; +type factoryOptions = { + logger: Logger; + reader: UrlReader; +}; + export class Preparers implements PreparerBuilder { private preparerMap = new Map(); + static async fromConfig( + // @ts-ignore + // Config not used now, but will be used in urlPreparer when it starts using + // @backstage/integration to get the tokens for providers. + config: Config, + { logger, reader }: factoryOptions, + ): Promise { + const preparers = new Preparers(); + + const directoryPreparer = new DirectoryPreparer(logger); + preparers.register('dir', directoryPreparer); + + const commonGitPreparer = new CommonGitPreparer(logger); + preparers.register('github', commonGitPreparer); + preparers.register('gitlab', commonGitPreparer); + preparers.register('azure/api', commonGitPreparer); + + const urlPreparer = new UrlPreparer(reader, logger); + preparers.register('url', urlPreparer); + + return preparers; + } + register(protocol: RemoteProtocol, preparer: PreparerBase) { this.preparerMap.set(protocol, preparer); } diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index dbced6db29..c61b9aa3ec 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -23,24 +23,27 @@ import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; const logger = getVoidLogger(); -const testDiscovery: jest.Mocked = { +const discovery: jest.Mocked = { getBaseUrl: jest.fn().mockResolvedValueOnce('http://localhost:7000'), getExternalBaseUrl: jest.fn(), }; describe('Publisher', () => { - it('should create local publisher by default', () => { + it('should create local publisher by default', async () => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', }, }); - const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); expect(publisher).toBeInstanceOf(LocalPublish); }); - it('should create local publisher from config', () => { + it('should create local publisher from config', async () => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -50,11 +53,14 @@ describe('Publisher', () => { }, }); - const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); expect(publisher).toBeInstanceOf(LocalPublish); }); - it('should create google gcs publisher from config', () => { + it('should create google gcs publisher from config', async () => { const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -69,7 +75,10 @@ describe('Publisher', () => { }, }); - const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); expect(publisher).toBeInstanceOf(GoogleGCSPublish); }); }); diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index 04a9d89996..95b5cf83e2 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -21,16 +21,20 @@ import { PublisherType, PublisherBase } from './types'; import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; +type factoryOptions = { + logger: Logger; + discovery: PluginEndpointDiscovery; +}; + /** * Factory class to create a TechDocs publisher based on defined publisher type in app config. * Uses `techdocs.publisher.type`. */ export class Publisher { - static fromConfig( + static async fromConfig( config: Config, - logger: Logger, - discovery: PluginEndpointDiscovery, - ): PublisherBase { + { logger, discovery }: factoryOptions, + ): Promise { const publisherType = (config.getOptionalString( 'techdocs.publisher.type', ) ?? 'local') as PublisherType; diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index b311d58b94..6d23f26691 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/test-utils +## 0.1.6 + +### Patch Changes + +- 1dc445e89: Update to use new plugin extension API +- Updated dependencies [d681db2b5] +- Updated dependencies [1dc445e89] + - @backstage/core-api@0.2.7 + ## 0.1.5 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index e550caee4b..8c5a7c1f39 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.5", + "version": "0.1.6", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-api": "^0.2.5", + "@backstage/core-api": "^0.2.7", "@backstage/test-utils-core": "^0.1.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.1", + "@backstage/cli": "^0.4.3", "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 7346722550..c907288424 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@kyma-project/asyncapi-react": "^0.14.2", @@ -49,9 +49,9 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 398268caf2..a602877525 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -37,7 +37,7 @@ export const ApiExplorerPage = () => { variant="contained" color="primary" component={RouterLink} - to="/register-component" + to="/catalog-import" > Register Existing API diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 95fea0ef3e..8e941457a9 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend +## 0.5.1 + +### Patch Changes + +- 5de26b9a6: Start warning about usage of deprecated location types, such as `github` +- 30d6c78fb: Added configuration schema for the commonly used properties +- 5084e5039: Updated the config schema + ## 0.5.0 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index dde2c2dc65..5ebe1c3e41 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,8 +57,8 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", - "@backstage/test-utils": "^0.1.5", + "@backstage/cli": "^0.4.3", + "@backstage/test-utils": "^0.1.6", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 25010268f9..144627d838 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/plugin-catalog-backend": "^0.5.0", "@backstage/integration": "^0.1.4", @@ -50,9 +50,9 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 73f38582ff..a2b8d7e1a4 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog +## 0.2.8 + +### Patch Changes + +- 342270e4d: Create AboutCard in core and use it in pagerduty and catalog plugin +- Updated dependencies [19554f6d6] +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] + - @backstage/plugin-scaffolder@0.3.5 + - @backstage/core@0.4.2 + - @backstage/plugin-techdocs@0.5.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 142ef4a1e6..8bdc1fecd0 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "dependencies": { "@backstage/catalog-client": "^0.3.4", "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", - "@backstage/plugin-scaffolder": "^0.3.4", - "@backstage/plugin-techdocs": "^0.5.0", + "@backstage/core": "^0.4.2", + "@backstage/plugin-scaffolder": "^0.3.5", + "@backstage/plugin-techdocs": "^0.5.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx new file mode 100644 index 0000000000..e0a314a391 --- /dev/null +++ b/plugins/catalog/src/components/CatalogEntityPage/CatalogEntityPage.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Outlet } from 'react-router'; +import { EntityProvider } from '../EntityProvider'; + +export const CatalogEntityPage = () => { + return ( + + + + ); +}; diff --git a/plugins/catalog/src/components/CatalogEntityPage/index.ts b/plugins/catalog/src/components/CatalogEntityPage/index.ts new file mode 100644 index 0000000000..627ca80e48 --- /dev/null +++ b/plugins/catalog/src/components/CatalogEntityPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { CatalogEntityPage } from './CatalogEntityPage'; diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx new file mode 100644 index 0000000000..5d1f010740 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityLayout } from './EntityLayout'; +import { + AlertApi, + alertApiRef, + ApiProvider, + ApiRegistry, +} from '@backstage/core'; +import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import { Routes, Route } from 'react-router'; +import { Entity } from '@backstage/catalog-model'; +import { EntityContext } from '../../hooks/useEntity'; +import { catalogApiRef } from '../../plugin'; +import { CatalogApi } from '@backstage/catalog-client'; + +const mockEntityData = { + loading: false, + error: undefined, + entity: { + kind: 'MyKind', + metadata: { + name: 'my-entity', + }, + } as Entity, +}; + +const mockApis = ApiRegistry.with(catalogApiRef, {} as CatalogApi).with( + alertApiRef, + {} as AlertApi, +); + +describe('EntityLayout', () => { + it('renders simplest case', async () => { + const rendered = await renderInTestApp( + + + + +
tabbed-test-content
+
+
+
+
, + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + }); + + it('throws if any other component is a child of TabbedLayout', async () => { + const { error } = await withLogCollector(async () => { + await expect( + renderInTestApp( + + +
tabbed-test-content
+
+
This will cause app to throw
+
, + ), + ).rejects.toThrow(/Child of EntityLayout must be an EntityLayout.Route/); + }); + + expect(error).toEqual([ + expect.stringMatching( + /Child of EntityLayout must be an EntityLayout.Route/, + ), + expect.stringMatching( + /The above error occurred in the component/, + ), + ]); + }); + + it('navigates when user clicks different tab', async () => { + const rendered = await renderInTestApp( + + + + + +
tabbed-test-content
+
+ +
tabbed-test-content-2
+
+
+
+ + } + /> +
, + ); + + const secondTab = rendered.queryAllByRole('tab')[1]; + act(() => { + fireEvent.click(secondTab); + }); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx new file mode 100644 index 0000000000..09b8d02c60 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -0,0 +1,192 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + Children, + Fragment, + PropsWithChildren, + ReactNode, + isValidElement, + useContext, + useState, +} from 'react'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + attachComponentData, + Content, + Header, + HeaderLabel, + Page, + Progress, +} from '@backstage/core'; +import { Box } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { useNavigate } from 'react-router'; +import { EntityContext } from '../../hooks/useEntity'; +import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; +import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; +import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; +import { useEntityCompoundName } from '../useEntityCompoundName'; +import { TabbedLayout } from './TabbedLayout'; + +type SubRoute = { + path: string; + title: string; + children: JSX.Element; +}; + +const Route: (props: SubRoute) => null = () => null; + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +export function createSubRoutesFromChildren(children: ReactNode): SubRoute[] { + return Children.toArray(children).flatMap(child => { + if (!isValidElement(child)) { + return []; + } + + if (child.type === Fragment) { + return createSubRoutesFromChildren(child.props.children); + } + + if (child.type !== Route) { + throw new Error('Child of EntityLayout must be an EntityLayout.Route'); + } + + const { path, title, children } = child.props; + return [{ path, title, children }]; + }); +} + +const EntityLayoutTitle = ({ + entity, + title, +}: { + title: string; + entity: Entity | undefined; +}) => ( + + {title} + {entity && } + +); + +const headerProps = ( + paramKind: string | undefined, + paramNamespace: string | undefined, + paramName: string | undefined, + entity: Entity | undefined, +): { headerTitle: string; headerType: string } => { + const kind = paramKind ?? entity?.kind ?? ''; + const namespace = paramNamespace ?? entity?.metadata.namespace ?? ''; + const name = paramName ?? entity?.metadata.name ?? ''; + return { + headerTitle: `${name}${ + namespace && namespace !== ENTITY_DEFAULT_NAMESPACE + ? ` in ${namespace}` + : '' + }`, + headerType: (() => { + let t = kind.toLowerCase(); + if (entity && entity.spec && 'type' in entity.spec) { + t += ' — '; + t += (entity.spec as { type: string }).type.toLowerCase(); + } + return t; + })(), + }; +}; + +/** + * EntityLayout is a compound component, which allows you to define a layout for + * entities using a sub-navigation mechanism. + * + * Consists of two parts: EntityLayout and EntityLayout.Route + * + * @example + * ```jsx + * + * + *
This is rendered under /example/anything-here route
+ *
+ *
+ * ``` + */ +export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { + const { kind, namespace, name } = useEntityCompoundName(); + const { entity, loading, error } = useContext(EntityContext); + + const routes = createSubRoutesFromChildren(children); + const { headerTitle, headerType } = headerProps( + kind, + namespace, + name, + entity, + ); + + const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); + const navigate = useNavigate(); + const cleanUpAfterRemoval = async () => { + setConfirmationDialogOpen(false); + navigate('/'); + }; + + const showRemovalDialog = () => setConfirmationDialogOpen(true); + + return ( + +
} + pageTitleOverride={headerTitle} + type={headerType} + > + {/* TODO: fix after catalog page customization is added */} + {entity && kind !== 'user' && ( + <> + + + + + )} +
+ + {loading && } + + {entity && } + + {error && ( + + {error.toString()} + + )} + setConfirmationDialogOpen(false)} + /> +
+ ); +}; + +EntityLayout.Route = Route; diff --git a/plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx new file mode 100644 index 0000000000..065265cea4 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TabbedLayout } from './TabbedLayout'; +import { renderInTestApp } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import { Routes, Route } from 'react-router'; + +const testRoute1 = { + path: '', + title: 'tabbed-test-title', + children:
tabbed-test-content
, +}; +const testRoute2 = { + title: 'tabbed-test-title-2', + path: '/some-other-path', + children:
tabbed-test-content-2
, +}; + +describe('TabbedLayout', () => { + it('renders simplest case', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + }); + + it('navigates when user clicks different tab', async () => { + const rendered = await renderInTestApp( + + } + /> + , + ); + + const secondTab = rendered.queryAllByRole('tab')[1]; + act(() => { + fireEvent.click(secondTab); + }); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); + + describe('correctly delegates nested links', () => { + const renderRoute = (route: string) => + renderInTestApp( + + + tabbed-test-content-2 + + tabbed-test-nested-content-2} + /> + + + ), + }, + ]} + /> + } + /> + , + { routeEntries: [route] }, + ); + + it('works for nested content', async () => { + const rendered = await renderRoute('/some-other-path/nested'); + + expect( + rendered.queryByText('tabbed-test-content'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + expect( + rendered.queryByText('tabbed-test-nested-content-2'), + ).toBeInTheDocument(); + }); + + it('works for non-nested content', async () => { + const rendered = await renderRoute('/some-other-path/'); + + expect( + rendered.queryByText('tabbed-test-content'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + expect( + rendered.queryByText('tabbed-test-nested-content-2'), + ).not.toBeInTheDocument(); + }); + }); + + it('shows only one tab contents at a time', async () => { + const rendered = await renderInTestApp( + , + { routeEntries: ['/some-other-path'] }, + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); + + it('redirects to the top level when no route is matching the url', async () => { + const rendered = await renderInTestApp( + , + { routeEntries: ['/non-existing-path'] }, + ); + + expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); + + expect( + rendered.queryByText('tabbed-test-content-2'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx b/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx new file mode 100644 index 0000000000..a2209ba492 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useMemo } from 'react'; +import { useParams, useNavigate, matchRoutes, useRoutes } from 'react-router'; +import { HeaderTabs, Content as LayoutContent } from '@backstage/core'; +import { Helmet } from 'react-helmet'; +import { SubRoute } from './types'; + +export function useSelectedSubRoute( + subRoutes: SubRoute[], +): { index: number; route: SubRoute; element: JSX.Element } { + const params = useParams(); + + const routes = subRoutes.map(({ path, children }) => ({ + caseSensitive: false, + path: `${path}/*`, + element: children, + })); + + const element = useRoutes(routes) ?? subRoutes[0].children; + + const [matchedRoute] = matchRoutes(routes, `/${params['*']}`) ?? []; + const foundIndex = matchedRoute + ? subRoutes.findIndex(t => `${t.path}/*` === matchedRoute.route.path) + : 0; + + return { + index: foundIndex === -1 ? 0 : foundIndex, + element, + route: subRoutes[foundIndex] ?? subRoutes[0], + }; +} + +export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => { + const navigate = useNavigate(); + const { index, route, element } = useSelectedSubRoute(routes); + const headerTabs = useMemo( + () => routes.map(t => ({ id: t.path, label: t.title })), + [routes], + ); + + const onTabChange = (index: number) => + // Remove trailing /* + // And remove leading / for relative navigation + // Note! route resolves relative to the position in the React tree, + // not relative to current location + navigate(routes[index].path.replace(/\/\*$/, '').replace(/^\//, '')); + + return ( + <> + + + + {element} + + + ); +}; diff --git a/plugins/catalog/src/components/EntityLayout/index.ts b/plugins/catalog/src/components/EntityLayout/index.ts new file mode 100644 index 0000000000..2e399765c3 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityLayout } from './EntityLayout'; diff --git a/plugins/catalog/src/components/EntityLayout/types.ts b/plugins/catalog/src/components/EntityLayout/types.ts new file mode 100644 index 0000000000..29ade88dc0 --- /dev/null +++ b/plugins/catalog/src/components/EntityLayout/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SubRoute = { + path: string; + title: string; + children: JSX.Element; +}; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx new file mode 100644 index 0000000000..292cd37ede --- /dev/null +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { EntityContext } from '../../hooks/useEntity'; +import { Entity } from '@backstage/catalog-model'; +import { EntitySwitch } from './EntitySwitch'; +import { isKind } from './conditions'; + +describe('EntitySwitch', () => { + it('should switch child when entity switches', () => { + const content = ( + + + + + + ); + + const rendered = render( + + {content} + , + ); + + expect(rendered.queryByText('A')).toBeInTheDocument(); + expect(rendered.queryByText('B')).not.toBeInTheDocument(); + expect(rendered.queryByText('C')).not.toBeInTheDocument(); + + rendered.rerender( + + {content} + , + ); + + expect(rendered.queryByText('A')).not.toBeInTheDocument(); + expect(rendered.queryByText('B')).toBeInTheDocument(); + expect(rendered.queryByText('C')).not.toBeInTheDocument(); + + rendered.rerender( + + {content} + , + ); + + expect(rendered.queryByText('A')).not.toBeInTheDocument(); + expect(rendered.queryByText('B')).not.toBeInTheDocument(); + expect(rendered.queryByText('C')).toBeInTheDocument(); + }); + + it('should switch child when filters switch', () => { + const entityContextValue = { + entity: { kind: 'component' } as Entity, + loading: false, + error: undefined, + }; + + const rendered = render( + + + + + + , + ); + + expect(rendered.queryByText('A')).toBeInTheDocument(); + expect(rendered.queryByText('B')).not.toBeInTheDocument(); + + rendered.rerender( + + + + + + , + ); + + expect(rendered.queryByText('A')).not.toBeInTheDocument(); + expect(rendered.queryByText('B')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx new file mode 100644 index 0000000000..f36bc29cee --- /dev/null +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ReactNode, + PropsWithChildren, + Children, + Fragment, + useMemo, + isValidElement, +} from 'react'; +import { useEntity } from '../../hooks/useEntity'; +import { Entity } from '@backstage/catalog-model'; + +const EntitySwitchCase = (_: { + if?: (entity: Entity) => boolean; + children: ReactNode; +}) => null; + +type SwitchCase = { + if?: (entity: Entity) => boolean; + children: JSX.Element; +}; + +function createSwitchCasesFromChildren(children: ReactNode): SwitchCase[] { + return Children.toArray(children).flatMap(child => { + if (!isValidElement(child)) { + return []; + } + + if (child.type === Fragment) { + return createSwitchCasesFromChildren(child.props.children); + } + + if (child.type !== EntitySwitchCase) { + throw new Error(`Child of EntitySwitch is not an EntitySwitch.Case`); + } + + const { if: condition, children } = child.props; + return [{ if: condition, children }]; + }); +} + +export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { + const { entity } = useEntity(); + const switchCases = useMemo(() => createSwitchCasesFromChildren(children), [ + children, + ]); + + const matchingCase = switchCases.find(switchCase => + switchCase.if ? switchCase.if(entity) : true, + ); + return matchingCase?.children ?? null; +}; + +EntitySwitch.Case = EntitySwitchCase; diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts new file mode 100644 index 0000000000..8e4da19656 --- /dev/null +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, ComponentEntity } from '@backstage/catalog-model'; + +function strCmp(a: string | undefined, b: string | undefined): boolean { + return Boolean(a && a?.toLowerCase() === b?.toLowerCase()); +} + +export function isKind(kind: string) { + return (entity: Entity) => strCmp(entity?.kind, kind); +} + +export function isComponentType(type: string) { + return (entity: Entity) => { + if (!strCmp(entity?.kind, 'component')) { + return false; + } + const componentEntity = entity as ComponentEntity; + return strCmp(componentEntity.spec.type, type); + }; +} + +export function isNamespace(namespace: string) { + return (entity: Entity) => strCmp(entity?.metadata?.namespace, namespace); +} diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts new file mode 100644 index 0000000000..089f2a108c --- /dev/null +++ b/plugins/catalog/src/components/EntitySwitch/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EntitySwitch } from './EntitySwitch'; +export { isKind, isNamespace, isComponentType } from './conditions'; diff --git a/plugins/catalog/src/extensions.tsx b/plugins/catalog/src/extensions.tsx new file mode 100644 index 0000000000..dcb9ba2aff --- /dev/null +++ b/plugins/catalog/src/extensions.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRoutableExtension } from '@backstage/core'; +import { catalogRouteRef, entityRouteRef } from './routes'; +import { plugin } from './plugin'; + +export const CatalogIndexPage = plugin.provide( + createRoutableExtension({ + component: () => + import('./components/CatalogPage').then(m => m.CatalogPage), + mountPoint: catalogRouteRef, + }), +); + +export const CatalogEntityPage = plugin.provide( + createRoutableExtension({ + component: () => + import('./components/CatalogEntityPage/CatalogEntityPage').then( + m => m.CatalogEntityPage, + ), + mountPoint: entityRouteRef, + }), +); diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 606ad9028d..d8686a18b0 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -17,8 +17,11 @@ export * from '@backstage/catalog-client'; export { AboutCard } from './components/AboutCard'; export { EntityPageLayout } from './components/EntityPageLayout'; +export { EntityLayout } from './components/EntityLayout'; +export * from './components/EntitySwitch'; export { Router } from './components/Router'; export { useEntityCompoundName } from './components/useEntityCompoundName'; export { EntityContext, useEntity } from './hooks/useEntity'; export { catalogApiRef, plugin } from './plugin'; export * from './routes'; +export * from './extensions'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 5f737ec12d..0c2b3b18c5 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -21,6 +21,7 @@ import { createPlugin, discoveryApiRef, } from '@backstage/core'; +import { catalogRouteRef, entityRouteRef } from './routes'; export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', @@ -37,4 +38,8 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), }), ], + routes: { + catalogIndex: catalogRouteRef, + catalogEntity: entityRouteRef, + }, }); diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index a39883e92b..2983b464ac 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -24,11 +24,14 @@ export const rootRoute = createRouteRef({ path: '', title: 'Catalog', }); +export const catalogRouteRef = rootRoute; + export const entityRoute = createRouteRef({ icon: NoIcon, path: ':namespace/:kind/:name/*', title: 'Entity', }); +export const entityRouteRef = entityRoute; // Utility function to get suitable route params for entityRoute, given an // entity instance diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index d52e5d7414..a4aca24f3a 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -50,9 +50,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 80e7404831..41072c9922 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -49,9 +49,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index e664883910..87f5e89a9b 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-cost-insights +## 0.5.3 + +### Patch Changes + +- c02defd57: Make alert url field optional + ## 0.5.2 ### Patch Changes diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 886efa8eb6..dae68a5ce2 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -110,4 +110,4 @@ The CostInsightsApi `getAlerts` method may return any type of alert or recommend The Alert type includes an `element` field to supply the JSX Element that will be rendered in the Cost Insights "Action Items" section; we recommend using Backstage's [InfoCard](https://backstage.io/storybook/?path=/story/layout-information-card--default) and [Recharts](http://recharts.org/en-US/) to show actionable visualizations. -The Alert `url` should link to documentation or instructions for resolving the alert. +The Alert `url` should link to documentation or instructions for resolving the alert. This may be omitted if no external link is needed. diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index d03d698a69..a85862eed7 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/test-utils": "^0.1.5", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -56,9 +56,9 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx new file mode 100644 index 0000000000..54d350298c --- /dev/null +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AlertInsightsSection } from './AlertInsightsSection'; +import { render } from '@testing-library/react'; +import { Alert } from '../../types'; +import { MockScrollProvider } from '../..'; + +const mockAlert: Alert = { + element:
, + subtitle: + 'Wherefore was I to this keen mockery born? When at your hands did I deserve this scorn?', + title: 'Mock alert', + url: '/cost-insights/test', +}; + +describe('', () => { + it('Renders alert without exploding', () => { + const { getByText } = render( + + + , + ); + expect(getByText(mockAlert.title)).toBeInTheDocument(); + expect(getByText(mockAlert.subtitle)).toBeInTheDocument(); + expect(getByText('View Instructions')).toBeInTheDocument(); + }); + + it('Hides instructions button if url is not provided', () => { + const alert = { + ...mockAlert, + url: undefined, + }; + const { queryByText } = render( + + + , + ); + expect(queryByText('View Instructions')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx index 426e943092..7e371c8001 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsightsSection.tsx @@ -34,12 +34,13 @@ export const AlertInsightsSection = ({ subtitle={alert.subtitle} number={number} /> - - - {/* */} - + {alert.url && ( + + + + )} {alert.element} ); diff --git a/plugins/cost-insights/src/types/Alert.ts b/plugins/cost-insights/src/types/Alert.ts index c5c0463d99..18b86c8cb8 100644 --- a/plugins/cost-insights/src/types/Alert.ts +++ b/plugins/cost-insights/src/types/Alert.ts @@ -25,7 +25,7 @@ import { Maybe } from './Maybe'; export type Alert = { title: string; subtitle: string; - url: string; + url?: string; buttonText?: string; // Default: View Instructions element: JSX.Element; }; diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 4ba7d93887..76358cf438 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,9 +42,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/fossa/.eslintrc.js b/plugins/fossa/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/fossa/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/fossa/README.md b/plugins/fossa/README.md new file mode 100644 index 0000000000..489aef90ab --- /dev/null +++ b/plugins/fossa/README.md @@ -0,0 +1,79 @@ +# FOSSA Plugin + +The FOSSA Plugin displays code statistics from [FOSSA](https://fossa.com/). + +![FOSSA Card](./docs/fossa-card.png) + +## Getting Started + +1. Install the FOSSA Plugin: + +```bash +# packages/app + +yarn add @backstage/plugin-fossa +``` + +2. Add plugin to the app: + +```js +// packages/app/src/plugins.ts + +export { plugin as Fossa } from '@backstage/plugin-fossa'; +``` + +3. Add the `FossaCard` to the EntityPage: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { FossaCard } from '@backstage/plugin-fossa'; + +const OverviewContent = ({ entity }: { entity: Entity }) => ( + + // ... + + + + // ... + +); +``` + +4. Add the proxy config: + +```yaml +# app-config.yaml + +proxy: + '/fossa': + target: https://app.fossa.io/api + allowedMethods: ['GET'] + headers: + Authorization: + # Content: 'token ' + $env: FOSSA_AUTH_HEADER + +# if you have a fossa organization, configure your id here +fossa: + organizationId: +``` + +5. Get an api-token and provide `FOSSA_AUTH_HEADER` as env variable (https://app.fossa.com/account/settings/integrations/api_tokens) + +6. Add the `fossa.io/project-name` annotation to your catalog-info.yaml file: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: | + Backstage is an open-source developer portal that puts the developer experience first. + annotations: + fossa.io/project-name: YOUR_PROJECT_NAME +spec: + type: library + owner: CNCF + lifecycle: experimental +``` diff --git a/plugins/fossa/config.d.ts b/plugins/fossa/config.d.ts new file mode 100644 index 0000000000..744a1cb52d --- /dev/null +++ b/plugins/fossa/config.d.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + fossa?: { + /** + * The organization id in fossa. + * @visibility frontend + */ + organizationId: string; + }; +} diff --git a/plugins/fossa/dev/index.tsx b/plugins/fossa/dev/index.tsx new file mode 100644 index 0000000000..7846324b25 --- /dev/null +++ b/plugins/fossa/dev/index.tsx @@ -0,0 +1,133 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createDevApp } from '@backstage/dev-utils'; +import { + Content, + createPlugin, + createRouteRef, + Header, + Page, +} from '@backstage/core'; +import React from 'react'; +import { Grid } from '@material-ui/core'; +import { FossaApi, fossaApiRef } from '../src/api'; +import { FossaCard } from '../src'; +import { Entity } from '@backstage/catalog-model'; +import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/useProjectName'; + +createDevApp() + .registerApi({ + api: fossaApiRef, + deps: {}, + factory: () => + ({ + getFindingSummary: async projectTitle => { + switch (projectTitle) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'zero-deps': + return { + timestamp: new Date().toISOString(), + issueCount: 0, + dependencyCount: 0, + projectDefaultBranch: 'master', + projectUrl: `/#${projectTitle}`, + }; + + case 'issues': + return { + timestamp: new Date().toISOString(), + issueCount: 5, + dependencyCount: 100, + projectDefaultBranch: 'develop', + projectUrl: `/#${projectTitle}`, + }; + + case 'no-issues': + return { + timestamp: new Date().toISOString(), + issueCount: 0, + dependencyCount: 150, + projectDefaultBranch: 'feat/fossa', + projectUrl: `/#${projectTitle}`, + }; + + default: + return undefined; + } + }, + } as FossaApi), + }) + .registerPlugin( + createPlugin({ + id: 'fossa-demo', + register({ router }) { + const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [FOSSA_PROJECT_NAME_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + + const ExamplePage = () => ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + router.addRoute( + createRouteRef({ path: '/', title: 'Fossa' }), + ExamplePage, + ); + }, + }), + ) + .render(); diff --git a/plugins/fossa/docs/fossa-card.png b/plugins/fossa/docs/fossa-card.png new file mode 100644 index 0000000000..a3b7f59294 Binary files /dev/null and b/plugins/fossa/docs/fossa-card.png differ diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json new file mode 100644 index 0000000000..820092c241 --- /dev/null +++ b/plugins/fossa/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-fossa", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/fossa" + }, + "keywords": [ + "backstage", + "fossa" + ], + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.6.0", + "@backstage/core": "^0.4.2", + "@backstage/theme": "^0.2.2", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "cross-fetch": "^3.0.6", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.4.3", + "@backstage/dev-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.6", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/fossa/src/api/FossaApi.ts b/plugins/fossa/src/api/FossaApi.ts new file mode 100644 index 0000000000..84406e1b1a --- /dev/null +++ b/plugins/fossa/src/api/FossaApi.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; + +export interface FindingSummary { + timestamp: string; + issueCount: number; + dependencyCount: number; + projectDefaultBranch: string; + projectUrl: string; +} + +export const fossaApiRef = createApiRef({ + id: 'plugin.fossa.service', + description: 'Used by the Fossa plugin to make requests', +}); + +export type FossaApi = { + getFindingSummary(projectTitle: string): Promise; +}; diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts new file mode 100644 index 0000000000..c01dd09327 --- /dev/null +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UrlPatternDiscovery } from '@backstage/core'; +import { msw } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { FindingSummary, FossaApi, FossaClient } from './index'; + +const server = setupServer(); + +describe('FossaClient', () => { + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + let client: FossaApi; + + beforeEach(() => { + client = new FossaClient({ discoveryApi, organizationId: '8736' }); + }); + + it('should report finding summary', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'count=1&title=our-service&organizationId=8736', + ); + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + default_branch: 'develop', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_licensing_issue_count: 5, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + timestamp: '2020-01-01T00:00:00Z', + issueCount: 5, + dependencyCount: 160, + projectDefaultBranch: 'develop', + projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service', + } as FindingSummary); + }); + + it('should report finding summary without licensing_issue_count', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'count=1&title=our-service&organizationId=8736', + ); + return res( + ctx.json([ + { + locator: 'custom+8736/our-service', + default_branch: 'refs/master', + revisions: [ + { + updatedAt: '2020-01-01T00:00:00Z', + dependency_count: 160, + unresolved_issue_count: 100, + }, + ], + }, + ]), + ); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + timestamp: '2020-01-01T00:00:00Z', + issueCount: 100, + dependencyCount: 160, + projectDefaultBranch: 'refs/master', + projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service', + } as FindingSummary); + }); + + it('should skip organizationId', async () => { + client = new FossaClient({ discoveryApi }); + + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'count=1&title=our-service', + ); + return res(ctx.status(404)); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toBeUndefined(); + }); + + it('should handle 404 status', async () => { + server.use( + rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'count=1&title=our-service&organizationId=8736', + ); + return res(ctx.status(404)); + }), + ); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toBeUndefined(); + }); +}); diff --git a/plugins/fossa/src/api/FossaClient.ts b/plugins/fossa/src/api/FossaClient.ts new file mode 100644 index 0000000000..10f688393b --- /dev/null +++ b/plugins/fossa/src/api/FossaClient.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi } from '@backstage/core'; +import fetch from 'cross-fetch'; +import { FindingSummary, FossaApi } from './FossaApi'; + +export class FossaClient implements FossaApi { + discoveryApi: DiscoveryApi; + organizationId?: string; + + constructor({ + discoveryApi, + organizationId, + }: { + discoveryApi: DiscoveryApi; + organizationId?: string; + }) { + this.discoveryApi = discoveryApi; + this.organizationId = organizationId; + } + + private async callApi(path: string): Promise { + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`; + const response = await fetch(`${apiUrl}/${path}`); + if (response.status === 200) { + return await response.json(); + } + return undefined; + } + + async getFindingSummary( + projectTitle: string, + ): Promise { + const project = await this.callApi( + `projects?count=1&title=${projectTitle}${ + this.organizationId ? `&organizationId=${this.organizationId}` : '' + }`, + ); + if (!project) { + return undefined; + } + + const revision = project[0].revisions[0]; + return { + timestamp: revision.updatedAt, + issueCount: + revision.unresolved_licensing_issue_count || + revision.unresolved_issue_count, + dependencyCount: revision.dependency_count, + projectDefaultBranch: project[0].default_branch, + projectUrl: `https://app.fossa.com/projects/${encodeURIComponent( + project[0].locator, + )}`, + }; + } +} diff --git a/plugins/fossa/src/api/index.ts b/plugins/fossa/src/api/index.ts new file mode 100644 index 0000000000..f31c4bd1d5 --- /dev/null +++ b/plugins/fossa/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { FossaApi, FindingSummary } from './FossaApi'; +export { fossaApiRef } from './FossaApi'; +export { FossaClient } from './FossaClient'; diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.tsx new file mode 100644 index 0000000000..d1ac06d365 --- /dev/null +++ b/plugins/fossa/src/components/FossaCard/FossaCard.tsx @@ -0,0 +1,178 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + EmptyState, + InfoCard, + MissingAnnotationEmptyState, + Progress, + useApi, +} from '@backstage/core'; +import { useAsync } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; +import { fossaApiRef } from '../../api'; +import { makeStyles } from '@material-ui/core/styles'; +import { + FOSSA_PROJECT_NAME_ANNOTATION, + useProjectName, +} from '../useProjectName'; +import { Grid, Tooltip } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + numberError: { + fontSize: '5rem', + textAlign: 'center', + fontWeight: theme.typography.fontWeightMedium, + margin: theme.spacing(2, 0), + color: theme.palette.error.main, + }, + numberSuccess: { + fontSize: '5rem', + textAlign: 'center', + fontWeight: theme.typography.fontWeightMedium, + margin: theme.spacing(2, 0), + color: theme.palette.success.main, + }, + description: { + fontSize: '1rem', + textAlign: 'center', + fontWeight: theme.typography.fontWeightMedium, + color: theme.palette.text.secondary, + }, + disabled: { + backgroundColor: theme.palette.background.default, + }, + lastAnalyzed: { + color: theme.palette.text.secondary, + textAlign: 'center', + }, + branch: { + textDecoration: 'underline dotted', + }, +})); + +export const FossaCard = ({ + entity, + variant = 'gridItem', +}: { + entity: Entity; + variant?: string; +}) => { + const fossaApi = useApi(fossaApiRef); + + const projectTitle = useProjectName(entity); + + const { value, loading } = useAsync( + async () => + projectTitle ? fossaApi.getFindingSummary(projectTitle) : undefined, + [fossaApi, projectTitle], + ); + + const deepLink = value + ? { + title: 'View more', + link: value.projectUrl, + } + : undefined; + + const classes = useStyles(); + + return ( + <> + + {loading && } + + {!loading && !projectTitle && ( + + )} + + {!loading && projectTitle && !value && ( + + )} + + {value && ( + + +

0 || value.dependencyCount === 0 + ? classes.numberError + : classes.numberSuccess + } + > + {value.issueCount} +

+ {value.dependencyCount > 0 && ( +

Number of issues

+ )} + {value.dependencyCount === 0 && ( +

+ No Dependencies. +
+ Please check your FOSSA project settings. +

+ )} +
+ + + Last analyzed on{' '} + {new Date(value.timestamp).toLocaleString('en-US', { + timeZone: 'UTC', + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + })} + + + Based on {value.dependencyCount} Dependencies on branch{' '} + + + {value.projectDefaultBranch} + + + . + +
+ )} +
+ + ); +}; diff --git a/plugins/fossa/src/components/FossaCard/index.ts b/plugins/fossa/src/components/FossaCard/index.ts new file mode 100644 index 0000000000..5f84660bf0 --- /dev/null +++ b/plugins/fossa/src/components/FossaCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { FossaCard } from './FossaCard'; diff --git a/plugins/fossa/src/components/index.ts b/plugins/fossa/src/components/index.ts new file mode 100644 index 0000000000..a49c71603b --- /dev/null +++ b/plugins/fossa/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FossaCard'; diff --git a/plugins/fossa/src/components/useProjectName.ts b/plugins/fossa/src/components/useProjectName.ts new file mode 100644 index 0000000000..88379ca3fa --- /dev/null +++ b/plugins/fossa/src/components/useProjectName.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +export const FOSSA_PROJECT_NAME_ANNOTATION = 'fossa.io/project-name'; + +export const useProjectName = (entity: Entity): string | undefined => { + return ( + entity?.metadata.annotations?.[FOSSA_PROJECT_NAME_ANNOTATION] ?? undefined + ); +}; diff --git a/plugins/fossa/src/index.ts b/plugins/fossa/src/index.ts new file mode 100644 index 0000000000..c35c534122 --- /dev/null +++ b/plugins/fossa/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { plugin } from './plugin'; +export * from './components'; diff --git a/plugins/fossa/src/plugin.test.ts b/plugins/fossa/src/plugin.test.ts new file mode 100644 index 0000000000..4f9b00a02b --- /dev/null +++ b/plugins/fossa/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { plugin } from './plugin'; + +describe('fossa', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/fossa/src/plugin.ts b/plugins/fossa/src/plugin.ts new file mode 100644 index 0000000000..d1fe621018 --- /dev/null +++ b/plugins/fossa/src/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + configApiRef, + createApiFactory, + createPlugin, + discoveryApiRef, +} from '@backstage/core'; +import { fossaApiRef, FossaClient } from './api'; + +export const plugin = createPlugin({ + id: 'fossa', + apis: [ + createApiFactory({ + api: fossaApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new FossaClient({ + discoveryApi, + organizationId: configApi.getOptionalString('fossa.organizationId'), + }), + }), + ], +}); diff --git a/plugins/fossa/src/setupTests.ts b/plugins/fossa/src/setupTests.ts new file mode 100644 index 0000000000..825bcd4115 --- /dev/null +++ b/plugins/fossa/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 9de4a38d1d..4fe8613e46 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index d51c9bd489..2d69b1499d 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-github-actions +## 0.2.6 + +### Patch Changes + +- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json` +- Updated dependencies [d681db2b5] +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] +- Updated dependencies [1dc445e89] + - @backstage/core-api@0.2.7 + - @backstage/core@0.4.2 + - @backstage/plugin-catalog@0.2.8 + ## 0.2.5 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index cfa30bfd77..a5612568e7 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", - "@backstage/core-api": "^0.2.6", - "@backstage/plugin-catalog": "^0.2.7", + "@backstage/core": "^0.4.2", + "@backstage/core-api": "^0.2.7", + "@backstage/plugin-catalog": "^0.2.8", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,9 +51,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 86a7befc77..a92956d6fa 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,9 +42,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index b14db4e8bb..537e178b6f 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphiql +## 0.2.3 + +### Patch Changes + +- 1dc445e89: Update to use new plugin extension API +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] + - @backstage/core@0.4.2 + ## 0.2.2 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index af30bc4b12..310dc96de1 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.2", + "version": "0.2.3", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 4c3799d550..d42282d738 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -46,9 +46,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 649df8353f..144786d58c 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-kubernetes-backend": "^0.2.3", "@backstage/theme": "^0.2.2", "@kubernetes/client-node": "^0.12.1", @@ -46,9 +46,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 93c4d53c21..a567410290 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.6.0", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/core-api": "^0.2.6", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", @@ -47,9 +47,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 2984bba457..e5c1c70ba2 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/package.json b/plugins/org/package.json index c81b44756e..a8d8f908fb 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -33,9 +33,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index efcd8fbe37..3e967101a7 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-pagerduty +## 0.2.4 + +### Patch Changes + +- 342270e4d: Create AboutCard in core and use it in pagerduty and catalog plugin +- Updated dependencies [1dc445e89] +- Updated dependencies [342270e4d] + - @backstage/core@0.4.2 + ## 0.2.3 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index aa6da07a02..6ee51ad1e9 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,9 +44,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/register-component/README.md b/plugins/register-component/README.md index d324b3941f..0abf27590f 100644 --- a/plugins/register-component/README.md +++ b/plugins/register-component/README.md @@ -1,5 +1,7 @@ # Register component plugin +> This plugin is deprecated in favor of [`@backstage/catalog-import`](https://github.com/backstage/backstage/tree/master/plugins/catalog-import), and will be soon removed from the project. + Welcome to the register-component plugin! This plugin allows you to submit your Backstage component using your software's YAML config. diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index bca520b5bc..dcf981365f 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.6.0", - "@backstage/core": "^0.4.1", + "@backstage/core": "^0.4.2", "@backstage/plugin-catalog": "^0.2.7", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -45,9 +45,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.2", + "@backstage/cli": "^0.4.3", "@backstage/dev-utils": "^0.1.6", - "@backstage/test-utils": "^0.1.5", + "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/register-component/src/components/Router.tsx b/plugins/register-component/src/components/Router.tsx index eee7fd3806..16aa1fa695 100644 --- a/plugins/register-component/src/components/Router.tsx +++ b/plugins/register-component/src/components/Router.tsx @@ -20,6 +20,13 @@ import { RouteRef } from '@backstage/core'; // As we don't know which path the catalog's router mounted on // We need to inject this from the app + +/** + * Provides a router for registering a component. + * + * @deprecated The router for this component is deprecated and replaced with the `catalog-import` plugin. + * @see https://github.com/backstage/backstage/tree/master/plugins/catalog-import + */ export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => ( [0-9]+) from backstage\/changeset-release\/master\n\nVersion Packages$/; + +// Initialize a GitHub client +const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, +}); + +// Get the message of the commit responsible for a tag +async function getCommitMessageUsingTagName(tagName) { + // Get the tag SHA using the provided tag name + const refData = await octokit.git.getRef({ + owner: GH_OWNER, + repo: GH_REPO, + ref: `tags/${tagName}`, + }); + if (refData.status !== 200) { + console.error('refData:'); + console.error(refData); + throw new Error( + 'Something went wrong when getting the tag SHA using tag name', + ); + } + const tagSha = refData.data.object.sha; + console.log(`SHA for the tag ${TAG_NAME} is ${tagSha}`); + + // Get the commit SHA using the tag SHA + const tagData = await octokit.git.getTag({ + owner: GH_REPO, + repo: GH_REPO, + tag_sha: tagSha, + }); + if (tagData.status !== 200) { + console.error('tagData:'); + console.error(tagData); + throw new Error( + 'Something went wrong when getting the commit SHA using tag SHA', + ); + } + const commitSha = tagData.data.object.sha; + console.log( + `The commit for the tag is https://github.com/backstage/backstage/commit/${commitSha}`, + ); + + // Get the commit message using the commit SHA + const commitData = await octokit.git.getCommit({ + owner: GH_OWNER, + repo: GH_REPO, + commit_sha: commitSha, + }); + if (commitData.status !== 200) { + console.error('commitData:'); + console.error(commitData); + throw new Error( + 'Something went wrong when getting the commit message using commit SHA', + ); + } + + // Example Commit Message + // Merge pull request #3555 from backstage/changeset-release/master Version Packages + return commitData.data.message; +} + +// There is a PR number in our expected commit message. Get the description of that PR. +async function getPrDescriptionFromCommitMessage(commitMessage) { + // It should exactly match the pattern of changeset commit message, or else will abort. + const expectedMessage = RegExp(EXPECTED_COMMIT_MESSAGE); + if (!expectedMessage.test(commitMessage)) { + throw new Error( + `Expected regex did not match commit message: ${commitMessage}`, + ); + } + + // Get the PR description from the commit message + const prNumber = commitMessage.match(expectedMessage).groups.prNumber; + console.log( + `Identified the changeset Pull request - https://github.com/backstage/backstage/pull/${prNumber}`, + ); + + const prData = await octokit.pulls.get({ + owner: GH_OWNER, + repo: GH_REPO, + pull_number: prNumber, + }); + + return prData.data.body; +} + +// Use the PR description to prepare for the release description +async function prepareReleaseDescription(prDescription) { + // TODO: Refine prDescription to remove the lines containing "Update Dependencies" + // Remove everything in the beginning until changelogs. + return prDescription.split('\n').slice(3).join('\n'); +} + +// Create Release on GitHub. +async function createRelease(releaseDescription) { + // Create draft release if BOOL_CREATE_RELEASE is undefined + // Publish release if BOOL_CREATE_RELEASE is not undefined + const boolCreateDraft = !BOOL_CREATE_RELEASE; + + const releaseResponse = await octokit.repos.createRelease({ + owner: GH_REPO, + repo: GH_REPO, + tag_name: TAG_NAME, + name: TAG_NAME, + body: releaseDescription, + draft: boolCreateDraft, + prerelease: false, + }); + + if (releaseResponse.status === 201) { + if (boolCreateDraft) { + console.log('Created draft release! Click Publish to notify users.'); + } else { + console.log('Published release!'); + } + console.log(releaseResponse.data.html_url); + } else { + console.error(releaseResponse); + throw new Error('Something went wrong when creating the release.'); + } +} + +async function main() { + const commitMessage = await getCommitMessageUsingTagName(TAG_NAME); + const prDescription = await getPrDescriptionFromCommitMessage(commitMessage); + const releaseDescription = await prepareReleaseDescription(prDescription); + + await createRelease(releaseDescription); +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/yarn.lock b/yarn.lock index 43da6893ed..528c653b38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6365,9 +6365,9 @@ integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== "@types/tar@^4.0.3": - version "4.0.3" - resolved "https://registry.npmjs.org/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489" - integrity sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA== + version "4.0.4" + resolved "https://registry.npmjs.org/@types/tar/-/tar-4.0.4.tgz#d680de60855e7778a51c672b755869a3b8d2889f" + integrity sha512-0Xv+xcmkTsOZdIF4yCnd7RkOOyfyqPaqJ7RZFKnwdxfDbkN3eAAE9sHl8zJFqBz4VhxolW9EErbjR1oyH7jK2A== dependencies: "@types/minipass" "*" "@types/node" "*" @@ -13641,7 +13641,7 @@ graphql@^14.5.3: growly@^1.3.0: version "1.3.0" - resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= gtoken@^5.0.4: @@ -14728,9 +14728,9 @@ is-directory@^0.3.1: integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= is-docker@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" - integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== is-dom@^1.0.9, is-dom@^1.1.0: version "1.1.0" @@ -15107,7 +15107,7 @@ isarray@^2.0.5: isexe@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isobject@^2.0.0: @@ -16870,7 +16870,7 @@ lru-cache@^5.0.0, lru-cache@^5.1.1: lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" @@ -17996,9 +17996,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" - integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== + version "8.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" + integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== dependencies: growly "^1.3.0" is-wsl "^2.2.0" @@ -22021,9 +22021,11 @@ semver@7.0.0: integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== semver@7.x, semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" @@ -22215,7 +22217,7 @@ shelljs@^0.8.2, shelljs@^0.8.4: shellwords@^0.1.1: version "0.1.1" - resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== shortid@^2.2.14: @@ -24536,9 +24538,9 @@ uuid@^7.0.3: integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache@^2.0.3: version "2.1.0" @@ -25334,7 +25336,7 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: yallist@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml-ast-parser@0.0.43, yaml-ast-parser@^0.0.43: