diff --git a/.changeset/beige-cobras-camp.md b/.changeset/beige-cobras-camp.md new file mode 100644 index 0000000000..56345db805 --- /dev/null +++ b/.changeset/beige-cobras-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': patch +--- + +Link to GitHub using host defined in app config if defined. diff --git a/.changeset/curvy-poems-cough.md b/.changeset/curvy-poems-cough.md new file mode 100644 index 0000000000..6013404490 --- /dev/null +++ b/.changeset/curvy-poems-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka': patch +--- + +Added lag column in the plugin main table diff --git a/.changeset/honest-steaks-beam.md b/.changeset/honest-steaks-beam.md new file mode 100644 index 0000000000..71c3193c99 --- /dev/null +++ b/.changeset/honest-steaks-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Add parseLocationReference, stringifyLocationReference diff --git a/.changeset/little-crabs-burn.md b/.changeset/little-crabs-burn.md new file mode 100644 index 0000000000..2bc3fe9e96 --- /dev/null +++ b/.changeset/little-crabs-burn.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-client': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/techdocs-common': patch +--- + +Make use of parseLocationReference/stringifyLocationReference diff --git a/.changeset/lucky-schools-bathe.md b/.changeset/lucky-schools-bathe.md new file mode 100644 index 0000000000..3134c8c05e --- /dev/null +++ b/.changeset/lucky-schools-bathe.md @@ -0,0 +1,6 @@ +--- +'@backstage/integration': patch +--- + +Add `resolveEditUrl` to integrations to resolve a URL that can be used to edit +a file in the web interfaces of an SCM. diff --git a/.changeset/many-eyes-rest.md b/.changeset/many-eyes-rest.md new file mode 100644 index 0000000000..d7a5a806b2 --- /dev/null +++ b/.changeset/many-eyes-rest.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixes task failures caused by undefined step input diff --git a/.changeset/nasty-garlics-explain.md b/.changeset/nasty-garlics-explain.md new file mode 100644 index 0000000000..815bda91ae --- /dev/null +++ b/.changeset/nasty-garlics-explain.md @@ -0,0 +1,18 @@ +--- +'@backstage/backend-common': patch +--- + +Bump to the latest version of the Knex library. + +You will most likely want to bump your own `packages/backend/package.json` as well: + +```diff +- "knex": "^0.21.18", ++ "knex": "^0.95.1", +``` + +Note that the recent versions of the Knex library have some changes that may affect your internal plugins' database migration files. Importantly, they now support `ALTER TABLE` on SQLite, and no longer accidentally remove indices when making some modifications. It now also exports the `Knex` typescript type as a named export. + +```ts +import { Knex } from 'knex'; +``` diff --git a/.changeset/nine-comics-smash.md b/.changeset/nine-comics-smash.md new file mode 100644 index 0000000000..147944930d --- /dev/null +++ b/.changeset/nine-comics-smash.md @@ -0,0 +1,7 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs': patch +--- + +OpenStack Swift publisher added for tech-docs. diff --git a/.changeset/old-laws-begin.md b/.changeset/old-laws-begin.md new file mode 100644 index 0000000000..42a31105e1 --- /dev/null +++ b/.changeset/old-laws-begin.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Bump to the latest version of the Knex library. diff --git a/.changeset/rotten-beds-itch.md b/.changeset/rotten-beds-itch.md new file mode 100644 index 0000000000..c046e71572 --- /dev/null +++ b/.changeset/rotten-beds-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Show a Not Found message when navigating to a nonexistent entity diff --git a/.changeset/serious-deers-laugh.md b/.changeset/serious-deers-laugh.md new file mode 100644 index 0000000000..1c17242f30 --- /dev/null +++ b/.changeset/serious-deers-laugh.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': minor +--- + +Show workflow name diff --git a/.changeset/shaggy-islands-mix.md b/.changeset/shaggy-islands-mix.md new file mode 100644 index 0000000000..a8f3ac6088 --- /dev/null +++ b/.changeset/shaggy-islands-mix.md @@ -0,0 +1,48 @@ +--- +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-kubernetes-backend': minor +--- + +Restructure configuration; Add GKE cluster locator + +Config migration + +1. `kubernetes.clusters` is now at `kubernetes.clusterLocatorMethods[].clusters` when the `clusterLocatorMethod` is of `type: 'config''` +2. `kubernetes.serviceLocatorMethod` is now an object. `multiTenant` is the only valid `type` currently + +Old config example: + +```yaml +kubernetes: + serviceLocatorMethod: 'multiTenant' + clusterLocatorMethods: + - 'config' + clusters: + - url: http://127.0.0.1:9999 + name: minikube + authProvider: 'serviceAccount' + serviceAccountToken: + $env: K8S_MINIKUBE_TOKEN + - url: http://127.0.0.2:9999 + name: aws-cluster-1 + authProvider: 'aws' +``` + +New config example: + +```yaml +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: + - url: http://127.0.0.1:9999 + name: minikube + authProvider: 'serviceAccount' + serviceAccountToken: + $env: K8S_MINIKUBE_TOKEN + - url: http://127.0.0.2:9999 + name: aws-cluster-1 + authProvider: 'aws' +``` diff --git a/.changeset/silly-deers-pay.md b/.changeset/silly-deers-pay.md new file mode 100644 index 0000000000..0b7521b5ad --- /dev/null +++ b/.changeset/silly-deers-pay.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +--- + +Move logic for generating URLs for the view, edit and source links of catalog +entities from the catalog frontend into the backend. This is done using the +existing support for the `backstage.io/view-url`, `backstage.io/edit-url` and +`backstage.io/source-location` annotations that are now filled by the +`AnnotateLocationEntityProcessor`. If these annotations are missing or empty, +the UI disables the related controls. diff --git a/.changeset/slow-jobs-live.md b/.changeset/slow-jobs-live.md new file mode 100644 index 0000000000..dd640a54f3 --- /dev/null +++ b/.changeset/slow-jobs-live.md @@ -0,0 +1,6 @@ +--- +'@backstage/core': patch +'@backstage/theme': patch +--- + +Deprecated `ItemCard`. Added `ItemCardGrid` and `ItemCardHeader` instead, that can be used to compose functionality around regular Material-UI `Card` components instead. diff --git a/.changeset/smart-waves-dream.md b/.changeset/smart-waves-dream.md new file mode 100644 index 0000000000..648493f2df --- /dev/null +++ b/.changeset/smart-waves-dream.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-react': patch +--- + +Standardize the tool cards in explore some more diff --git a/.changeset/strong-wasps-watch.md b/.changeset/strong-wasps-watch.md new file mode 100644 index 0000000000..44923d5335 --- /dev/null +++ b/.changeset/strong-wasps-watch.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-explore': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch +--- + +Make use of the new core `ItemCardGrid` and `ItemCardHeader` instead of the deprecated `ItemCard`. diff --git a/.changeset/techdocs-curvy-geckos-rhyme.md b/.changeset/techdocs-curvy-geckos-rhyme.md new file mode 100644 index 0000000000..a0a53fc127 --- /dev/null +++ b/.changeset/techdocs-curvy-geckos-rhyme.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +- Adds a link to the owner entity +- Corrects the link to the component which includes the namespace diff --git a/.changeset/techdocs-kind-seals-fix.md b/.changeset/techdocs-kind-seals-fix.md new file mode 100644 index 0000000000..b0ac18c744 --- /dev/null +++ b/.changeset/techdocs-kind-seals-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Enhanced the example documented-component to better demonstrate TechDocs features diff --git a/.changeset/techdocs-metal-trees-push.md b/.changeset/techdocs-metal-trees-push.md new file mode 100644 index 0000000000..f6a88c404e --- /dev/null +++ b/.changeset/techdocs-metal-trees-push.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +--- + +- Improve deprecation warning messaging in logs. +- Replace temp folder path from git provider domain(`source`) to full git host name (`resource`). (See: https://github.com/IonicaBizau/git-url-parse#giturlparseurl) diff --git a/.changeset/techdocs-old-guests-add.md b/.changeset/techdocs-old-guests-add.md new file mode 100644 index 0000000000..e4f991a1b6 --- /dev/null +++ b/.changeset/techdocs-old-guests-add.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Refactor log messaging to improve clarity diff --git a/.changeset/tidy-kangaroos-guess.md b/.changeset/tidy-kangaroos-guess.md new file mode 100644 index 0000000000..019d259736 --- /dev/null +++ b/.changeset/tidy-kangaroos-guess.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs': patch +--- + +Add an optional configuration option for setting the url endpoint for AWS S3 publisher: `techdocs.publisher.awsS3.endpoint` diff --git a/.changeset/violet-queens-crash.md b/.changeset/violet-queens-crash.md new file mode 100644 index 0000000000..1065e33cd5 --- /dev/null +++ b/.changeset/violet-queens-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for most TypeScript 4.1 syntax. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d68af23491..402617b66d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,3 +11,4 @@ - [ ] Added or updated documentation - [ ] Tests for new functionality and regression tests for bug fixes - [ ] Screenshots attached (for UI changes) +- [ ] All your commits have a `Signed-off-by` line in the message. ([more info](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#developer-certificate-of-origin)) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index e60d9b9528..3593bd7bad 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -19,6 +19,7 @@ Docusaurus Dominik Ek Env +Erdogan Expedia Figma Firekube @@ -51,6 +52,7 @@ Namespaces Niklas OAuth Okta +Oksuz Oldsberg Olle Onboarding @@ -63,6 +65,7 @@ Protobuf Proxying Raghunandan Readme +rebase Recharts Redash Repo @@ -86,6 +89,7 @@ Templaters Thauer Tolerations Tuite +Trendyol Voi WWW Wealthsimple diff --git a/ADOPTERS.md b/ADOPTERS.md index e1539372ab..9f2dbf3c06 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,23 +1,24 @@ -| Organization | Contact | Description of Use | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| Organization | Contact | Description of Use | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56404b2c81..1b7433888c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,12 @@ Awesome commit message Signed-off-by: Jane Smith jane.smith@example.com ``` +- In case you forgot to add it to the most recent commit, use `git commit --amend --signoff` +- In case you forgot to add it to the last N commits in your branch, use `git rebase --signoff HEAD~N` and replace N with the number of new commits you created in your branch. +- If you have a very deep branch with a lot of commits, run `git rebase -i --signoff $(git merge-base -a master HEAD)`, double check to make sense of the commits (keep all lines as `pick`) and save and close the editor. This should bulk sign all the commits in your PR. Do be careful though. If you have a complex flow with a lot of branching and re-merging of work branches and stuff, merge-base may not be the right solution for you. + +Note: If you have already pushed you branch to a remote, you might have to force push: `git push -f` after the rebase. + ## Creating Changesets We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. They help us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy, it helps when contributors include changesets with their pull requests. diff --git a/README.md b/README.md index d01be3f54c..6dd490e43b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how ## Community - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project -- [Good First Issues](https://github.com/backstage/backstage/contribute) - Start here if you want to contribute +- [Contributing to Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - Start here if you want to contribute - [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the technical direction - [FAQ](https://backstage.io/docs/FAQ) - Frequently Asked Questions - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll diff --git a/app-config.yaml b/app-config.yaml index f68f5a30a2..465c7b0049 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -94,7 +94,7 @@ techdocs: generators: techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage'. Read documentation for using alternatives. + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives. sentry: organization: my-company @@ -108,10 +108,11 @@ lighthouse: baseUrl: http://localhost:3003 kubernetes: - serviceLocatorMethod: 'multiTenant' + serviceLocatorMethod: + type: 'multiTenant' clusterLocatorMethods: - - 'config' - clusters: [] + - type: 'config' + clusters: [] kafka: clientId: backstage @@ -207,7 +208,7 @@ catalog: # groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') locations: - # Add a location here to ingest it, for example from an URL: + # Add a location here to ingest it, for example from a URL: # # - type: url # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index f146eebe55..ab48456a3b 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -17,18 +17,22 @@ The following is a full example entry in `app-config.yaml`: ```yaml kubernetes: - serviceLocatorMethod: 'multiTenant' + serviceLocatorMethod: + type: 'multiTenant' clusterLocatorMethods: - - 'config' - clusters: - - url: http://127.0.0.1:9999 - name: minikube - authProvider: 'serviceAccount' - serviceAccountToken: - $env: K8S_MINIKUBE_TOKEN - - url: http://127.0.0.2:9999 - name: gke-cluster-1 - authProvider: 'google' + - type: 'config' + clusters: + - url: http://127.0.0.1:9999 + name: minikube + authProvider: 'serviceAccount' + serviceAccountToken: + $env: K8S_MINIKUBE_TOKEN + - url: http://127.0.0.2:9999 + name: aws-cluster-1 + authProvider: 'aws' + - type: 'gke' + projectId: 'gke-clusters' + region: 'europe-west1' ``` ### `serviceLocatorMethod` @@ -44,26 +48,28 @@ Currently, the only valid value is: This is an array used to determine where to retrieve cluster configuration from. -Currently, the only valid cluster locator method is: +Valid cluster locator methods are: -- `config` - This cluster locator method will read cluster information from your - app-config (see below). +#### `config` -### `clusters` +This cluster locator method will read cluster information from your app-config +(see below). + +##### `clusters` Used by the `config` cluster locator method to construct Kubernetes clients. -### `clusters.\*.url` +##### `clusters.\*.url` The base URL to the Kubernetes control plane. Can be found by using the "Kubernetes master" result from running the `kubectl cluster-info` command. -### `clusters.\*.name` +##### `clusters.\*.name` A name to represent this cluster, this must be unique within the `clusters` array. Users will see this value in the Service Catalog Kubernetes plugin. -### `clusters.\*.authProvider` +##### `clusters.\*.authProvider` This determines how the Kubernetes client authenticates with the Kubernetes cluster. Valid values are: @@ -73,7 +79,7 @@ cluster. Valid values are: | `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | | `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | -### `clusters.\*.serviceAccountToken` (optional) +##### `clusters.\*.serviceAccountToken` (optional) The service account token to be used when using the `serviceAccount` auth provider. You could get the service account token with: @@ -85,6 +91,38 @@ kubectl -n get secret $(kubectl -n get sa { @@ -35,7 +35,7 @@ export const createNewFileAction = () => { title: 'Contents', description: 'The contents of the file', }, - contents: { + filename: { type: 'string', title: 'Filename', description: 'The filename of the file that will be created', @@ -46,7 +46,7 @@ export const createNewFileAction = () => { async handler(ctx) { await fs.outputFile( `${ctx.workspacePath}/${ctx.input.filename}`, - ctx.input.content, + ctx.input.contents, ); }, }); diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 2fa48f57b0..7025364f78 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -54,6 +54,7 @@ providers are used. | Google Cloud Storage (GCS) | Yes ✅ | | Amazon Web Services (AWS) S3 | Yes ✅ | | Azure Blob Storage | Yes ✅ | +| OpenStack Swift | Yes ✅ | [Reach out to us](#feedback) if you want to request more platforms. diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 20e436d8a4..9bf2c25926 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -76,6 +76,12 @@ techdocs: region: $env: AWS_REGION + # (Optional) Endpoint URI to send requests to. + # If not set, the default endpoint is built from the configured region. + # https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property + endpoint: + $env: AWS_ENDPOINT + # Required when techdocs.publisher.type is set to 'azureBlobStorage'. Skip otherwise. azureBlobStorage: diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index cd4b058644..fd8f320859 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -257,7 +257,8 @@ techdocs: **3a. (Recommended) Authentication using environment variable** -Set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in +If you do not prefer (3a) and optionally like to use a service account, you can +set the config `techdocs.publisher.azureBlobStorage.credentials.accountName` in your `app-config.yaml` to the your account name. The storage blob client will automatically use the environment variable @@ -309,3 +310,77 @@ and read the static generated documentation files. When you start the backend of the app, you should be able to see `techdocs info Successfully connected to the Azure Blob Storage container` in the logs. + +## Configuring OpenStack Swift Container with TechDocs + +Follow the +[official OpenStack Api documentation](https://docs.openstack.org/api-ref/identity/v3/) +for the latest instructions on the following steps involving OpenStack Storage. + +**1. Set `techdocs.publisher.type` config in your `app-config.yaml`** + +Set `techdocs.publisher.type` to `'openStackSwift'`. + +```yaml +techdocs: + publisher: + type: 'openStackSwift' +``` + +**2. Create an OpenStack Swift Storage Container** + +Create a dedicated container for TechDocs sites. +[Refer to the official documentation](https://docs.openstack.org/mitaka/user-guide/dashboard_manage_containers.html). + +TechDocs will publish documentation to this container and will fetch files from +here to serve documentation in Backstage. Note that the container names are +globally unique. + +Set the config `techdocs.publisher.openStackSwift.containerName` in your +`app-config.yaml` to the name of the container you just created. + +```yaml +techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-container' +``` + +**3. Authentication using app-config.yaml** + +Set the configs in your `app-config.yaml` to point to your container name. + +https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization +for more details. + +```yaml +techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + userName: + $env: OPENSTACK_SWIFT_STORAGE_USERNAME + password: + $env: OPENSTACK_SWIFT_STORAGE_PASSWORD + authUrl: + $env: OPENSTACK_SWIFT_STORAGE_AUTH_URL + keystoneAuthVersion: + $env: OPENSTACK_SWIFT_STORAGE_AUTH_VERSION + domainId: + $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_ID + domainName: + $env: OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME + region: + $env: OPENSTACK_SWIFT_STORAGE_REGION +``` + +**4. That's it!** + +Your Backstage app is now ready to use OpenStack Swift Storage for TechDocs, to +store and read the static generated documentation files. When you start the +backend of the app, you should be able to see +`techdocs info Successfully connected to the OpenStack Swift Storage container` +in the logs. diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 64b9e772d1..cf7806a5d2 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -4,20 +4,30 @@ title: Configuring App with plugins description: Documentation on How Configuring App with plugins --- +Backstage plugins customize the app for your needs. There is a +[plugin marketplace](https://backstage.io/plugins) with plugins for many common +infrastructure needs - CI/CD, monitoring, auditing, and more. + ## Adding existing plugins to your app -The following steps assume that you have created a new Backstage app and want to -add an existing plugin to it. We are using the +The following steps assume that you have +[created a Backstage app](./create-an-app.md) and want to add an existing plugin +to it. We are using the [CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md) plugin in this example. 1. Add the plugin's npm package to the repo: ```bash -yarn add @backstage/plugin-circleci +yarn workspace app add @backstage/plugin-circleci ``` -2. Add the plugin itself: +Note the plugin is added to the `app` package, rather than the root +package.json. Backstage Apps are set up as monorepos with +[yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/). Since +CircleCI is a frontend UI plugin, it goes in `app` rather than `backend`. + +2. Add the plugin itself to the App: ```js // packages/app/src/plugins.ts diff --git a/docs/getting-started/deployment-docker.md b/docs/getting-started/deployment-docker.md index 69158f9104..37a48f5457 100644 --- a/docs/getting-started/deployment-docker.md +++ b/docs/getting-started/deployment-docker.md @@ -27,9 +27,9 @@ The required steps in the host build are to install dependencies with `yarn install`, generate type definitions using `yarn tsc`, and build all packages with `yarn build`. -> NOTE: Using `yarn build` to build packages and bundle the backend assumes that -> you have migrated to using `backstage-cli backend:bundle` as your build script -> in the backend package. +> NOTE: If you created your app prior to 2021-02-18, follow the +> [migration step](https://github.com/backstage/backstage/releases/tag/release-2021-02-18) +> to move from `backend:build` to `backend:bundle`. In a CI workflow it might look something like this: @@ -43,22 +43,10 @@ yarn tsc yarn build ``` -Once the host build is complete, we are ready to build our image. We use the -following `Dockerfile`, which is also included when creating a new app with -`@backstage/create-app`: +Once the host build is complete, we are ready to build our image. The following +`Dockerfile` is included when creating a new app with `@backstage/create-app`: ```Dockerfile -# This dockerfile builds an image for the backend package. -# It should be executed with the root of the repo as docker context. -# -# Before building this image, be sure to have run the following commands in the repo root: -# -# yarn install -# yarn tsc -# yarn build -# -# Once the commands have been run, you can build the image using `yarn build-image` - FROM node:14-buster-slim WORKDIR /app @@ -78,15 +66,15 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml"] For more details on how the `backend:bundle` command and the `skeleton.tar.gz` file works, see the -[`backend:bundle` command docs](../cli/commands.md#backendbundle) +[`backend:bundle` command docs](../cli/commands.md#backendbundle). -The `Dockerfile` is typically placed at `packages/backend/Dockerfile`, but needs -to be executed with the root of the repo as the build context, in order to get -access to the root `yarn.lock` and `package.json`, along with any other files -that might be needed, such as `.npmrc`. +The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be +executed with the root of the repo as the build context, in order to get access +to the root `yarn.lock` and `package.json`, along with any other files that +might be needed, such as `.npmrc`. -In order to speed up the build we can significantly reduce the build context -size using the following `.dockerignore` in the root of the repo: +The `@backstage/create-app` command adds the following `.dockerignore` in the +root of the repo to speed up the build by reducing build context size: ```text .git @@ -96,9 +84,9 @@ packages plugins ``` -With the project build and the `.dockerignore` and `Dockerfile` in place, we are -now ready to build the final image. Assuming we're at the root of the repo, we -execute the build like this: +With the project built and the `.dockerignore` and `Dockerfile` in place, we are +now ready to build the final image. From the root of the repo, execute the +build: ```bash docker image build . -f packages/backend/Dockerfile --tag backstage diff --git a/package.json b/package.json index 527304b4f3..3a1ed1ed8d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "husky": "^4.2.3", "lerna": "^4.0.0", "lint-staged": "^10.1.0", - "prettier": "^2.0.5", + "prettier": "^2.2.1", "recursive-readdir": "^2.2.2", "shx": "^0.3.2" }, diff --git a/packages/app/package.json b/packages/app/package.json index a9d19ab98a..e0dd0e5a2a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -60,7 +60,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/jquery": "^3.3.34", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/react-dom": "^16.9.8", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 77e5c01e19..9a665141c0 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -66,7 +66,7 @@ <% } %> - +
+ * + * + * @deprecated Use plain MUI and composable helpers instead. + * @see https://material-ui.com/components/cards/ + */ export const ItemCard = ({ description, tags, @@ -63,43 +72,33 @@ export const ItemCard = ({ onClick, href, }: ItemCardProps) => { - const classes = useStyles(); - return ( -
- {(subtitle || type) && ( - {subtitle ?? type} + + + + + {tags?.length ? ( + + {tags.map((tag, i) => ( + + ))} + + ) : null} + {description} + + + {!href && ( + )} - {title} -
-
- {tags?.map((tag, i) => ( - - ))} - 0 && classes.withTags, - )} - > - {description} - -
- {!href && ( - - )} - {href && ( - - )} -
-
+ {href && ( + + )} +
); }; diff --git a/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx b/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx new file mode 100644 index 0000000000..51ef8c544f --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardGrid.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { Card } from '@material-ui/core'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { ItemCardGrid } from './ItemCardGrid'; + +describe('', () => { + it('renders default without exploding', async () => { + await renderInTestApp( + + Hello! + , + ); + expect(screen.getByRole('grid')).toBeInTheDocument(); + expect(screen.getByText('Hello!')).toBeInTheDocument(); + }); + + it('renders custom styles', async () => { + await renderInTestApp( + <> + + Hello! + + + Goodbye! + + , + ); + expect(screen.getAllByRole('grid')[0]).toHaveStyle({ + gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', + }); + expect(screen.getAllByRole('grid')[1]).toHaveClass('my-css-class'); + }); +}); diff --git a/packages/core/src/layout/ItemCard/ItemCardGrid.tsx b/packages/core/src/layout/ItemCard/ItemCardGrid.tsx new file mode 100644 index 0000000000..551c3c67d5 --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardGrid.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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 { createStyles, makeStyles, Theme, WithStyles } from '@material-ui/core'; +import React from 'react'; + +const styles = (theme: Theme) => + createStyles({ + root: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', + gridAutoRows: '1fr', + gridGap: theme.spacing(2), + }, + }); + +const useStyles = makeStyles(styles); + +export type ItemCardGridProps = Partial> & { + /** + * The Card items of the grid. + */ + children?: React.ReactNode; +}; + +/** + * A default grid to use when arranging "item cards" - cards that let users + * select among several options. + * + * The immediate children are expected to be MUI Card components. + * + * Styles for the grid can be overridden using the `classes` prop, e.g.: + * + * + * + * + * + * This can be useful for e.g. overriding gridTemplateColumns to adapt the + * minimum size of the cells to fit the content better. + */ +export const ItemCardGrid = (props: ItemCardGridProps) => { + const { children, ...otherProps } = props; + const classes = useStyles(otherProps); + return ( +
+ {children} +
+ ); +}; diff --git a/packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx b/packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx new file mode 100644 index 0000000000..ec9af6436e --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardHeader.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { Card, CardMedia } from '@material-ui/core'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { ItemCardHeader } from './ItemCardHeader'; + +describe('', () => { + it('renders default without exploding', async () => { + await renderInTestApp( + + + + + , + ); + expect(screen.getByText('My Title')).toBeInTheDocument(); + expect(screen.getByText('My Subtitle')).toBeInTheDocument(); + }); + + it('renders custom children', async () => { + await renderInTestApp( + + + My Custom Text + + , + ); + expect(screen.getByText('My Title')).toBeInTheDocument(); + expect(screen.getByText('My Custom Text')).toBeInTheDocument(); + }); + + it('renders custom styles', async () => { + await renderInTestApp( + + + + My Custom Text + + + , + ); + expect(screen.getByText('My Custom Text')).toHaveClass('my-css-class'); + }); +}); diff --git a/packages/core/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core/src/layout/ItemCard/ItemCardHeader.tsx new file mode 100644 index 0000000000..17c4a19e58 --- /dev/null +++ b/packages/core/src/layout/ItemCard/ItemCardHeader.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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 { + createStyles, + makeStyles, + Theme, + Typography, + WithStyles, +} from '@material-ui/core'; +import React from 'react'; + +const styles = (theme: Theme) => + createStyles({ + root: { + color: theme.palette.common.white, + padding: theme.spacing(2, 2, 3), + backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + backgroundPosition: 0, + backgroundSize: 'inherit', + }, + }); + +const useStyles = makeStyles(styles); + +export type ItemCardHeaderProps = Partial> & { + /** + * A large title to show in the header, providing the main heading. + * + * Use this if you want to have the default styling and placement of a title. + */ + title?: React.ReactNode; + /** + * A slightly smaller title to show in the header, providing additional + * details. + * + * Use this if you want to have the default styling and placement of a + * subtitle. + */ + subtitle?: React.ReactNode; + /** + * Custom children to draw in the header. + * + * If the title and/or subtitle were specified, the children are drawn below + * those. + */ + children?: React.ReactNode; +}; + +/** + * A simple card header, rendering a default look for "item cards" - cards that + * are arranged in a grid for users to select among several options. + * + * This component expects to be placed within a MUI . + * + * Styles for the header can be overridden using the `classes` prop, e.g.: + * + * + * + * + */ +export const ItemCardHeader = (props: ItemCardHeaderProps) => { + const { title, subtitle, children } = props; + const classes = useStyles(props); + return ( +
+ {subtitle && {subtitle}} + {title && {title}} + {children} +
+ ); +}; diff --git a/packages/core/src/layout/ItemCard/index.ts b/packages/core/src/layout/ItemCard/index.ts index b38dd2acc2..da2c1dd546 100644 --- a/packages/core/src/layout/ItemCard/index.ts +++ b/packages/core/src/layout/ItemCard/index.ts @@ -15,3 +15,7 @@ */ export { ItemCard } from './ItemCard'; +export { ItemCardGrid } from './ItemCardGrid'; +export type { ItemCardGridProps } from './ItemCardGrid'; +export { ItemCardHeader } from './ItemCardHeader'; +export type { ItemCardHeaderProps } from './ItemCardHeader'; diff --git a/packages/core/src/layout/Sidebar/Intro.tsx b/packages/core/src/layout/Sidebar/Intro.tsx index d0af0c3633..b95719c05f 100644 --- a/packages/core/src/layout/Sidebar/Intro.tsx +++ b/packages/core/src/layout/Sidebar/Intro.tsx @@ -133,9 +133,10 @@ export const SidebarIntro = () => { starredItemsDismissed: false, recentlyViewedItemsDismissed: false, }; - const [dismissedIntro, setDismissedIntro] = useLocalStorage< - SidebarIntroLocalStorage - >(SIDEBAR_INTRO_LOCAL_STORAGE); + const [ + dismissedIntro, + setDismissedIntro, + ] = useLocalStorage(SIDEBAR_INTRO_LOCAL_STORAGE); const { starredItemsDismissed, recentlyViewedItemsDismissed } = dismissedIntro ?? {}; diff --git a/packages/create-app/package.json b/packages/create-app/package.json index ba4963f652..ba69da9c8c 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -56,7 +56,6 @@ "@backstage/plugin-catalog": "*", "@backstage/plugin-catalog-backend": "*", "@backstage/plugin-catalog-import": "*", - "@backstage/plugin-circleci": "*", "@backstage/plugin-explore": "*", "@backstage/plugin-github-actions": "*", "@backstage/plugin-lighthouse": "*", diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index c7561fe091..10701d65eb 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -15,7 +15,6 @@ "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", - "@backstage/plugin-circleci": "^{{version '@backstage/plugin-circleci'}}", "@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}", "@backstage/plugin-github-actions": "^{{version '@backstage/plugin-github-actions'}}", "@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}", @@ -34,7 +33,7 @@ "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/react-dom": "^16.9.8", "cross-env": "^7.0.0", "cypress": "^4.2.0", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index a3a3715ab0..de6ab2b6b5 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -17,9 +17,9 @@ import { ApiEntity, Entity } from '@backstage/catalog-model'; import { WarningPanel } from '@backstage/core'; import { ApiDefinitionCard, - ConsumedApisCard, - ConsumingComponentsCard, - ProvidedApisCard, + ConsumedApisCard, + ConsumingComponentsCard, + ProvidedApisCard, ProvidingComponentsCard } from '@backstage/plugin-api-docs'; import { @@ -28,9 +28,6 @@ import { import { useEntity } from '@backstage/plugin-catalog-react'; -import { - isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter -} from '@backstage/plugin-circleci'; import { isPluginApplicableToEntity as isGitHubActionsAvailable, Router as GitHubActionsRouter } from '@backstage/plugin-github-actions'; @@ -45,8 +42,6 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { switch (true) { case isGitHubActionsAvailable(entity): return ; - case isCircleCIAvailable(entity): - return ; default: return ( diff --git a/packages/create-app/templates/default-app/packages/app/src/plugins.ts b/packages/create-app/templates/default-app/packages/app/src/plugins.ts index 28b42d5be2..df53885723 100644 --- a/packages/create-app/templates/default-app/packages/app/src/plugins.ts +++ b/packages/create-app/templates/default-app/packages/app/src/plugins.ts @@ -1,7 +1,6 @@ export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; export { plugin as CatalogImport } from '@backstage/plugin-catalog-import'; -export { plugin as Circleci } from '@backstage/plugin-circleci'; export { plugin as GithubActions } from '@backstage/plugin-github-actions'; export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs'; diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index f17da4c1be..d511d015ea 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -49,7 +49,7 @@ "devDependencies": { "@backstage/cli": "^0.6.3", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0" + "@types/node": "^14.14.32" }, "files": [ "dist" diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 67486710f3..bb00620967 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@types/fs-extra": "^9.0.1", "@types/github-slugger": "^1.3.0", - "@types/node": "^13.7.2", + "@types/node": "^14.14.32", "nodemon": "^2.0.2" }, "files": [ diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 4327d7161c..6cd740c7b7 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@backstage/cli-common": "^0.1.1", "@types/fs-extra": "^9.0.1", - "@types/node": "^13.7.2", + "@types/node": "^14.14.32", "chalk": "^4.0.0", "commander": "^6.1.0", "cross-fetch": "^3.0.6", diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index 44e427a5d0..749af97422 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -88,4 +88,10 @@ describe('ScmIntegrations', () => { }), ).toBe('https://absolute.com/path'); }); + + it('can resolveEditUrl using fallback', () => { + expect(i.resolveEditUrl('http://example.com/x/a.yaml')).toBe( + 'http://example.com/x/a.yaml', + ); + }); }); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index dddf6ed5d0..c3de569990 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -91,4 +91,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry { return integration.resolveUrl(options); } + + resolveEditUrl(url: string): string { + const integration = this.byUrl(url); + if (!integration) { + return url; + } + + return integration.resolveEditUrl(url); + } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 3f9e165b90..122a06cfe3 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -99,4 +99,18 @@ describe('AzureIntegration', () => { ).toBe('https://dev.azure.com/organization/project/test'); }); }); + + it('resolve edit URL', () => { + const integration = new AzureIntegration({ host: 'h.com' } as any); + + // TODO: The Azure integration doesn't support resolving an edit URL yet, + // instead we keep the input URL. + expect( + integration.resolveEditUrl( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml', + ), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml', + ); + }); }); diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index 1413b7b343..870c8053e4 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -15,7 +15,7 @@ */ import parseGitUrl from 'git-url-parse'; -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, isValidUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; @@ -53,12 +53,8 @@ export class AzureIntegration implements ScmIntegration { const { url, base } = options; // If we can parse the url, it is absolute - then return it verbatim - try { - // eslint-disable-next-line no-new - new URL(url); + if (isValidUrl(url)) { return url; - } catch { - // Ignore intentionally - looks like a relative path } const parsed = parseGitUrl(base); @@ -78,4 +74,10 @@ export class AzureIntegration implements ScmIntegration { return newUrl.toString(); } + + resolveEditUrl(url: string): string { + // TODO: Implement edit URL for Azure, fallback to view url as I don't know + // how azure works. + return url; + } } diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 3f130a393c..51b49f6374 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -44,4 +44,16 @@ describe('BitbucketIntegration', () => { expect(integration.type).toBe('bitbucket'); expect(integration.title).toBe('h.com'); }); + + it('resolve edit URL', () => { + const integration = new BitbucketIntegration({ host: 'h.com' } as any); + + expect( + integration.resolveEditUrl( + 'https://bitbucket.org/my-owner/my-project/src/master/README.md', + ), + ).toBe( + 'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master', + ); + }); }); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index 10b69877e4..0b529f6305 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import parseGitUrl from 'git-url-parse'; import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { @@ -51,4 +52,16 @@ export class BitbucketIntegration implements ScmIntegration { resolveUrl(options: { url: string; base: string }): string { return defaultScmResolveUrl(options); } + + resolveEditUrl(url: string): string { + const urlData = parseGitUrl(url); + const editUrl = new URL(url); + + editUrl.searchParams.set('mode', 'edit'); + // TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it + // but this is taken over from the initial implementation. + editUrl.searchParams.set('spa', '0'); + editUrl.searchParams.set('at', urlData.ref); + return editUrl.toString(); + } } diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index 9056517f32..3bb4b4d3fc 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitHubIntegration } from './GitHubIntegration'; +import { GitHubIntegration, replaceUrlType } from './GitHubIntegration'; describe('GitHubIntegration', () => { it('has a working factory', () => { @@ -49,4 +49,60 @@ describe('GitHubIntegration', () => { expect(integration.title).toBe('h.com'); expect(integration.config.host).toBe('h.com'); }); + + it('resolveUrl', () => { + const integration = new GitHubIntegration({ host: 'h.com' }); + + expect( + integration.resolveUrl({ + url: '../a.yaml', + base: + 'https://github.com/backstage/backstage/blob/master/test/README.md', + }), + ).toBe('https://github.com/backstage/backstage/tree/master/a.yaml'); + + expect( + integration.resolveUrl({ + url: './', + base: + 'https://github.com/backstage/backstage/blob/master/test/README.md', + }), + ).toBe('https://github.com/backstage/backstage/tree/master/test/'); + }); + + it('resolve edit URL', () => { + const integration = new GitHubIntegration({ host: 'h.com' }); + + expect( + integration.resolveEditUrl( + 'https://github.com/backstage/backstage/blob/master/README.md', + ), + ).toBe('https://github.com/backstage/backstage/edit/master/README.md'); + }); +}); + +describe('replaceUrlType', () => { + it('should replace with expected type', () => { + expect( + replaceUrlType( + 'https://github.com/backstage/backstage/blob/master/README.md', + 'edit', + ), + ).toBe('https://github.com/backstage/backstage/edit/master/README.md'); + expect( + replaceUrlType( + 'https://github.com/webmodules/blob/blob/master/test', + 'tree', + ), + ).toBe('https://github.com/webmodules/blob/tree/master/test'); + expect( + replaceUrlType('https://github.com/blob/blob/blob/master/test', 'tree'), + ).toBe('https://github.com/blob/blob/tree/master/test'); + expect( + replaceUrlType( + 'https://github.com/backstage/backstage/edit/tree/README.md', + 'blob', + ), + ).toBe('https://github.com/backstage/backstage/blob/tree/README.md'); + }); }); diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index c60ab462c9..34bebebd9e 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -47,6 +47,25 @@ export class GitHubIntegration implements ScmIntegration { } resolveUrl(options: { url: string; base: string }): string { - return defaultScmResolveUrl(options); + // GitHub uses blob URLs for files and tree urls for directory listings. But + // there is a redirect from tree to blob for files, so we can always return + // tree urls here. + return replaceUrlType(defaultScmResolveUrl(options), 'tree'); + } + + resolveEditUrl(url: string): string { + return replaceUrlType(url, 'edit'); } } + +export function replaceUrlType( + url: string, + type: 'blob' | 'tree' | 'edit', +): string { + return url.replace( + /\/\/([^/]+)\/([^/]+)\/([^/]+)\/(blob|tree|edit)\//, + (_, host, owner, repo) => { + return `//${host}/${owner}/${repo}/${type}/`; + }, + ); +} diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 6faae3a5e7..5cc6d410e3 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitLabIntegration } from './GitLabIntegration'; +import { GitLabIntegration, replaceUrlType } from './GitLabIntegration'; describe('GitLabIntegration', () => { it('has a working factory', () => { @@ -43,4 +43,43 @@ describe('GitLabIntegration', () => { expect(integration.type).toBe('gitlab'); expect(integration.title).toBe('h.com'); }); + + it('resolve edit URL', () => { + const integration = new GitLabIntegration({ host: 'h.com' } as any); + + expect( + integration.resolveEditUrl( + 'https://gitlab.com/my-org/my-project/-/blob/develop/README.md', + ), + ).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md'); + }); +}); + +describe('replaceUrlType', () => { + it('should replace with expected type', () => { + expect( + replaceUrlType( + 'https://gitlab.com/my-org/my-project/-/blob/develop/README.md', + 'edit', + ), + ).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md'); + expect( + replaceUrlType( + 'https://gitlab.com/webmodules/blob/-/blob/develop/test', + 'tree', + ), + ).toBe('https://gitlab.com/webmodules/blob/-/tree/develop/test'); + expect( + replaceUrlType( + 'https://gitlab.com/blob/blob/-/blob/develop/test', + 'tree', + ), + ).toBe('https://gitlab.com/blob/blob/-/tree/develop/test'); + expect( + replaceUrlType( + 'https://gitlab.com/blob/blob/-/edit/develop/README.md', + 'tree', + ), + ).toBe('https://gitlab.com/blob/blob/-/tree/develop/README.md'); + }); }); diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 131825edb5..2fd9f54ffe 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -49,4 +49,15 @@ export class GitLabIntegration implements ScmIntegration { resolveUrl(options: { url: string; base: string }): string { return defaultScmResolveUrl(options); } + + resolveEditUrl(url: string): string { + return replaceUrlType(url, 'edit'); + } +} + +export function replaceUrlType( + url: string, + type: 'blob' | 'tree' | 'edit', +): string { + return url.replace(/\/\-\/(blob|tree|edit)\//, `/-/${type}/`); } diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index c63a323dc3..4aeaa34f7e 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -51,6 +51,19 @@ export interface ScmIntegration { * @param options.base The base URL onto which this resolution happens */ resolveUrl(options: { url: string; base: string }): string; + + /** + * Resolves the edit URL for a file within the SCM system. + * + * Most SCM systems have a web interface that allows viewing and editing files + * in the repository. The returned URL directly jumps into the edit mode for + * the file. + * If this is not possible, the integration can fall back to a URL to view + * the file in the web interface. + * + * @param url The absolute URL to the file that should be edited. + */ + resolveEditUrl(url: string): string; } /** @@ -103,6 +116,19 @@ export interface ScmIntegrationRegistry * @param options.base The base URL onto which this resolution happens */ resolveUrl(options: { url: string; base: string }): string; + + /** + * Resolves the edit URL for a file within the SCM system. + * + * Most SCM systems have a web interface that allows viewing and editing files + * in the repository. The returned URL directly jumps into the edit mode for + * the file. + * If this is not possible, the integration can fall back to a URL to view + * the file in the web interface. + * + * @param url The absolute URL to the file that should be edited. + */ + resolveEditUrl(url: string): string; } export type ScmIntegrationsFactory = (options: { diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts new file mode 100644 index 0000000000..5d9f81cda0 --- /dev/null +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -0,0 +1,109 @@ +/* + * 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 fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; +import { EventEmitter } from 'events'; + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const checkFileExists = async (Key: string): Promise => { + // Key will always have / as file separator irrespective of OS since cloud providers expects /. + // Normalize Key to OS specific path before checking if file exists. + const filePath = path.join(rootDir, Key); + + try { + fs.accessSync(filePath, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } +}; + +class PkgCloudStorageClient { + getFile( + containerName: string, + file: string, + callback: (err: any, file: string) => any, + ) { + checkFileExists(file).then(res => { + if (!res) { + callback('File does not exist', file); + throw new Error('File does not exist'); + } else { + callback(undefined, 'success'); + } + }); + } + + getContainer( + containerName: string, + callback: (err: string, container: string) => any, + ) { + if (containerName !== 'mock') { + callback('Container does not exist', containerName); + throw new Error('Container does not exist'); + } else { + callback('Container does not exist', 'success'); + } + } + + upload({ remote }: { remote: string }) { + const filePath = path.join(rootDir, remote); + + const emitter = new EventEmitter(); + + process.nextTick(() => { + if (fs.existsSync(filePath)) { + emitter.emit('success'); + (emitter as any).end = () => true; + } else { + emitter.emit( + 'error', + new Error(`The file ${filePath} does not exist !`), + ); + } + }); + + return emitter; + } + + download({ remote }: { remote: string }) { + const filePath = path.join(rootDir, remote); + + const emitter = new EventEmitter(); + + process.nextTick(() => { + if (fs.existsSync(filePath)) { + emitter.emit('data', Buffer.from(fs.readFileSync(filePath))); + emitter.emit('end'); + } else { + emitter.emit( + 'error', + new Error(`The file ${filePath} does not exist !`), + ); + } + }); + + return emitter; + } +} + +export class storage { + static createClient() { + return new PkgCloudStorageClient(); + } +} diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 914a9a728d..8e3c28e9ea 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -56,6 +56,7 @@ "mime-types": "^2.1.27", "mock-fs": "^4.13.0", "p-limit": "^3.1.0", + "pkgcloud": "^2.2.0", "recursive-readdir": "^2.2.2", "winston": "^3.2.1" }, @@ -66,6 +67,7 @@ "@types/js-yaml": "^3.12.5", "@types/mime-types": "^2.1.0", "@types/mock-fs": "^4.13.0", + "@types/pkgcloud": "^1.7.4", "@types/recursive-readdir": "^2.2.0" }, "jest": { diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index 6520f0f8e4..c31b1266fa 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -105,7 +105,7 @@ describe('parseReferenceAnnotation', () => { 'backstage.io/techdocs-ref', mockEntityWithBadAnnotation, ); - }).toThrow(/Failure to parse/); + }).toThrow(/Unable to parse/); }); }); diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index ec58bb7b40..856624c568 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -15,7 +15,7 @@ */ import { Git, InputError, UrlReader } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, parseLocationReference } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import fs from 'fs-extra'; import parseGitUrl from 'git-url-parse'; @@ -36,28 +36,15 @@ export const parseReferenceAnnotation = ( entity: Entity, ): ParsedLocationAnnotation => { const annotation = entity.metadata.annotations?.[annotationName]; - if (!annotation) { throw new InputError( `No location annotation provided in entity: ${entity.metadata.name}`, ); } - // split on the first colon for the protocol and the rest after the first split - // is the location. - const [type, target] = annotation.split(/:(.+)/) as [ - RemoteProtocol?, - string?, - ]; - - if (!type || !target) { - throw new InputError( - `Failure to parse either protocol or location for entity: ${entity.metadata.name}`, - ); - } - + const { type, target } = parseLocationReference(annotation); return { - type, + type: type as RemoteProtocol, target, }; }; @@ -77,8 +64,9 @@ export const getLocationForEntity = ( case 'url': return { type, target }; case 'dir': - if (path.isAbsolute(target)) return { type, target }; - + if (path.isAbsolute(target)) { + return { type, target }; + } return parseReferenceAnnotation( 'backstage.io/managed-by-location', entity, @@ -107,7 +95,7 @@ export const getGitRepositoryTempFolder = async ( // fs.realpathSync fixes a problem with macOS returning a path that is a symlink fs.realpathSync(os.tmpdir()), 'backstage-repo', - parsedGitLocation.source, + parsedGitLocation.resource, parsedGitLocation.owner, parsedGitLocation.name, parsedGitLocation.ref, @@ -216,12 +204,12 @@ export const getDocFilesFromRepository = async ( entity, ); - opts?.logger?.info(`Reading files from ${target}`); + opts?.logger?.debug(`Reading files from ${target}`); // readTree will throw NotModifiedError if etag has not changed. const readTreeResponse = await reader.readTree(target, { etag: opts?.etag }); const preparedDir = await readTreeResponse.dir(); - opts?.logger?.info(`Tree downloaded and stored at ${preparedDir}`); + opts?.logger?.debug(`Tree downloaded and stored at ${preparedDir}`); return { preparedDir, diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index a88ed13231..2035eb8da9 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -252,7 +252,7 @@ export const patchMkdocsYmlPreBuild = async ( mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); } catch (error) { logger.warn( - `Could not read file ${mkdocsYmlPath} before running the generator. ${error.message}`, + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, ); return; } diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.ts b/packages/techdocs-common/src/stages/prepare/commonGit.ts index 46b96675f3..c38f5ced9d 100644 --- a/packages/techdocs-common/src/stages/prepare/commonGit.ts +++ b/packages/techdocs-common/src/stages/prepare/commonGit.ts @@ -40,9 +40,9 @@ export class CommonGitPreparer implements PreparerBase { options?: { etag?: string }, ): Promise { this.logger.warn( - 'You are using the legacy git preparer in TechDocs which will be removed in near future (March 2021). ' + - 'Migrate to URL reader by updating `backstage.io/techdocs-ref` annotation in `catalog-info.yaml` ' + - 'to be prefixed with `url:`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 ', + `You are using the legacy git preparer in TechDocs for \`${entity.metadata.name}\` which will be removed in near future (March 2021). ` + + `Migrate to URL reader by updating \`backstage.io/techdocs-ref\` annotation in \`catalog-info.yaml\` ` + + `to be prefixed with \`url:\`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 `, ); const { target } = parseReferenceAnnotation( diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index a1abd5899d..cf950da72f 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -94,11 +94,16 @@ export class AwsS3Publish implements PublisherBase { // or AWS shared credentials file at ~/.aws/credentials will be used. const region = config.getOptionalString('techdocs.publisher.awsS3.region'); + // AWS endpoint is an optional config. If missing, the default endpoint is built from + // the configured region. + const endpoint = config.getOptionalString( + 'techdocs.publisher.awsS3.endpoint', + ); + const storageClient = new aws.S3({ credentials, - ...(region && { - region, - }), + ...(region && { region }), + ...(endpoint && { endpoint }), }); // Check if the defined bucket exists. Being able to connect means the configuration is good diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts new file mode 100644 index 0000000000..b831012acf --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -0,0 +1,248 @@ +/* + * 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, + EntityName, + ENTITY_DEFAULT_NAMESPACE, +} from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import mockFs from 'mock-fs'; +import os from 'os'; +import path from 'path'; +import * as winston from 'winston'; +import { OpenStackSwiftPublish } from './openStackSwift'; +import { PublisherBase, TechDocsMetadata } from './types'; + +// NOTE: /packages/techdocs-common/__mocks__ is being used to mock pkgcloud client library + +const createMockEntity = (annotations = {}): Entity => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + namespace: 'test-namespace', + annotations: { + ...annotations, + }, + }, + }; +}; + +const createMockEntityName = (): EntityName => ({ + kind: 'TestKind', + name: 'test-component-name', + namespace: 'test-namespace', +}); + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const getEntityRootDir = (entity: Entity) => { + const { + kind, + metadata: { namespace, name }, + } = entity; + + return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); +}; + +const logger = winston.createLogger(); +jest.spyOn(logger, 'info').mockReturnValue(logger); +jest.spyOn(logger, 'error').mockReturnValue(logger); + +let publisher: PublisherBase; + +beforeEach(() => { + mockFs.restore(); + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'openStackSwift', + openStackSwift: { + credentials: { + username: 'mockuser', + password: 'verystrongpass', + }, + authUrl: 'mockauthurl', + region: 'mockregion', + containerName: 'mock', + }, + }, + }, + }); + + publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger); +}); + +describe('OpenStackSwiftPublish', () => { + describe('publish', () => { + beforeEach(() => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should publish a directory', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined(); + }); + + it('should fail to publish a directory', async () => { + expect.assertions(3); + const wrongPathToGeneratedDirectory = path.join( + rootDir, + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); + + const entity = createMockEntity(); + await expect( + publisher.publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }), + ).rejects.toThrowError(); + + await publisher + .publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }) + .catch(error => { + expect(error.message).toEqual( + // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error + // Issue reported https://github.com/tschaub/mock-fs/issues/118 + expect.stringContaining( + `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`, + ), + ); + expect(error.message).toEqual( + expect.stringContaining(wrongPathToGeneratedDirectory), + ); + }); + mockFs.restore(); + }); + }); + + describe('hasDocsBeenGenerated', () => { + it('should return true if docs has been generated', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': 'file-content', + }, + }); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); + mockFs.restore(); + }); + + it('should return false if docs has not been generated', async () => { + const entity = createMockEntity(); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false); + }); + }); + + describe('fetchTechDocsMetadata', () => { + it('should return tech docs metadata', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`, + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return an error if the techdocs_metadata.json file is not present', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + await publisher + .fetchTechDocsMetadata(entityNameMock) + .catch(error => + expect(error).toEqual( + new Error( + `TechDocs metadata fetch failed, The file ${path.join( + entityRootDir, + 'techdocs_metadata.json', + )} does not exist !`, + ), + ), + ); + }); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts new file mode 100644 index 0000000000..0e60bc5796 --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -0,0 +1,263 @@ +/* + * 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, EntityName } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { storage } from 'pkgcloud'; +import express from 'express'; +import fs from 'fs-extra'; +import JSON5 from 'json5'; +import createLimiter from 'p-limit'; +import path from 'path'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; +import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; + +const streamToBuffer = (stream: Readable): Promise => { + return new Promise((resolve, reject) => { + try { + const chunks: any[] = []; + stream.on('data', chunk => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks))); + } catch (e) { + throw new Error(`Unable to parse the response data ${e.message}`); + } + }); +}; + +export class OpenStackSwiftPublish implements PublisherBase { + static fromConfig(config: Config, logger: Logger): PublisherBase { + let containerName = ''; + try { + containerName = config.getString( + 'techdocs.publisher.openStackSwift.containerName', + ); + } catch (error) { + throw new Error( + "Since techdocs.publisher.type is set to 'openStackSwift' in your app config, " + + 'techdocs.publisher.openStackSwift.containerName is required.', + ); + } + + const openStackSwiftConfig = config.getConfig( + 'techdocs.publisher.openStackSwift', + ); + + const storageClient = storage.createClient({ + provider: 'openstack', + username: openStackSwiftConfig.getString('credentials.username'), + password: openStackSwiftConfig.getString('credentials.password'), + authUrl: openStackSwiftConfig.getString('authUrl'), + keystoneAuthVersion: + openStackSwiftConfig.getOptionalString('keystoneAuthVersion') || 'v3', + domainId: openStackSwiftConfig.getOptionalString('domainId') || 'default', + domainName: + openStackSwiftConfig.getOptionalString('domainName') || 'Default', + region: openStackSwiftConfig.getString('region'), + }); + + // Check if the defined container exists. Being able to connect means the configuration is good + // and the storage client will work. + storageClient.getContainer(containerName, (err, container) => { + if (container) { + logger.info( + `Successfully connected to the OpenStack Swift container ${containerName}.`, + ); + } else { + logger.error( + `Could not retrieve metadata about the OpenStack Swift container ${containerName}. ` + + 'Make sure the container exists. Also make sure that authentication is setup either by ' + + 'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' + + 'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage', + ); + + logger.error(`from OpenStack client library: ${err.message}`); + } + }); + + return new OpenStackSwiftPublish(storageClient, containerName, logger); + } + + constructor( + private readonly storageClient: storage.Client, + private readonly containerName: string, + private readonly logger: Logger, + ) { + this.storageClient = storageClient; + this.containerName = containerName; + this.logger = logger; + } + + /** + * Upload all the files from the generated `directory` to the OpenStack Swift container. + * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html + */ + async publish({ entity, directory }: PublishRequest): Promise { + try { + // Note: OpenStack Swift manages creation of parent directories if they do not exist. + // So collecting path of only the files is good enough. + const allFilesToUpload = await getFileTreeRecursively(directory); + const limiter = createLimiter(10); + const uploadPromises: Array> = []; + for (const filePath of allFilesToUpload) { + // Remove the absolute path prefix of the source directory + // Path of all files to upload, relative to the root of the source directory + // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] + const relativeFilePath = path.relative(directory, filePath); + + // Convert destination file path to a POSIX path for uploading. + // Swift expects / as path separator and relativeFilePath will contain \\ on Windows. + // https://docs.openstack.org/python-openstackclient/pike/cli/man/openstack.html + const relativeFilePathPosix = relativeFilePath + .split(path.sep) + .join(path.posix.sep); + + // The / delimiter is intentional since it represents the cloud storage and not the local file system. + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + const destination = `${entityRootDir}/${relativeFilePathPosix}`; // Swift container file relative path + + const params = { + container: this.containerName, + remote: destination, + }; + + // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) + const uploadFile = limiter( + () => + new Promise((res, rej) => { + const readStream = fs.createReadStream(filePath, 'utf8'); + + const writeStream = this.storageClient.upload(params); + + writeStream.on('error', rej); + + writeStream.on('success', res); + + readStream.pipe(writeStream); + }), + ); + uploadPromises.push(uploadFile); + } + await Promise.all(uploadPromises); + this.logger.info( + `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`, + ); + return; + } catch (e) { + const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${e}`; + this.logger.error(errorMessage); + throw new Error(errorMessage); + } + } + + async fetchTechDocsMetadata( + entityName: EntityName, + ): Promise { + try { + return await new Promise(async (resolve, reject) => { + const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; + + const stream = this.storageClient.download({ + container: this.containerName, + remote: `${entityRootDir}/techdocs_metadata.json`, + }); + + try { + const techdocsMetadataJson = await streamToBuffer(stream); + if (!techdocsMetadataJson) { + throw new Error( + `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, + ); + } + + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); + + resolve(techdocsMetadata); + } catch (err) { + this.logger.error(err.message); + reject(new Error(err.message)); + } + }); + } catch (e) { + throw new Error(`TechDocs metadata fetch failed, ${e.message}`); + } + } + + /** + * Express route middleware to serve static files on a route in techdocs-backend. + */ + docsRouter(): express.Handler { + return async (req, res) => { + // Trim the leading forward slash + // filePath example - /default/Component/documented-component/index.html + + const filePath = req.path.replace(/^\//, ''); + + // Files with different extensions (CSS, HTML) need to be served with different headers + const fileExtension = path.extname(filePath); + const responseHeaders = getHeadersForFileExtension(fileExtension); + + const stream = this.storageClient.download({ + container: this.containerName, + remote: filePath, + }); + + try { + // Inject response headers + for (const [headerKey, headerValue] of Object.entries( + responseHeaders, + )) { + res.setHeader(headerKey, headerValue); + } + + res.send(await streamToBuffer(stream)); + } catch (err) { + this.logger.warn(err.message); + res.status(404).send(err.message); + } + }; + } + + /** + * A helper function which checks if index.html of an Entity's docs site is available. This + * can be used to verify if there are any pre-generated docs available to serve. + */ + async hasDocsBeenGenerated(entity: Entity): Promise { + try { + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; + + return new Promise(res => { + this.storageClient.getFile( + this.containerName, + `${entityRootDir}/index.html`, + (err, file) => { + if (!err && file) { + res(true); + } else { + res(false); + this.logger.warn(err.message); + } + }, + ); + }); + } catch (e) { + return Promise.resolve(false); + } + } +} diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts index d1faf82b78..948b92aaa6 100644 --- a/packages/techdocs-common/src/stages/publish/publish.test.ts +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -23,6 +23,7 @@ import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; +import { OpenStackSwiftPublish } from './openStackSwift'; const logger = getVoidLogger(); const discovery: jest.Mocked = { @@ -161,4 +162,30 @@ describe('Publisher', () => { }); expect(publisher).toBeInstanceOf(AzureBlobStoragePublish); }); + + it('should create Open Stack Swift publisher from config', async () => { + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'openStackSwift', + openStackSwift: { + credentials: { + username: 'mockuser', + password: 'verystrongpass', + }, + authUrl: 'mockauthurl', + region: 'mockregion', + containerName: 'mock', + }, + }, + }, + }); + + const publisher = await Publisher.fromConfig(mockConfig, { + logger, + discovery, + }); + expect(publisher).toBeInstanceOf(OpenStackSwiftPublish); + }); }); diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/packages/techdocs-common/src/stages/publish/publish.ts index a4e33a2d9c..f70a5f7626 100644 --- a/packages/techdocs-common/src/stages/publish/publish.ts +++ b/packages/techdocs-common/src/stages/publish/publish.ts @@ -22,6 +22,7 @@ import { LocalPublish } from './local'; import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; +import { OpenStackSwiftPublish } from './openStackSwift'; type factoryOptions = { logger: Logger; @@ -53,6 +54,11 @@ export class Publisher { 'Creating Azure Blob Storage Container publisher for TechDocs', ); return AzureBlobStoragePublish.fromConfig(config, logger); + case 'openStackSwift': + logger.info( + 'Creating OpenStack Swift Container publisher for TechDocs', + ); + return OpenStackSwiftPublish.fromConfig(config, logger); case 'local': logger.info('Creating Local publisher for TechDocs'); return new LocalPublish(config, logger, discovery); diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 5e953deb81..f0ecb3cb2b 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -23,7 +23,8 @@ export type PublisherType = | 'local' | 'googleGcs' | 'awsS3' - | 'azureBlobStorage'; + | 'azureBlobStorage' + | 'openStackSwift'; export type PublishRequest = { entity: Entity; diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index ab1fea89b5..d3c8d11bb5 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/jest": "^26.0.7", - "@types/node": "^12.0.0" + "@types/node": "^14.14.32" }, "files": [ "dist" diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 47d223539e..52c1a620c6 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@backstage/cli": "^0.6.2", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0" + "@types/node": "^14.14.32" }, "files": [ "dist" diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 37b8e3b47f..e7bb7e58b6 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -234,12 +234,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`, }, }, + MuiCard: { + root: { + // When cards have a forced size, such as when they are arranged in a + // CSS grid, the content needs to flex such that the actions (buttons + // etc) end up at the bottom of the card instead of just below the body + // contents. + display: 'flex', + flexDirection: 'column', + }, + }, MuiCardHeader: { root: { // Reduce padding between header and content paddingBottom: 0, }, }, + MuiCardContent: { + root: { + // When cards have a forced size, such as when they are arranged in a + // CSS grid, the content needs to flex such that the actions (buttons + // etc) end up at the bottom of the card instead of just below the body + // contents. + flexGrow: 1, + }, + }, MuiCardActions: { root: { // We default to putting the card actions at the end diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index abb5de92c8..f3e0e1f432 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -56,7 +56,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/swagger-ui-react": "^3.23.3", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/auth-backend/migrations/20200619125845_init.js b/plugins/auth-backend/migrations/20200619125845_init.js index e31731b037..4005edd480 100644 --- a/plugins/auth-backend/migrations/20200619125845_init.js +++ b/plugins/auth-backend/migrations/20200619125845_init.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { return knex.schema.createTable('signing_keys', table => { @@ -39,7 +39,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { return knex.schema.dropTable('auth_keystore'); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5be148c984..a5b52ecb7b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -48,7 +48,7 @@ "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "^3.1.0", - "knex": "^0.21.6", + "knex": "^0.95.1", "luxon": "^1.25.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index dd2941a3d5..362022839f 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import Knex from 'knex'; import { resolvePackagePath } from '@backstage/backend-common'; -import { AnyJWK, KeyStore, StoredKey } from './types'; +import { Knex } from 'knex'; import { DateTime } from 'luxon'; +import { AnyJWK, KeyStore, StoredKey } from './types'; const migrationsDir = resolvePackagePath( '@backstage/plugin-auth-backend', diff --git a/plugins/catalog-backend/migrations/20200511113813_init.js b/plugins/catalog-backend/migrations/20200511113813_init.js index 0e4992b329..7f3d75e35c 100644 --- a/plugins/catalog-backend/migrations/20200511113813_init.js +++ b/plugins/catalog-backend/migrations/20200511113813_init.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { return ( @@ -120,7 +120,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { return knex.schema diff --git a/plugins/catalog-backend/migrations/20200520140700_location_update_log_table.js b/plugins/catalog-backend/migrations/20200520140700_location_update_log_table.js index 163b38945a..d8093fc9b4 100644 --- a/plugins/catalog-backend/migrations/20200520140700_location_update_log_table.js +++ b/plugins/catalog-backend/migrations/20200520140700_location_update_log_table.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { return knex.schema.createTable('location_update_log', table => { @@ -36,7 +36,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { return knex.schema.dropTableIfExists('location_update_log'); diff --git a/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js b/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js index 083d0a38aa..a0f0f33a65 100644 --- a/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js +++ b/plugins/catalog-backend/migrations/20200527114117_location_update_log_latest_view.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { // Get list sorted by created_at timestamp in descending order @@ -25,7 +25,7 @@ exports.up = async function up(knex) { return knex.schema.raw(` CREATE VIEW location_update_log_latest AS SELECT t1.* FROM location_update_log t1 - JOIN + JOIN ( SELECT location_id, MAX(created_at) AS MAXDATE FROM location_update_log @@ -38,7 +38,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { return knex.schema.raw(`DROP VIEW location_update_log_latest;`); diff --git a/plugins/catalog-backend/migrations/20200702153613_entities.js b/plugins/catalog-backend/migrations/20200702153613_entities.js index 9acd7564db..0f1c204f9b 100644 --- a/plugins/catalog-backend/migrations/20200702153613_entities.js +++ b/plugins/catalog-backend/migrations/20200702153613_entities.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { // SQLite does not support FK and PK @@ -126,7 +126,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { // SQLite does not support FK and PK diff --git a/plugins/catalog-backend/migrations/20200721115244_location_update_log_latest_deduplicate.js b/plugins/catalog-backend/migrations/20200721115244_location_update_log_latest_deduplicate.js index 9046f85da0..87b41a80fc 100644 --- a/plugins/catalog-backend/migrations/20200721115244_location_update_log_latest_deduplicate.js +++ b/plugins/catalog-backend/migrations/20200721115244_location_update_log_latest_deduplicate.js @@ -13,11 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = function up(knex) { return knex.schema.raw(`DROP VIEW location_update_log_latest;`).raw(` CREATE VIEW location_update_log_latest AS SELECT t1.* FROM location_update_log t1 - JOIN + JOIN ( SELECT location_id, MAX(created_at) AS MAXDATE FROM location_update_log @@ -30,6 +36,9 @@ exports.up = function up(knex) { `); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = function down(knex) { knex.schema.raw(`DROP VIEW location_update_log_latest;`); }; diff --git a/plugins/catalog-backend/migrations/20200805163904_location_update_log_duplication_fix.js b/plugins/catalog-backend/migrations/20200805163904_location_update_log_duplication_fix.js index 05c1658641..de2b194cff 100644 --- a/plugins/catalog-backend/migrations/20200805163904_location_update_log_duplication_fix.js +++ b/plugins/catalog-backend/migrations/20200805163904_location_update_log_duplication_fix.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = function up(knex) { return knex.schema @@ -52,7 +52,7 @@ exports.up = function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = function down(knex) { return knex.schema diff --git a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js index b3a9673641..45226e53b4 100644 --- a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js +++ b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { // Sqlite does not support alter column. @@ -29,7 +29,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { // Sqlite does not support alter column. diff --git a/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js index 379928493d..a90813fe85 100644 --- a/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js +++ b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { // Adds a single 'bootstrap' location that can be used to trigger work in processors. @@ -30,7 +30,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex('locations') diff --git a/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js index eeec950d7f..ea5ba9e58d 100644 --- a/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js +++ b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex('entities') diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js index 4c1ea76de4..aae1861658 100644 --- a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js +++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.alterTable('entities', table => { @@ -45,7 +45,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex.schema.alterTable('entities', table => { diff --git a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js index a326a08a8b..a8964efbf6 100644 --- a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js +++ b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.alterTable('entities', table => { @@ -46,20 +46,10 @@ exports.up = async function up(knex) { table.text('data').notNullable().alter(); }); } - - // NOTE(freben): For some reason, specifically sqlite3 in-mem just drops some - // subset of constraints sometimes, when a table column is dropped - even if - // the column had no relation at all to the constraint. We therefore recreate - // the constraint here as a stupid fix. - if (knex.client.config.client === 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.unique(['full_name'], 'entities_unique_full_name'); - }); - } }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex.schema.alterTable('entities', table => { diff --git a/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js b/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js index 7bacc67338..f40df5f73e 100644 --- a/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js +++ b/plugins/catalog-backend/migrations/20201006203131_entity_remove_redundant_columns.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.alterTable('entities', table => { @@ -26,20 +26,10 @@ exports.up = async function up(knex) { table.dropColumn('name'); table.dropColumn('namespace'); }); - - // NOTE(freben): For some reason, specifically sqlite3 in-mem just drops some - // subset of constraints sometimes, when a table column is dropped - even if - // the column had no relation at all to the constraint. We therefore recreate - // the constraint here as a stupid fix. - if (knex.client.config.client === 'sqlite3') { - await knex.schema.alterTable('entities', table => { - table.unique(['full_name'], 'entities_unique_full_name'); - }); - } }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex.schema.alterTable('entities', table => { diff --git a/plugins/catalog-backend/migrations/20201007201501_index_entity_search.js b/plugins/catalog-backend/migrations/20201007201501_index_entity_search.js index b902073e64..77bf0529eb 100644 --- a/plugins/catalog-backend/migrations/20201007201501_index_entity_search.js +++ b/plugins/catalog-backend/migrations/20201007201501_index_entity_search.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.alterTable('entities_search', table => { @@ -27,7 +27,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex.schema.alterTable('entities_search', table => { diff --git a/plugins/catalog-backend/migrations/20201019130742_add_relations_table.js b/plugins/catalog-backend/migrations/20201019130742_add_relations_table.js index 70150ff173..85e729f814 100644 --- a/plugins/catalog-backend/migrations/20201019130742_add_relations_table.js +++ b/plugins/catalog-backend/migrations/20201019130742_add_relations_table.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.createTable('entities_relations', table => { @@ -47,7 +47,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { await knex.schema.dropTable('entities_relations'); diff --git a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js index a8cf62b4c4..9e8198b5eb 100644 --- a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js +++ b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { if (knex.client.config.client === 'sqlite3') { @@ -55,7 +55,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { if (knex.client.config.client === 'sqlite3') { diff --git a/plugins/catalog-backend/migrations/20201210185851_fk_index.js b/plugins/catalog-backend/migrations/20201210185851_fk_index.js index acf90b0a27..abb26cd5fc 100644 --- a/plugins/catalog-backend/migrations/20201210185851_fk_index.js +++ b/plugins/catalog-backend/migrations/20201210185851_fk_index.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { if (knex.client.config.client !== 'sqlite3') { @@ -31,7 +31,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { if (knex.client.config.client !== 'sqlite3') { diff --git a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js index 71f9500cf2..d924b0414a 100644 --- a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js +++ b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { if (knex.client.config.client !== 'sqlite3') { @@ -46,7 +46,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { if (knex.client.config.client !== 'sqlite3') { diff --git a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js index 5591717c13..ccfb1faffb 100644 --- a/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js +++ b/plugins/catalog-backend/migrations/20210209121210_locations_fk_index.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { if (knex.client.config.client !== 'sqlite3') { @@ -31,7 +31,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { if (knex.client.config.client !== 'sqlite3') { diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 5ffc789635..fff8d9eaff 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -46,7 +46,7 @@ "fs-extra": "^9.0.0", "git-url-parse": "^11.4.4", "glob": "^7.1.6", - "knex": "^0.21.6", + "knex": "^0.95.1", "ldapjs": "^2.2.0", "lodash": "^4.17.15", "morgan": "^1.10.0", diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 2edc0ca5b8..cc83350019 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -30,7 +30,7 @@ import { Location, parseEntityName, } from '@backstage/catalog-model'; -import Knex from 'knex'; +import { Knex } from 'knex'; import lodash from 'lodash'; import type { Logger } from 'winston'; import { buildEntitySearch } from './search'; @@ -99,7 +99,7 @@ export class CommonDatabase implements Database { txOpaque: Transaction, request: DbEntityRequest[], ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const result: DbEntityResponse[] = []; const entityRows: DbEntitiesRow[] = []; @@ -149,7 +149,7 @@ export class CommonDatabase implements Database { matchingEtag?: string, matchingGeneration?: number, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const { uid } = request.entity.metadata; if (!uid) { @@ -213,7 +213,7 @@ export class CommonDatabase implements Database { txOpaque: Transaction, filter?: EntityFilter, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; let entitiesQuery = tx('entities'); @@ -256,7 +256,7 @@ export class CommonDatabase implements Database { txOpaque: Transaction, name: EntityName, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const rows = await tx('entities') .where({ @@ -275,7 +275,7 @@ export class CommonDatabase implements Database { txOpaque: Transaction, uid: string, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const rows = await tx('entities') .where({ id: uid }) @@ -289,7 +289,7 @@ export class CommonDatabase implements Database { } async removeEntityByUid(txOpaque: Transaction, uid: string): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const result = await tx('entities').where({ id: uid }).del(); @@ -303,7 +303,7 @@ export class CommonDatabase implements Database { originatingEntityId: string, relations: EntityRelationSpec[], ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const relationRows = this.toRelationRows(originatingEntityId, relations); await tx('entities_relations') @@ -316,7 +316,7 @@ export class CommonDatabase implements Database { txOpaque: Transaction, location: Location, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const row: DbLocationsRow = { id: location.id, @@ -328,7 +328,7 @@ export class CommonDatabase implements Database { } async removeLocation(txOpaque: Transaction, id: string): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex.Transaction; const locations = await tx('locations') .where({ id }) @@ -467,7 +467,7 @@ export class CommonDatabase implements Database { } private async toEntityResponses( - tx: Knex.Transaction, + tx: Knex.Transaction, rows: DbEntitiesRow[], ): Promise { // TODO(Rugvip): This is here because it's simple for now, but we likely @@ -501,7 +501,7 @@ export class CommonDatabase implements Database { // Returns a mapping from e.g. component:default/foo to the relations whose // source_full_name matches that. private async getRelationsPerFullName( - tx: Knex.Transaction, + tx: Knex.Transaction, sourceFullNames: string[], ): Promise> { const batches = lodash.chunk(lodash.uniq(sourceFullNames), 500); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 465bf654f2..106876af57 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -15,11 +15,11 @@ */ import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common'; -import Knex from 'knex'; +import knexFactory, { Knex } from 'knex'; +import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { CommonDatabase } from './CommonDatabase'; import { Database } from './types'; -import { v4 as uuidv4 } from 'uuid'; const migrationsDir = resolvePackagePath( '@backstage/plugin-catalog-backend', @@ -52,7 +52,7 @@ export class DatabaseManager { } public static async createInMemoryDatabaseConnection(): Promise { - const knex = Knex({ + const knex = knexFactory({ client: 'sqlite3', connection: ':memory:', useNullAsDefault: true, @@ -85,11 +85,11 @@ export class DatabaseManager { useNullAsDefault: true, }; - let knex = Knex(config); + let knex = knexFactory(config); if (typeof config.connection !== 'string') { const tempDbName = `d${uuidv4().replace(/-/g, '')}`; await knex.raw(`CREATE DATABASE ${tempDbName};`); - knex = Knex({ + knex = knexFactory({ ...config, connection: { ...config.connection, diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 5a9f60853a..ee2d4222c3 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -115,7 +115,7 @@ export type EntityFilter = { * An abstraction for transactions of the underlying database technology. */ export type Transaction = { - rollback(): Promise; + rollback(): Promise; }; /** diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 467c0eb2e8..9d167603d0 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Location, LocationSpec } from '@backstage/catalog-model'; +import { + Location, + LocationSpec, + stringifyLocationReference, +} from '@backstage/catalog-model'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; @@ -127,14 +131,18 @@ export class HigherOrderOperations implements HigherOrderOperation { for (const { data: location } of locations) { logger.info( - `Locations Refresh: Refreshing location ${location.type}:${location.target}`, + `Locations Refresh: Refreshing location ${stringifyLocationReference( + location, + )}`, ); try { await this.refreshSingleLocation(location, logger); await this.locationsCatalog.logUpdateSuccess(location.id, undefined); } catch (e) { logger.warn( - `Locations Refresh: Failed to refresh location ${location.type}:${location.target}, ${e.stack}`, + `Locations Refresh: Failed to refresh location ${stringifyLocationReference( + location, + )}, ${e.stack}`, ); await this.locationsCatalog.logUpdateFailure(location.id, e); } @@ -162,14 +170,18 @@ export class HigherOrderOperations implements HigherOrderOperation { for (const item of readerOutput.errors) { logger.warn( - `Failed item in location ${item.location.type}:${item.location.target}, ${item.error.stack}`, + `Failed item in location ${stringifyLocationReference( + item.location, + )}, ${item.error.stack}`, ); } logger.info( - `Read ${readerOutput.entities.length} entities from location ${ - location.type - }:${location.target} in ${durationText(startTimestamp)}`, + `Read ${ + readerOutput.entities.length + } entities from location ${stringifyLocationReference( + location, + )} in ${durationText(startTimestamp)}`, ); startTimestamp = process.hrtime(); @@ -198,9 +210,11 @@ export class HigherOrderOperations implements HigherOrderOperation { ); logger.info( - `Wrote ${readerOutput.entities.length} entities from location ${ - location.type - }:${location.target} in ${durationText(startTimestamp)}`, + `Wrote ${ + readerOutput.entities.length + } entities from location ${stringifyLocationReference( + location, + )} in ${durationText(startTimestamp)}`, ); } } diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts index cfd98e9e6a..9c543cdcd4 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts @@ -15,6 +15,8 @@ */ import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; import { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; describe('AnnotateLocationEntityProcessor', () => { @@ -30,14 +32,17 @@ describe('AnnotateLocationEntityProcessor', () => { const location: LocationSpec = { type: 'url', - target: 'my-location', + target: + 'https://github.com/backstage/backstage/blob/master/packages/app/catalog-info.yaml', }; const originLocation: LocationSpec = { type: 'url', - target: 'my-origin-location', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }; - const processor = new AnnotateLocationEntityProcessor(); + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); + const processor = new AnnotateLocationEntityProcessor({ integrations }); expect( await processor.preProcessEntity( @@ -52,8 +57,110 @@ describe('AnnotateLocationEntityProcessor', () => { metadata: { name: 'my-component', annotations: { - 'backstage.io/managed-by-location': 'url:my-location', - 'backstage.io/managed-by-origin-location': 'url:my-origin-location', + 'backstage.io/managed-by-location': + 'url:https://github.com/backstage/backstage/blob/master/packages/app/catalog-info.yaml', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + 'backstage.io/view-url': + 'https://github.com/backstage/backstage/blob/master/packages/app/catalog-info.yaml', + 'backstage.io/edit-url': + 'https://github.com/backstage/backstage/edit/master/packages/app/catalog-info.yaml', + 'backstage.io/source-location': + 'url:https://github.com/backstage/backstage/tree/master/packages/app/', + }, + }, + }); + }); + + it('does not override existing annotations', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/view-url': 'https://example.com/view', + 'backstage.io/edit-url': 'https://example.com/edit', + 'backstage.io/source-location': 'url:https://example.com/source', + }, + }, + }; + + const location: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/packages/app/catalog-info.yaml', + }; + const originLocation: LocationSpec = { + type: 'url', + target: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }; + + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); + const processor = new AnnotateLocationEntityProcessor({ integrations }); + + expect( + await processor.preProcessEntity( + entity, + location, + () => {}, + originLocation, + ), + ).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/backstage/backstage/blob/master/packages/app/catalog-info.yaml', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + 'backstage.io/view-url': 'https://example.com/view', + 'backstage.io/edit-url': 'https://example.com/edit', + 'backstage.io/source-location': 'url:https://example.com/source', + }, + }, + }); + }); + + it('does not output view, edit or source location annotations for non url type locations', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + }, + }; + + const location: LocationSpec = { + type: 'file', + target: './test.yaml', + }; + const originLocation: LocationSpec = { + type: 'file', + target: './test.yaml', + }; + + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); + const processor = new AnnotateLocationEntityProcessor({ integrations }); + + expect( + await processor.preProcessEntity( + entity, + location, + () => {}, + originLocation, + ), + ).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/managed-by-location': 'file:./test.yaml', + 'backstage.io/managed-by-origin-location': 'file:./test.yaml', }, }, }); diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts index b40378226a..f892055a6a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts @@ -15,28 +15,71 @@ */ import { + EDIT_URL_ANNOTATION, Entity, - LOCATION_ANNOTATION, LocationSpec, + LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, + SOURCE_LOCATION_ANNOTATION, + stringifyLocationReference, + VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; -import lodash from 'lodash'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { identity, merge, pickBy } from 'lodash'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; +type Options = { + integrations: ScmIntegrationRegistry; +}; + export class AnnotateLocationEntityProcessor implements CatalogProcessor { + constructor(private readonly options: Options) {} + async preProcessEntity( entity: Entity, location: LocationSpec, _: CatalogProcessorEmit, originLocation: LocationSpec, ): Promise { - return lodash.merge( + const { integrations } = this.options; + let viewUrl; + let editUrl; + let sourceLocation; + + if (location.type === 'url') { + const scmIntegration = integrations.byUrl(location.target); + + viewUrl = location.target; + editUrl = scmIntegration?.resolveEditUrl(location.target); + + const sourceUrl = scmIntegration?.resolveUrl({ + url: './', + base: location.target, + }); + + if (sourceUrl) { + sourceLocation = stringifyLocationReference({ + type: 'url', + target: sourceUrl, + }); + } + } + + return merge( { metadata: { - annotations: { - [LOCATION_ANNOTATION]: `${location.type}:${location.target}`, - [ORIGIN_LOCATION_ANNOTATION]: `${originLocation.type}:${originLocation.target}`, - }, + annotations: pickBy( + { + [LOCATION_ANNOTATION]: stringifyLocationReference(location), + [ORIGIN_LOCATION_ANNOTATION]: stringifyLocationReference( + originLocation, + ), + [VIEW_URL_ANNOTATION]: viewUrl, + [EDIT_URL_ANNOTATION]: editUrl, + [SOURCE_LOCATION_ANNOTATION]: sourceLocation, + }, + identity, + ), }, }, entity, diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts index e1d7e22ccf..8a3951428e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts @@ -15,7 +15,11 @@ */ import { NotFoundError, UrlReader } from '@backstage/backend-common'; -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { + Entity, + LocationSpec, + stringifyLocationReference, +} from '@backstage/catalog-model'; import * as codeowners from 'codeowners-utils'; import { CodeOwnersEntry } from 'codeowners-utils'; // NOTE: This can be removed when ES2021 is implemented @@ -108,11 +112,15 @@ export async function findRawCodeOwners( ); if (hardError) { options.logger.warn( - `Failed to read codeowners for location ${location.type}:${location.target}, ${hardError}`, + `Failed to read codeowners for location ${stringifyLocationReference( + location, + )}, ${hardError}`, ); } else { options.logger.debug( - `Failed to find codeowners for location ${location.type}:${location.target}`, + `Failed to find codeowners for location ${stringifyLocationReference( + location, + )}`, ); } return undefined; diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts index 00b6af94e5..9884fa1927 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts @@ -331,7 +331,7 @@ describe('PlaceholderProcessor', () => { }, ), ).rejects.toThrow( - 'Placeholder $text could not form an URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml', + 'Placeholder $text could not form a URL out of ./a/b/catalog-info.yaml and ../c/catalog-info.yaml', ); expect(read).not.toBeCalled(); diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts index b7b3df7b6e..b71e5c1cbb 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts @@ -203,7 +203,7 @@ function relativeUrl({ key, value, baseUrl }: ResolverParams): string { // path traversal attacks and access to any file on the host system. Implementing this // would require additional security measures. throw new Error( - `Placeholder \$${key} could not form an URL out of ${baseUrl} and ${value}`, + `Placeholder \$${key} could not form a URL out of ${baseUrl} and ${value}`, ); } } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts index bc58338fce..cdc01ba60e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger, UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; -import Knex from 'knex'; +import { Knex } from 'knex'; import yaml from 'yaml'; import { DatabaseManager } from '../database'; import { CatalogProcessorParser } from '../ingestion'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index a75c7da6e8..126a138de4 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -311,7 +311,7 @@ export class CatalogBuilder { new UrlReaderProcessor({ reader, logger }), new CodeOwnersProcessor({ reader, logger }), new LocationEntityProcessor({ integrations }), - new AnnotateLocationEntityProcessor(), + new AnnotateLocationEntityProcessor({ integrations }), ); } diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index f9cea55a3b..07ec94a7c0 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -59,7 +59,7 @@ "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx index 9d59f4e10e..ad848d7893 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.tsx @@ -55,9 +55,13 @@ export const PreparePullRequestForm = < onSubmit, render, }: Props) => { - const { handleSubmit, watch, control, register, errors } = useForm< - TFieldValues - >({ mode: 'onTouched', defaultValues }); + const { + handleSubmit, + watch, + control, + register, + errors, + } = useForm({ mode: 'onTouched', defaultValues }); return (
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 5e7540d1c7..474fa6d46d 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -47,7 +47,7 @@ "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2", "react-test-renderer": "^16.13.1" diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index a61bc6fa68..453da7c7ed 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,6 +33,7 @@ "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.0", + "@backstage/integration": "^0.5.0", "@backstage/plugin-catalog-react": "^0.1.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -59,7 +60,7 @@ "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2", "react-test-renderer": "^16.13.1" diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 57b6b419e0..926deec325 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -14,25 +14,74 @@ * limitations under the License. */ -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { - SOURCE_LOCATION_ANNOTATION, - EDIT_URL_ANNOTATION, -} from '@backstage/catalog-model'; -import { render, act, fireEvent } from '@testing-library/react'; + ApiProvider, + ApiRegistry, + configApiRef, + ConfigReader, +} from '@backstage/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; import { AboutCard } from './AboutCard'; -describe(' GitHub', () => { - it('renders info and "view source" link', async () => { +describe('', () => { + it('renders info', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + description: 'This is the decription', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { + kind: 'user', + name: 'guest', + namespace: 'default', + }, + }, + ], + }; + const apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ + integrations: {}, + }), + ); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('service')).toBeInTheDocument(); + expect(getByText('user:guest')).toBeInTheDocument(); + expect(getByText('production')).toBeInTheDocument(); + expect(getByText('This is the decription')).toBeInTheDocument(); + }); + + it('renders "view source" link', async () => { const entity = { apiVersion: 'v1', kind: 'Component', metadata: { name: 'software', annotations: { - 'backstage.io/managed-by-location': - 'github:https://github.com/backstage/backstage/blob/master/software.yaml', + 'backstage.io/source-location': + 'url:https://github.com/backstage/backstage/blob/master/software.yaml', }, }, spec: { @@ -41,16 +90,71 @@ describe(' GitHub', () => { lifecycle: 'production', }, }; - const { getByText, getByTitle } = render( - - - , + const apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ + integrations: { + github: [ + { + host: 'github.com', + token: '...', + }, + ], + }, + }), + ); + + const { getByText } = await renderInTestApp( + + + + + , ); - expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', 'https://github.com/backstage/backstage/blob/master/software.yaml', ); + }); + + it('renders "edit metadata" button', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'backstage.io/edit-url': + 'https://github.com/backstage/backstage/edit/master/software.yaml', + }, + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const apis = ApiRegistry.with( + configApiRef, + new ConfigReader({ + integrations: { + github: [ + { + host: 'github.com', + token: '...', + }, + ], + }, + }), + ); + + const { getByTitle } = await renderInTestApp( + + + + + , + ); const editButton = getByTitle('Edit Metadata'); window.open = jest.fn(); @@ -62,19 +166,13 @@ describe(' GitHub', () => { '_blank', ); }); -}); -describe(' GitLab', () => { - it('renders info and "view source" link', async () => { + it('renders without "view source" link', async () => { const entity = { apiVersion: 'v1', kind: 'Component', metadata: { name: 'software', - annotations: { - 'backstage.io/managed-by-location': - 'gitlab:https://gitlab.com/backstage/backstage/-/blob/master/software.yaml', - }, }, spec: { owner: 'guest', @@ -82,108 +180,15 @@ describe(' GitLab', () => { lifecycle: 'production', }, }; - const { getByText, getByTitle } = render( - - - , - ); + const apis = ApiRegistry.with(configApiRef, new ConfigReader({})); - expect(getByText('service')).toBeInTheDocument(); - expect(getByText('View Source').closest('a')).toHaveAttribute( - 'href', - 'https://gitlab.com/backstage/backstage/-/blob/master/software.yaml', - ); - - const editButton = getByTitle('Edit Metadata'); - window.open = jest.fn(); - await act(async () => { - fireEvent.click(editButton); - }); - expect(window.open).toHaveBeenCalledWith( - `https://gitlab.com/backstage/backstage/-/edit/master/software.yaml`, - '_blank', + const { getByText } = await renderInTestApp( + + + + + , ); - }); -}); - -describe(' BitBucket', () => { - it('renders info and "view source" link', async () => { - const entity = { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'software', - annotations: { - 'backstage.io/managed-by-location': - 'bitbucket:https://bitbucket.org/backstage/backstage/src/master/software.yaml', - }, - }, - spec: { - owner: 'guest', - type: 'service', - lifecycle: 'production', - }, - }; - const { getByText, getByTitle } = render( - - - , - ); - expect(getByText('service')).toBeInTheDocument(); - expect(getByText('View Source').closest('a')).toHaveAttribute( - 'href', - 'https://bitbucket.org/backstage/backstage/src/master/software.yaml', - ); - - const editButton = getByTitle('Edit Metadata'); - window.open = jest.fn(); - await act(async () => { - fireEvent.click(editButton); - }); - expect(window.open).toHaveBeenCalledWith( - `https://bitbucket.org/backstage/backstage/src/master/software.yaml?mode=edit&spa=0&at=master`, - '_blank', - ); - }); -}); - -describe(' custom links', () => { - it('renders info and "view source" link', async () => { - const entity = { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'software', - annotations: { - 'backstage.io/managed-by-location': - 'bitbucket:https://bitbucket.org/backstage/backstage/src/master/software.yaml', - [EDIT_URL_ANNOTATION]: 'https://another.place', - [SOURCE_LOCATION_ANNOTATION]: - 'url:https://another.place/backstage.git', - }, - }, - spec: { - owner: 'guest', - type: 'service', - lifecycle: 'production', - }, - }; - const { getByText, getByTitle } = render( - - - , - ); - expect(getByText('service')).toBeInTheDocument(); - expect(getByText('View Source').closest('a')).toHaveAttribute( - 'href', - 'https://another.place/backstage.git', - ); - - const editButton = getByTitle('Edit Metadata'); - window.open = jest.fn(); - await act(async () => { - fireEvent.click(editButton); - }); - expect(window.open).toHaveBeenCalledWith(`https://another.place`, '_blank'); + expect(getByText('View Source').closest('a')).not.toHaveAttribute('href'); }); }); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ff411f80d4..c8bac651ce 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -16,13 +16,17 @@ import { Entity, - LocationSpec, ENTITY_DEFAULT_NAMESPACE, - SOURCE_LOCATION_ANNOTATION, + RELATION_CONSUMES_API, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { HeaderIconLinkRow, IconLinkVerticalProps } from '@backstage/core'; -import { useEntity } from '@backstage/plugin-catalog-react'; +import { + configApiRef, + HeaderIconLinkRow, + IconLinkVerticalProps, + useApi, +} from '@backstage/core'; +import { getEntityRelations, useEntity } from '@backstage/plugin-catalog-react'; import { Card, CardContent, @@ -34,11 +38,10 @@ import { import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import ExtensionIcon from '@material-ui/icons/Extension'; -import GitHubIcon from '@material-ui/icons/GitHub'; import React from 'react'; -import { findLocationForEntityMeta, parseLocation } from '../../data/utils'; -import { findEditUrl, determineUrlType } from '../actions'; +import { getEntityMetadataEditUrl, getEntitySourceLocation } from '../../utils'; import { AboutContent } from './AboutContent'; +import { ScmIntegrationIcon } from './ScmIntegrationIcon'; const useStyles = makeStyles({ gridItemCard: { @@ -52,47 +55,6 @@ const useStyles = makeStyles({ }, }); -const iconMap: Record = { - github: , -}; - -type CodeLinkInfo = { - icon?: React.ReactNode; - edithref?: string; - href?: string; -}; - -function getSourceLocationForEntity( - entity: Entity, - location?: LocationSpec, -): LocationSpec | undefined { - const annotation = entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION]; - const parsed = annotation && parseLocation(annotation); - - return parsed || location; -} - -function getCodeLinkInfo(entity: Entity): CodeLinkInfo { - const location = findLocationForEntityMeta(entity?.metadata); - const editUrl = findEditUrl(entity); - let sourceLocation = getSourceLocationForEntity(entity, location); - - if (location) { - sourceLocation = sourceLocation || location; - const type = - sourceLocation.type === 'url' - ? determineUrlType(sourceLocation.target) - : sourceLocation.type; - return { - edithref: editUrl, - icon: iconMap[type], - href: sourceLocation.target, - }; - } - - return { edithref: editUrl, href: sourceLocation?.target }; -} - type AboutCardProps = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; @@ -102,13 +64,25 @@ type AboutCardProps = { export function AboutCard({ variant }: AboutCardProps) { const classes = useStyles(); const { entity } = useEntity(); - const codeLink = getCodeLinkInfo(entity); - // TODO: Also support RELATION_CONSUMES_API here - const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API); + const configApi = useApi(configApiRef); + const entitySourceLocation = getEntitySourceLocation(entity, configApi); + const entityMetadataEditUrl = getEntityMetadataEditUrl(entity); + const providesApiRelations = getEntityRelations( + entity, + RELATION_PROVIDES_API, + ); + const consumesApiRelations = getEntityRelations( + entity, + RELATION_CONSUMES_API, + ); + const hasApis = + providesApiRelations.length > 0 || consumesApiRelations.length > 0; + const viewInSource: IconLinkVerticalProps = { label: 'View Source', - href: codeLink.href, - icon: codeLink.icon, + disabled: !entitySourceLocation, + icon: , + href: entitySourceLocation?.locationTargetUrl, }; const viewInTechDocs: IconLinkVerticalProps = { label: 'View TechDocs', @@ -133,9 +107,10 @@ export function AboutCard({ variant }: AboutCardProps) { action={ { - window.open(codeLink.edithref || '#', '_blank'); + window.open(entityMetadataEditUrl ?? '#', '_blank'); }} > diff --git a/plugins/catalog/src/components/AboutCard/ScmIntegrationIcon.tsx b/plugins/catalog/src/components/AboutCard/ScmIntegrationIcon.tsx new file mode 100644 index 0000000000..c96401135e --- /dev/null +++ b/plugins/catalog/src/components/AboutCard/ScmIntegrationIcon.tsx @@ -0,0 +1,31 @@ +/* + * 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 CodeIcon from '@material-ui/icons/Code'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import React from 'react'; + +export const ScmIntegrationIcon = ({ type }: { type?: string }) => { + // TODO: In the future we might want to support more types here as a GitLab or + // Bitbucket icons were requested here in the past, or even use the icon + // customization feature of the app. But material UI react doesn't provide more. + + switch (type) { + case 'github': + return ; + default: + return ; + } +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 9190ae703e..0ec5dd31af 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -74,9 +74,10 @@ const CatalogPageContents = () => { const errorApi = useApi(errorApiRef); const { isStarredEntity } = useStarredEntities(); const [selectedTab, setSelectedTab] = useState(); - const [selectedSidebarItem, setSelectedSidebarItem] = useState< - CatalogFilterType - >(); + const [ + selectedSidebarItem, + setSelectedSidebarItem, + ] = useState(); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; const createComponentLink = useRouteRef(createComponentRouteRef); const addMockData = useCallback(async () => { diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 5a8040cd5b..dc80622bbe 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -21,11 +21,11 @@ import { } from '@backstage/catalog-model'; import { CodeSnippet, + OverflowTooltip, Table, TableColumn, TableProps, WarningPanel, - OverflowTooltip, } from '@backstage/core'; import { EntityRefLink, @@ -38,7 +38,10 @@ import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import React from 'react'; -import { findViewUrl, findEditUrl } from '../actions'; +import { + getEntityMetadataEditUrl, + getEntityMetadataViewUrl, +} from '../../utils'; import { favouriteEntityIcon, favouriteEntityTooltip, @@ -152,10 +155,11 @@ export const CatalogTable = ({ const actions: TableProps['actions'] = [ ({ entity }) => { - const url = findViewUrl(entity); + const url = getEntityMetadataViewUrl(entity); return { icon: () => , tooltip: 'View', + disabled: !url, onClick: () => { if (!url) return; window.open(url, '_blank'); @@ -163,10 +167,11 @@ export const CatalogTable = ({ }; }, ({ entity }) => { - const url = findEditUrl(entity); + const url = getEntityMetadataEditUrl(entity); return { icon: () => , tooltip: 'Edit', + disabled: !url, onClick: () => { if (!url) return; window.open(url, '_blank'); diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 34378d5496..6f9899ff82 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -18,7 +18,15 @@ import { ENTITY_DEFAULT_NAMESPACE, RELATION_OWNED_BY, } from '@backstage/catalog-model'; -import { Content, Header, HeaderLabel, Page, Progress } from '@backstage/core'; +import { + Content, + Header, + HeaderLabel, + Link, + Page, + Progress, + WarningPanel, +} from '@backstage/core'; import { EntityContext, EntityRefLinks, @@ -125,7 +133,11 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { )} - {loading && } + {loading && ( + + + + )} {entity && {children}} @@ -134,6 +146,19 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { {error.toString()} )} + + {!loading && !error && !entity && ( + + + There is no {kind} with the requested{' '} + + kind, namespace, and name + + . + + + )} + ) => { ); }; + EntityPageLayout.Content = Tabbed.Content; diff --git a/plugins/catalog/src/components/actions.ts b/plugins/catalog/src/components/actions.ts deleted file mode 100644 index 72e1bf67ec..0000000000 --- a/plugins/catalog/src/components/actions.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 { - LocationSpec, - Entity, - EDIT_URL_ANNOTATION, - VIEW_URL_ANNOTATION, -} from '@backstage/catalog-model'; -import { findLocationForEntityMeta } from '../data/utils'; -import parseGitUrl from 'git-url-parse'; - -/** - * Creates the edit link for components yaml file - * @see LocationSpec - * @param location The LocationSpec being used to determine entity SCM location - * @returns string representing the edit location based on SCM path - */ - -export const createEditLink = (location: LocationSpec): string | undefined => { - try { - const urlData = parseGitUrl(location.target); - const url = new URL(location.target); - switch (location.type) { - case 'github': - case 'gitlab': - return location.target.replace('/blob/', '/edit/'); - case 'bitbucket': - url.searchParams.set('mode', 'edit'); - url.searchParams.set('spa', '0'); - url.searchParams.set('at', urlData.ref); - return url.toString(); - case 'url': - if ( - urlData.source === 'github.com' || - urlData.source === 'gitlab.com/' - ) { - return location.target.replace('/blob/', '/edit/'); - } else if (urlData.source === 'bitbucket.org') { - url.searchParams.set('mode', 'edit'); - url.searchParams.set('spa', '0'); - url.searchParams.set('at', urlData.ref); - return url.toString(); - } - return location.target; - default: - return location.target; - } - } catch { - return undefined; - } -}; - -/** - * Determines type based on passed in url. This is used to set the icon associated with the type of entity - * @param url - * @returns string representing type of icon to be used - */ -export const determineUrlType = (url: string): string => { - const urlData = parseGitUrl(url); - - if (urlData.source === 'github.com') { - return 'github'; - } else if (urlData.source === 'bitbucket.org') { - return 'bitbucket'; - } else if (urlData.source === 'gitlab.com') { - return 'gitlab'; - } - return 'url'; -}; - -export const findEditUrl = ({ metadata }: Entity): string | undefined => { - const annotations = metadata.annotations || {}; - - const editUrl = annotations[EDIT_URL_ANNOTATION]; - - if (editUrl) return editUrl; - - const location = findLocationForEntityMeta(metadata); - - return location && createEditLink(location); -}; - -export const findViewUrl = ({ metadata }: Entity): string | undefined => { - const annotations = metadata.annotations || {}; - const location = findLocationForEntityMeta(metadata); - - return annotations[VIEW_URL_ANNOTATION] || location?.target; -}; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts deleted file mode 100644 index ec63d94754..0000000000 --- a/plugins/catalog/src/data/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { - EntityMeta, - LocationSpec, - LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; - -export function findLocationForEntityMeta( - meta: EntityMeta, -): LocationSpec | undefined { - if (!meta) { - return undefined; - } - - const annotation = meta.annotations?.[LOCATION_ANNOTATION]; - if (!annotation) { - return undefined; - } - - return parseLocation(annotation); -} - -export function parseLocation(reference: string): LocationSpec | undefined { - const separatorIndex = reference.indexOf(':'); - if (separatorIndex === -1) { - return undefined; - } - - return { - type: reference.substring(0, separatorIndex), - target: reference.substring(separatorIndex + 1), - }; -} diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx b/plugins/catalog/src/utils/getEntityMetadataUrl.ts similarity index 53% rename from plugins/explore/src/components/DomainCard/DomainCardGrid.tsx rename to plugins/catalog/src/utils/getEntityMetadataUrl.ts index b4a13aa39f..a28fdb26d7 100644 --- a/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx +++ b/plugins/catalog/src/utils/getEntityMetadataUrl.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Spotify AB + * 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. @@ -13,21 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DomainEntity } from '@backstage/catalog-model'; -import { Grid } from '@material-ui/core'; -import React from 'react'; -import { DomainCard } from '.'; -type DomainCardGridProps = { - entities: DomainEntity[]; -}; +import { + EDIT_URL_ANNOTATION, + Entity, + VIEW_URL_ANNOTATION, +} from '@backstage/catalog-model'; -export const DomainCardGrid = ({ entities }: DomainCardGridProps) => ( - - {entities.map((e, i) => ( - - - - ))} - -); +export function getEntityMetadataViewUrl(entity: Entity): string | undefined { + return entity.metadata.annotations?.[VIEW_URL_ANNOTATION]; +} + +export function getEntityMetadataEditUrl(entity: Entity): string | undefined { + return entity.metadata.annotations?.[EDIT_URL_ANNOTATION]; +} diff --git a/plugins/catalog/src/utils/getEntitySourceLocation.ts b/plugins/catalog/src/utils/getEntitySourceLocation.ts new file mode 100644 index 0000000000..5b957a850a --- /dev/null +++ b/plugins/catalog/src/utils/getEntitySourceLocation.ts @@ -0,0 +1,53 @@ +/* + * 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, + parseLocationReference, + SOURCE_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { ConfigApi } from '@backstage/core'; +import { ScmIntegrations } from '@backstage/integration'; + +export type EntitySourceLocation = { + locationTargetUrl: string; + integrationType?: string; +}; + +export function getEntitySourceLocation( + entity: Entity, + config: ConfigApi, +): EntitySourceLocation | undefined { + const sourceLocation = + entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION]; + + if (!sourceLocation) { + return undefined; + } + + try { + const sourceLocationRef = parseLocationReference(sourceLocation); + const scmIntegrations = ScmIntegrations.fromConfig(config); + const integration = scmIntegrations.byUrl(sourceLocationRef.target); + + return { + locationTargetUrl: sourceLocationRef.target, + integrationType: integration?.type, + }; + } catch { + return undefined; + } +} diff --git a/plugins/catalog/src/utils/index.ts b/plugins/catalog/src/utils/index.ts new file mode 100644 index 0000000000..bba42dac7e --- /dev/null +++ b/plugins/catalog/src/utils/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { + getEntityMetadataEditUrl, + getEntityMetadataViewUrl, +} from './getEntityMetadataUrl'; +export { getEntitySourceLocation } from './getEntitySourceLocation'; diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 5a78c1a7d6..8fcdf3cd9f 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -57,7 +57,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/react-lazylog": "^4.5.0", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 4a7c060448..20fa652b62 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -54,7 +54,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts index 2150e01d98..cc2ccafa04 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -17,9 +17,7 @@ import { useAsync } from 'react-use'; import { ActionsListWorkflowRunsForRepoResponseData } from '../../api/types'; export const useWorkflowRunJobs = (jobsUrl?: string) => { - const jobs = useAsync(async (): Promise< - ActionsListWorkflowRunsForRepoResponseData - > => { + const jobs = useAsync(async (): Promise => { if (jobsUrl === undefined) { return { builds: [], diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index c03952c25c..7e9d015db2 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -62,7 +62,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/pluralize": "^0.0.29", "@types/recharts": "^1.8.14", "@types/regression": "^2.0.0", diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 1097d86be4..26f51fbed2 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -38,9 +38,7 @@ type PartialPropsWithChildren = PropsWithChildren>; export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; -export type MockFilterProviderProps = PartialPropsWithChildren< - FilterContextProps ->; +export type MockFilterProviderProps = PartialPropsWithChildren; export const MockFilterProvider = ({ children, @@ -64,9 +62,7 @@ export const MockFilterProvider = ({ ); }; -export type MockLoadingProviderProps = PartialPropsWithChildren< - LoadingContextProps ->; +export type MockLoadingProviderProps = PartialPropsWithChildren; export const MockLoadingProvider = ({ children, @@ -84,9 +80,7 @@ export const MockLoadingProvider = ({ ); }; -export type MockConfigProviderProps = PartialPropsWithChildren< - ConfigContextProps ->; +export type MockConfigProviderProps = PartialPropsWithChildren; export const MockConfigProvider = ({ children, @@ -106,9 +100,7 @@ export const MockConfigProvider = ({ ); }; -export type MockCurrencyProviderProps = PartialPropsWithChildren< - CurrencyContextProps ->; +export type MockCurrencyProviderProps = PartialPropsWithChildren; export const MockCurrencyProvider = ({ children, @@ -129,9 +121,7 @@ export const MockCurrencyProvider = ({ ); }; -export type MockBillingDateProviderProps = PartialPropsWithChildren< - BillingDateContextProps ->; +export type MockBillingDateProviderProps = PartialPropsWithChildren; export const MockBillingDateProvider = ({ children, @@ -161,9 +151,7 @@ export const MockScrollProvider = ({ children }: MockScrollProviderProps) => { ); }; -export type MockGroupsProviderProps = PartialPropsWithChildren< - GroupsContextProps ->; +export type MockGroupsProviderProps = PartialPropsWithChildren; export const MockGroupsProvider = ({ children, diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 321b5b557f..faf8ac693f 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -38,7 +38,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/explore-react/src/tools/api.ts b/plugins/explore-react/src/tools/api.ts index 5d96033546..b7c39b4d50 100644 --- a/plugins/explore-react/src/tools/api.ts +++ b/plugins/explore-react/src/tools/api.ts @@ -28,7 +28,6 @@ export type ExploreTool = { image: string; tags?: string[]; lifecycle?: string; - newsTag?: string; }; export interface ExploreToolsConfig { diff --git a/plugins/explore/package.json b/plugins/explore/package.json index e0165cccef..bbf0343ea6 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -52,7 +52,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index 67bee13696..ee06fd39fd 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -14,12 +14,20 @@ * limitations under the License. */ import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { ItemCard, useRouteRef } from '@backstage/core'; +import { Button, ItemCardHeader, useRouteRef } from '@backstage/core'; import { EntityRefLinks, entityRouteParams, getEntityRelations, } from '@backstage/plugin-catalog-react'; +import { + Box, + Card, + CardActions, + CardContent, + CardMedia, + Chip, +} from '@material-ui/core'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; @@ -28,23 +36,39 @@ type DomainCardProps = { }; export const DomainCard = ({ entity }: DomainCardProps) => { - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); const catalogEntityRoute = useRouteRef(catalogEntityRouteRef); - return ( - - } - label="Explore" - href={catalogEntityRoute(entityRouteParams(entity))} + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const url = catalogEntityRoute(entityRouteParams(entity)); + + const owner = ( + ); + + return ( + + + + + + {entity.metadata.tags?.length ? ( + + {entity.metadata.tags.map(tag => ( + + ))} + + ) : null} + {entity.metadata.description} + + + + + + ); }; diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx deleted file mode 100644 index ee06a50427..0000000000 --- a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { DomainEntity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { catalogEntityRouteRef } from '../../routes'; -import { DomainCardGrid } from './DomainCardGrid'; - -describe('', () => { - it('renders a grid of domain cards', async () => { - const entities: DomainEntity[] = [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Domain', - metadata: { - name: 'playback', - }, - spec: { - owner: 'guest', - }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Domain', - metadata: { - name: 'artists', - }, - spec: { - owner: 'guest', - }, - }, - ]; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': catalogEntityRouteRef, - }, - }, - ); - - expect(getByText('artists')).toBeInTheDocument(); - expect(getByText('playback')).toBeInTheDocument(); - }); -}); diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts index 6acf10800a..3511ff023d 100644 --- a/plugins/explore/src/components/DomainCard/index.ts +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { DomainCard } from './DomainCard'; -export { DomainCardGrid } from './DomainCardGrid'; diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx index 182adf70ea..a12811e486 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -18,6 +18,7 @@ import { Content, ContentHeader, EmptyState, + ItemCardGrid, Progress, SupportButton, useApi, @@ -27,47 +28,64 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Button } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { DomainCardGrid } from '../DomainCard'; +import { DomainCard } from '../DomainCard'; -export const DomainExplorerContent = () => { +const Body = () => { const catalogApi = useApi(catalogApiRef); const { value: entities, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities({ filter: { kind: 'domain' }, }); - return response.items as DomainEntity[]; }, [catalogApi]); + if (loading) { + return ; + } + + if (error) { + return ( + + {error.message} + + ); + } + + if (!entities?.length) { + return ( + + Read more + + } + /> + ); + } + + return ( + + {entities.map((entity, index) => ( + + ))} + + ); +}; + +export const DomainExplorerContent = () => { return ( Discover the domains in your ecosystem. - - {loading && } - {error && ( - - {error.message} - - )} - {!loading && !error && (!entities || entities.length === 0) && ( - - Read more - - } - /> - )} - {!loading && entities && } + ); }; diff --git a/plugins/explore/src/components/ToolCard/ToolCard.tsx b/plugins/explore/src/components/ToolCard/ToolCard.tsx index c6d812b098..60f467ca80 100644 --- a/plugins/explore/src/components/ToolCard/ToolCard.tsx +++ b/plugins/explore/src/components/ToolCard/ToolCard.tsx @@ -17,6 +17,7 @@ import { ExploreTool } from '@backstage/plugin-explore-react'; import { BackstageTheme } from '@backstage/theme'; import { + Box, Button, Card, CardActions, @@ -32,14 +33,6 @@ import React from 'react'; // TODO: Align styling between Domain and ToolCard const useStyles = makeStyles(theme => ({ - card: { - display: 'flex', - flexDirection: 'column', - }, - cardActions: { - flexGrow: 1, - alignItems: 'flex-end', - }, media: { height: 128, }, @@ -59,13 +52,6 @@ const useStyles = makeStyles(theme => ({ beta: { backgroundColor: theme.palette.status.warning, }, - domains: { - position: 'relative', - top: theme.spacing(2), - }, - spaceBetween: { - justifyContent: 'space-between', - }, })); type Props = { @@ -76,10 +62,10 @@ type Props = { export const ToolCard = ({ card, objectFit }: Props) => { const classes = useStyles(); - const { title, description, url, image, lifecycle, newsTag, tags } = card; + const { title, description, url, image, lifecycle, tags } = card; return ( - + { })} /> - + {title}{' '} {lifecycle && lifecycle.toLowerCase() !== 'ga' && ( { /> )} - - {description || 'Description missing'} - + {description || 'Description missing'} {tags && ( -
+ {tags.map((item, idx) => ( - + ))} -
+ )}
- - diff --git a/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx b/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx deleted file mode 100644 index 5b6fbd1834..0000000000 --- a/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { ExploreTool } from '@backstage/plugin-explore-react'; -import { BackstageTheme } from '@backstage/theme'; -import { makeStyles } from '@material-ui/core'; -import React from 'react'; -import { ToolCard } from './ToolCard'; - -const useStyles = makeStyles(theme => ({ - container: { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, 296px)', - gridGap: theme.spacing(3), - marginBottom: theme.spacing(6), - }, -})); - -type ToolCardGridProps = { - tools: ExploreTool[]; -}; - -export const ToolCardGrid = ({ tools }: ToolCardGridProps) => { - const classes = useStyles(); - - return ( -
- {tools.map((card: ExploreTool, ix: any) => ( - - ))} -
- ); -}; diff --git a/plugins/explore/src/components/ToolCard/index.ts b/plugins/explore/src/components/ToolCard/index.ts index 84599aa163..805d822a05 100644 --- a/plugins/explore/src/components/ToolCard/index.ts +++ b/plugins/explore/src/components/ToolCard/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { ToolCard } from './ToolCard'; -export { ToolCardGrid } from './ToolCardGrid'; diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx index 8b8b3f0a61..e00606eeb2 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -17,36 +17,55 @@ import { Content, ContentHeader, EmptyState, + ItemCardGrid, Progress, SupportButton, useApi, + WarningPanel, } from '@backstage/core'; import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; import React from 'react'; import { useAsync } from 'react-use'; -import { ToolCardGrid } from '../ToolCard'; +import { ToolCard } from '../ToolCard'; -export const ToolExplorerContent = () => { +const Body = () => { const exploreToolsConfigApi = useApi(exploreToolsConfigRef); - const { value: tools, loading } = useAsync(async () => { + const { value: tools, loading, error } = useAsync(async () => { return await exploreToolsConfigApi.getTools(); }, [exploreToolsConfigApi]); - return ( - - - Discover the tools in your ecosystem. - + if (loading) { + return ; + } - {loading && } - {!loading && (!tools || tools.length === 0) && ( - - )} - {!loading && tools && } - + if (error) { + return ; + } + + if (!tools?.length) { + return ( + + ); + } + + return ( + + {tools.map((tool, index) => ( + + ))} + ); }; + +export const ToolExplorerContent = () => ( + + + Discover the tools in your ecosystem. + + + +); diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 90dc8c52b4..efeec3f2ba 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -51,7 +51,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index b6fa8d7135..c5a7cd1add 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -48,7 +48,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 4741fa2d4d..4c62506e85 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -57,7 +57,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 30807482a4..1126c2a2a0 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -72,6 +72,8 @@ export const RecentWorkflowRunsCard = ({ } }, [error, errorApi]); + const githubHost = hostname || 'github.com'; + return !runs.length ? ( Create new Workflow diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index 762561582c..b5915f400c 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -210,6 +210,12 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { {details.value?.head_commit.id} + + + Workflow + + {details.value?.name} + Status diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index 6a3b88b052..82f7d64484 100644 --- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -41,6 +41,7 @@ import { Entity } from '@backstage/catalog-model'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; export type WorkflowRun = { + workflowName: string; id: string; message: string; url?: string; @@ -86,6 +87,10 @@ const generatedColumns: TableColumn[] = [ ), }, + { + title: 'Workflow', + field: 'workflowName', + }, { title: 'Status', width: '150px', @@ -186,6 +191,8 @@ export const WorkflowRunsTable = ({ branch, }); + const githubHost = hostname || 'github.com'; + return !runs ? ( Create new Workflow diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts index 4a4ded2d94..eb453e6221 100644 --- a/plugins/github-actions/src/components/useWorkflowRuns.ts +++ b/plugins/github-actions/src/components/useWorkflowRuns.ts @@ -58,6 +58,7 @@ export function useWorkflowRuns({ setTotal(workflowRunsData.total_count); // Transformation here return workflowRunsData.workflow_runs.map(run => ({ + workflowName: run.name, message: run.head_commit.message, id: `${run.id}`, onReRunClick: async () => { diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 6b51f338dc..ee1c8c534a 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -49,7 +49,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index ee6a834c4f..67c6c1f541 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -49,9 +49,9 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", - "@types/codemirror": "^0.0.100", + "@types/codemirror": "^0.0.108", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2", "react-router-dom": "6.0.0-beta.0" diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 8c9eff6322..eeabc99d5e 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -54,7 +54,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index cda91a1b33..a35c458cce 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -41,7 +41,7 @@ "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "jest-when": "^3.1.0", "msw": "^0.21.2" diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx index 9cedc5e135..a34cbc9cbc 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx @@ -37,5 +37,10 @@ describe('ConsumerGroupOffsets', () => { expect(rendered.getByText(/consumer/)).toBeInTheDocument(); expect(rendered.getByText('topic1')).toBeInTheDocument(); expect(rendered.getByText('topic2')).toBeInTheDocument(); + + const lag = ( + +data.offsets[1].topicOffset - +data.offsets[1].groupOffset + ).toString(); + expect(rendered.getByText(lag)).toBeInTheDocument(); }); }); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx index 7928393526..90a99be015 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx @@ -57,6 +57,17 @@ const generatedColumns: TableColumn[] = [ return <>{row.groupOffset ?? ''}; }, }, + { + title: 'Lag', + field: 'lag', + render: (row: Partial) => { + let lag = undefined; + if (row.topicOffset && row.groupOffset) { + lag = +row.topicOffset - +row.groupOffset; + } + return <>{lag ?? ''}; + }, + }, ]; type Props = { diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 2537b7c735..a722d74b4d 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -34,6 +34,7 @@ "@backstage/backend-common": "^0.5.5", "@backstage/catalog-model": "^0.7.3", "@backstage/config": "^0.1.3", + "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.13.2", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index 31b6a2022b..4c71e6a839 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { ClusterLocatorMethod } from './src/types'; + export interface Config { kubernetes?: { - serviceLocatorMethod: 'multiTenant'; - clusterLocatorMethods: 'config'[]; - clusters: { - url: string; - name: string; - serviceAccountToken: string | undefined; - authProvider: 'aws' | 'google' | 'serviceAccount'; - }[]; + serviceLocatorMethod: { + type: 'multiTenant'; + }; + clusterLocatorMethods: ClusterLocatorMethod[]; }; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 339f886a53..cb79a3020c 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -24,9 +24,7 @@ describe('ConfigClusterLocator', () => { clusters: [], }); - const sut = ConfigClusterLocator.fromConfig( - config.getConfigArray('clusters'), - ); + const sut = ConfigClusterLocator.fromConfig(config); const result = await sut.getClusters(); @@ -44,9 +42,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig( - config.getConfigArray('clusters'), - ); + const sut = ConfigClusterLocator.fromConfig(config); const result = await sut.getClusters(); @@ -77,9 +73,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig( - config.getConfigArray('clusters'), - ); + const sut = ConfigClusterLocator.fromConfig(config); const result = await sut.getClusters(); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 5ff4581f71..6b925aca8d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -24,11 +24,11 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { this.clusterDetails = clusterDetails; } - static fromConfig(config: Config[]): ConfigClusterLocator { + static fromConfig(config: Config): ConfigClusterLocator { // TODO: Add validation that authProvider is required and serviceAccountToken // is required if authProvider is serviceAccount return new ConfigClusterLocator( - config.map(c => { + config.getConfigArray('clusters').map(c => { return { name: c.getString('name'), url: c.getString('url'), diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts new file mode 100644 index 0000000000..cf555bd8c9 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -0,0 +1,221 @@ +/* + * 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 '@backstage/backend-common'; +import { ConfigReader, Config } from '@backstage/config'; +import { GkeClusterLocator } from './GkeClusterLocator'; + +const mockedListClusters = jest.fn(); + +describe('GkeClusterLocator', () => { + beforeEach(() => { + mockedListClusters.mockRestore(); + }); + describe('config-parsing', () => { + it('should accept missing region', async () => { + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + }); + + GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + expect(mockedListClusters).toBeCalledTimes(0); + }); + it('should not accept missing projectId', async () => { + const config: Config = new ConfigReader({ + type: 'gke', + }); + + expect(() => + GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any), + ).toThrow("Missing required config value at 'projectId'"); + + expect(mockedListClusters).toBeCalledTimes(0); + }); + }); + describe('listClusters', () => { + it('empty clusters returns empty cluster details', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + it('1 cluster returns 1 cluster details', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + it('use region wildcard when no region provided', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/-', + }); + }); + it('2 cluster returns 2 cluster details', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + }, + { + name: 'some-other-cluster', + endpoint: '6.7.8.9', + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + }, + { + authProvider: 'google', + name: 'some-other-cluster', + url: 'https://6.7.8.9', + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + it('Handle errors gracefully', async () => { + mockedListClusters.mockImplementation(() => { + throw new Error('some error'); + }); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + await expect(sut.getClusters()).rejects.toThrow( + 'There was an error retrieving clusters from GKE for projectId=some-project region=some-region : [some error]', + ); + + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts new file mode 100644 index 0000000000..68042d6cf4 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2021 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 { ClusterDetails, KubernetesClustersSupplier } from '..'; +import { Config } from '@backstage/config'; +import * as container from '@google-cloud/container'; + +export class GkeClusterLocator implements KubernetesClustersSupplier { + private readonly projectId: string; + private readonly region: string | undefined; + private readonly client: container.v1.ClusterManagerClient; + + constructor( + projectId: string, + client: container.v1.ClusterManagerClient, + region?: string, + ) { + this.projectId = projectId; + this.region = region; + this.client = client; + } + + static fromConfigWithClient( + config: Config, + client: container.v1.ClusterManagerClient, + ): GkeClusterLocator { + const projectId = config.getString('projectId'); + const region = config.getOptionalString('region'); + return new GkeClusterLocator(projectId, client, region); + } + + static fromConfig(config: Config): GkeClusterLocator { + return GkeClusterLocator.fromConfigWithClient( + config, + new container.v1.ClusterManagerClient(), + ); + } + + async getClusters(): Promise { + const region = this.region ?? '-'; + const request = { + parent: `projects/${this.projectId}/locations/${region}`, + }; + + try { + const [response] = await this.client.listClusters(request); + return (response.clusters ?? []).map(r => ({ + // TODO filter out clusters which don't have name or endpoint + name: r.name ?? 'unknown', + url: `https://${r.endpoint ?? ''}`, + authProvider: 'google', + })); + } catch (e) { + throw new Error( + `There was an error retrieving clusters from GKE for projectId=${this.projectId} region=${region} : [${e.message}]`, + ); + } + } +} diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 63018cd338..d586f90151 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -22,17 +22,22 @@ describe('getCombinedClusterDetails', () => { const config: Config = new ConfigReader( { kubernetes: { - clusters: [ + clusterLocatorMethods: [ { - name: 'cluster1', - serviceAccountToken: 'token', - url: 'http://localhost:8080', - authProvider: 'serviceAccount', - }, - { - name: 'cluster2', - url: 'http://localhost:8081', - authProvider: 'google', + type: 'config', + clusters: [ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ], }, ], }, @@ -40,7 +45,7 @@ describe('getCombinedClusterDetails', () => { 'ctx', ); - const result = await getCombinedClusterDetails(['config'], config); + const result = await getCombinedClusterDetails(config); expect(result).toStrictEqual([ { @@ -59,9 +64,36 @@ describe('getCombinedClusterDetails', () => { }); it('throws an error when using an unsupported cluster locator', async () => { - await expect( - getCombinedClusterDetails(['magic' as any], new ConfigReader({})), - ).rejects.toStrictEqual( + const config: Config = new ConfigReader( + { + kubernetes: { + clusterLocatorMethods: [ + { + type: 'config', + clusters: [ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ], + }, + { + type: 'magic', + }, + ], + }, + }, + 'ctx', + ); + + await expect(getCombinedClusterDetails(config)).rejects.toStrictEqual( new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'), ); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index 9e6558510c..712fa768fd 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -14,29 +14,34 @@ * limitations under the License. */ -import { ClusterDetails, ClusterLocatorMethod } from '..'; +import { ClusterDetails } from '..'; import { Config } from '@backstage/config'; import { ConfigClusterLocator } from './ConfigClusterLocator'; - -export { ConfigClusterLocator } from './ConfigClusterLocator'; +import { GkeClusterLocator } from './GkeClusterLocator'; export const getCombinedClusterDetails = async ( - clusterLocatorMethods: ClusterLocatorMethod[], rootConfig: Config, ): Promise => { return Promise.all( - clusterLocatorMethods.map(clusterLocatorMethod => { - switch (clusterLocatorMethod) { - case 'config': - return ConfigClusterLocator.fromConfig( - rootConfig.getConfigArray('kubernetes.clusters'), - ).getClusters(); - default: - throw new Error( - `Unsupported kubernetes.clusterLocatorMethods: "${clusterLocatorMethod}"`, - ); - } - }), + rootConfig + .getConfigArray('kubernetes.clusterLocatorMethods') + .map(clusterLocatorMethod => { + const type = clusterLocatorMethod.getString('type'); + switch (type) { + case 'config': + return ConfigClusterLocator.fromConfig( + clusterLocatorMethod, + ).getClusters(); + case 'gke': + return GkeClusterLocator.fromConfig( + clusterLocatorMethod, + ).getClusters(); + default: + throw new Error( + `Unsupported kubernetes.clusterLocatorMethods: "${type}"`, + ); + } + }), ) .then(res => { return res.flat(); diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index c91cae27df..842c96b86f 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -29,7 +29,19 @@ describe('router', () => { getKubernetesObjectsByEntity: jest.fn(), } as any; - const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler); + const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler, [ + { + name: 'some-cluster', + authProvider: 'serviceAccount', + url: 'https://localhost:1234', + serviceAccountToken: 'someToken', + }, + { + name: 'some-other-cluster', + url: 'https://localhost:1235', + authProvider: 'google', + }, + ]); app = express().use(router); }); @@ -37,6 +49,25 @@ describe('router', () => { jest.resetAllMocks(); }); + describe('get /clusters', () => { + it('happy path: lists clusters', async () => { + const response = await request(app).get('/clusters'); + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + items: [ + { + name: 'some-cluster', + authProvider: 'serviceAccount', + }, + { + name: 'some-other-cluster', + authProvider: 'google', + }, + ], + }); + }); + }); describe('post /services/:serviceId', () => { it('happy path: lists kubernetes objects without auth in request body', async () => { const result = { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 90bea82077..ac98e571e0 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -25,7 +25,6 @@ import { KubernetesRequestBody, KubernetesServiceLocator, ServiceLocatorMethod, - ClusterLocatorMethod, ClusterDetails, KubernetesClustersSupplier, } from '..'; @@ -43,7 +42,7 @@ const getServiceLocator = ( clusterDetails: ClusterDetails[], ): KubernetesServiceLocator => { const serviceLocatorMethod = config.getString( - 'kubernetes.serviceLocatorMethod', + 'kubernetes.serviceLocatorMethod.type', ) as ServiceLocatorMethod; switch (serviceLocatorMethod) { @@ -61,6 +60,7 @@ const getServiceLocator = ( export const makeRouter = ( logger: Logger, kubernetesFanOutHandler: KubernetesFanOutHandler, + clusterDetails: ClusterDetails[], ): express.Router => { const router = Router(); router.use(express.json()); @@ -81,6 +81,14 @@ export const makeRouter = ( } }); + router.get('/clusters', async (_, res) => { + res.json({ + items: clusterDetails.map(cd => ({ + name: cd.name, + authProvider: cd.authProvider, + })), + }); + }); return router; }; @@ -96,21 +104,18 @@ export async function createRouter( logger, }); - const clusterLocatorMethods = options.config.getStringArray( - 'kubernetes.clusterLocatorMethods', - ) as ClusterLocatorMethod[]; - let clusterDetails: ClusterDetails[]; if (options.clusterSupplier) { clusterDetails = await options.clusterSupplier.getClusters(); } else { - clusterDetails = await getCombinedClusterDetails( - clusterLocatorMethods, - options.config, - ); + clusterDetails = await getCombinedClusterDetails(options.config); } + logger.info( + `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`, + ); + const serviceLocator = getServiceLocator(options.config, clusterDetails); const kubernetesFanOutHandler = new KubernetesFanOutHandler( @@ -119,5 +124,5 @@ export async function createRouter( serviceLocator, ); - return makeRouter(logger, kubernetesFanOutHandler); + return makeRouter(logger, kubernetesFanOutHandler, clusterDetails); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 86191c6d72..4309bc9075 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -146,6 +146,49 @@ export interface KubernetesFetchError { resourcePath?: string; } +export interface ConfigClusterLocatorMethod { + /** + * @visibility frontend + */ + type: 'config'; + clusters: { + /** + * @visibility frontend + */ + url: string; + /** + * @visibility frontend + */ + name: string; + /** + * @visibility secret + */ + serviceAccountToken: string | undefined; + /** + * @visibility frontend + */ + authProvider: 'aws' | 'google' | 'serviceAccount'; + }[]; +} + +export interface GKEClusterLocatorMethod { + /** + * @visibility frontend + */ + type: 'gke'; + /** + * @visibility frontend + */ + projectId: string; + /** + * @visibility frontend + */ + region?: string; +} + +export type ClusterLocatorMethod = + | ConfigClusterLocatorMethod + | GKEClusterLocatorMethod; + export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http -export type ClusterLocatorMethod = 'config'; export type AuthProviderType = 'google' | 'serviceAccount' | 'aws'; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 47be4bcc84..fc1aa59d5f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -55,7 +55,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/kubernetes/schema.d.ts b/plugins/kubernetes/schema.d.ts index 39598edb39..ce8b94e917 100644 --- a/plugins/kubernetes/schema.d.ts +++ b/plugins/kubernetes/schema.d.ts @@ -13,33 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { ClusterLocatorMethod } from '@backstage/plugin-kubernetes-backend'; + export interface Config { kubernetes?: { /** * @visibility frontend */ - serviceLocatorMethod: 'multiTenant'; + serviceLocatorMethod: { + /** + * @visibility frontend + */ + type: 'multiTenant'; + }; /** * @visibility frontend */ - clusterLocatorMethods: 'config'[]; - clusters: { - /** - * @visibility frontend - */ - url: string; - /** - * @visibility frontend - */ - name: string; - /** - * @visibility secret - */ - serviceAccountToken: string | undefined; - /** - * @visibility frontend - */ - authProvider: 'aws' | 'google' | 'serviceAccount'; - }[]; + clusterLocatorMethods: ClusterLocatorMethod[]; }; } diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 353f5e8800..1eadec3fb0 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -14,44 +14,26 @@ * limitations under the License. */ -import { ConfigApi, DiscoveryApi, IdentityApi } from '@backstage/core'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { KubernetesApi } from './types'; import { KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; -import { Config } from '@backstage/config'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; - private readonly configApi: ConfigApi; constructor(options: { discoveryApi: DiscoveryApi; identityApi: IdentityApi; - configApi: ConfigApi; }) { this.discoveryApi = options.discoveryApi; this.identityApi = options.identityApi; - this.configApi = options.configApi; } - private async getRequired( - path: string, - requestBody: KubernetesRequestBody, - ): Promise { - const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; - const idToken = await this.identityApi.getIdToken(); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(idToken && { Authorization: `Bearer ${idToken}` }), - }, - body: JSON.stringify(requestBody), - }); - + private async handleResponse(response: Response): Promise { if (!response.ok) { const payload = await response.text(); let message; @@ -69,16 +51,40 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } + private async postRequired( + path: string, + requestBody: KubernetesRequestBody, + ): Promise { + const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; + const idToken = await this.identityApi.getIdToken(); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(requestBody), + }); + + return this.handleResponse(response); + } + async getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise { - return await this.getRequired( + return await this.postRequired( `/services/${requestBody.entity.metadata.name}`, requestBody, ); } - getClusters(): Config[] { - return this.configApi.getConfigArray('kubernetes.clusters'); + async getClusters(): Promise<{ name: string; authProvider: string }[]> { + const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`; + + const response = await fetch(url, { + method: 'GET', + }); + + return (await this.handleResponse(response)).items; } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index d9ed79e720..da1b36989a 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; import { createApiRef } from '@backstage/core'; import { KubernetesRequestBody, @@ -31,5 +30,5 @@ export interface KubernetesApi { getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise; - getClusters(): Config[]; + getClusters(): Promise<{ name: string; authProvider: string }[]>; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index ec08267579..c61676fc06 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -23,7 +23,6 @@ import { Grid, Typography, } from '@material-ui/core'; -import { Config } from '@backstage/config'; import { Content, Page, @@ -168,16 +167,14 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { >(undefined); const [error, setError] = useState(undefined); - const clusters: Config[] = kubernetesApi.getClusters(); - const allAuthProviders: string[] = clusters.map(c => - c.getString('authProvider'), - ); - const authProviders: string[] = [...new Set(allAuthProviders)]; - const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef); useEffect(() => { (async () => { + const clusters = await kubernetesApi.getClusters(); + const authProviders: string[] = [ + ...new Set(clusters.map(c => c.authProvider)), + ]; // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider let requestBody: KubernetesRequestBody = { entity, diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index 5bd253d2c1..2bed2838b9 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -23,12 +23,13 @@ export interface KubernetesAuthProvider { ): Promise; } -export const kubernetesAuthProvidersApiRef = createApiRef< - KubernetesAuthProvidersApi ->({ - id: 'plugin.kubernetes-auth-providers.service', - description: 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders', -}); +export const kubernetesAuthProvidersApiRef = createApiRef( + { + id: 'plugin.kubernetes-auth-providers.service', + description: + 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders', + }, +); export interface KubernetesAuthProvidersApi { decorateRequestBodyForAuth( diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index 6da1abb4b6..278bd95e85 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -21,7 +21,6 @@ import { identityApiRef, googleAuthApiRef, createRoutableExtension, - configApiRef, } from '@backstage/core'; import { KubernetesBackendClient } from './api/KubernetesBackendClient'; import { kubernetesApiRef } from './api/types'; @@ -41,10 +40,9 @@ export const kubernetesPlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef, - configApi: configApiRef, }, - factory: ({ discoveryApi, identityApi, configApi }) => - new KubernetesBackendClient({ discoveryApi, identityApi, configApi }), + factory: ({ discoveryApi, identityApi }) => + new KubernetesBackendClient({ discoveryApi, identityApi }), }), createApiFactory({ api: kubernetesAuthProvidersApiRef, diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 050a894b19..bc47b97a42 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -53,7 +53,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/react": "^16.9", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index 03b42a38b9..2ff7797935 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -125,9 +125,10 @@ export class LighthouseRestApi implements LighthouseApi { return await resp.json(); } - async getWebsiteList({ limit, offset }: LASListRequest = {}): Promise< - WebsiteListResponse - > { + async getWebsiteList({ + limit, + offset, + }: LASListRequest = {}): Promise { const params = new URLSearchParams(); if (typeof limit === 'number') params.append('limit', limit.toString()); if (typeof offset === 'number') params.append('offset', offset.toString()); diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index dba1531b02..346b6c0afe 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -48,7 +48,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/org/package.json b/plugins/org/package.json index fe94402bc5..39be6d7cb0 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -42,7 +42,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index e60e22a8f2..a73216b23b 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -125,14 +125,13 @@ export const MembersListCard = (_props: { kind: 'User', }, }); - const groupMembersList = ((membersList.items as unknown) as Array< - UserEntity - >).filter(member => - member?.relations?.some( - r => - r.type === RELATION_MEMBER_OF && - r.target.name.toLowerCase() === groupName.toLowerCase(), - ), + const groupMembersList = ((membersList.items as unknown) as Array).filter( + member => + member?.relations?.some( + r => + r.type === RELATION_MEMBER_OF && + r.target.name.toLowerCase() === groupName.toLowerCase(), + ), ); return groupMembersList; }, [catalogApi]); diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index a4873f03d0..1483799191 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -53,7 +53,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2", "node-fetch": "^2.6.1" diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index ab293cc9b7..6c47043602 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -127,9 +127,9 @@ describe('buildMiddleware', () => { 'user-agent': 'mocked', cookie: 'mocked', } as Partial; - const expectedHeaders = { ...testHeaders } as Partial< - http.IncomingHttpHeaders - >; + const expectedHeaders = { + ...testHeaders, + } as Partial; delete expectedHeaders.cookie; expect(testHeaders).toBeDefined(); @@ -160,9 +160,9 @@ describe('buildMiddleware', () => { authorization: 'mocked', cookie: 'mocked', } as Partial; - const expectedHeaders = { ...testHeaders } as Partial< - http.IncomingHttpHeaders - >; + const expectedHeaders = { + ...testHeaders, + } as Partial; delete expectedHeaders.cookie; expect(testHeaders).toBeDefined(); @@ -192,9 +192,9 @@ describe('buildMiddleware', () => { cookie: 'mocked', 'x-auth-request-user': 'mocked', } as Partial; - const expectedHeaders = { ...testHeaders } as Partial< - http.IncomingHttpHeaders - >; + const expectedHeaders = { + ...testHeaders, + } as Partial; delete expectedHeaders['x-auth-request-user']; expect(testHeaders).toBeDefined(); diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 252e172138..a90c1b5763 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -52,7 +52,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 6668163855..2a2398b088 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -55,7 +55,7 @@ "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "@types/react": "^16.9", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js index 5bde7a7c68..433f8417d0 100644 --- a/plugins/scaffolder-backend/migrations/20210120143715_init.js +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -17,7 +17,7 @@ // @ts-check /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { await knex.schema.createTable('tasks', table => { @@ -72,7 +72,7 @@ exports.up = async function up(knex) { }; /** - * @param {import('knex')} knex + * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { if (knex.client.config.client !== 'sqlite3') { diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index a3c48c6ef5..ff249c4568 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -55,7 +55,7 @@ "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", - "knex": "^0.21.6", + "knex": "^0.95.1", "luxon": "^1.26.0", "morgan": "^1.10.0", "uuid": "^8.2.0", diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json index 6ae3e63a17..75925bf567 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/package.json @@ -24,7 +24,7 @@ "@spotify/tsconfig": "^5.0.0", "@spotify/web-scripts": "^5.0.0", "@testing-library/react": "^8.0.1", - "@types/node": "^13.1.4", + "@types/node": "^14.14.32", "@types/react": "^16.8.7", "@types/react-dom": "^16.8.2", "@types/styled-components": "^4.1.18", diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts index 5c809d229e..fc36eb64e5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.test.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { parseLocationAnnotation, joinGitUrlPath } from './helpers'; import { - TemplateEntityV1alpha1, LOCATION_ANNOTATION, + TemplateEntityV1alpha1, } from '@backstage/catalog-model'; +import { joinGitUrlPath, parseLocationAnnotation } from './helpers'; describe('Helpers', () => { describe('parseLocationAnnotation', () => { @@ -30,7 +30,7 @@ describe('Helpers', () => { name: 'graphql-starter', title: 'GraphQL Service', description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', + 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', generation: 1, @@ -78,7 +78,7 @@ describe('Helpers', () => { name: 'graphql-starter', title: 'GraphQL Service', description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', + 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', generation: 1, @@ -108,11 +108,13 @@ describe('Helpers', () => { expect(() => parseLocationAnnotation(mockEntity)).toThrow( expect.objectContaining({ - name: 'InputError', - message: `Failure to parse either protocol or location for entity: ${mockEntity.metadata.name}`, + name: 'TypeError', + message: + "Unable to parse location reference ':https://github.com/o/r/blob/master/template.yaml', expected ':', e.g. 'url:https://host/path'", }), ); }); + it('should throw an error when the location part is not set in the location annotation', () => { const mockEntity: TemplateEntityV1alpha1 = { apiVersion: 'backstage.io/v1alpha1', @@ -124,7 +126,7 @@ describe('Helpers', () => { name: 'graphql-starter', title: 'GraphQL Service', description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', + 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', generation: 1, @@ -154,8 +156,8 @@ describe('Helpers', () => { expect(() => parseLocationAnnotation(mockEntity)).toThrow( expect.objectContaining({ - name: 'InputError', - message: `Failure to parse either protocol or location for entity: ${mockEntity.metadata.name}`, + name: 'TypeError', + message: `Unable to parse location reference 'github:', expected ':', e.g. 'url:https://host/path'`, }), ); }); @@ -216,7 +218,7 @@ describe('Helpers', () => { name: 'graphql-starter', title: 'GraphQL Service', description: - 'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n', + 'A GraphQL starter template for backstage to get you up and running\nthe best practices with GraphQL\n', uid: '9cf16bad-16e0-4213-b314-c4eec773c50b', etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2', generation: 1, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts index 784e72ca87..924b9f5811 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { posix as posixPath } from 'path'; -import { - TemplateEntityV1alpha1, - LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; import { InputError } from '@backstage/backend-common'; +import { + LOCATION_ANNOTATION, + parseLocationReference, + TemplateEntityV1alpha1, +} from '@backstage/catalog-model'; +import { posix as posixPath } from 'path'; export type ParsedLocationAnnotation = { protocol: 'file' | 'url'; @@ -30,29 +31,16 @@ export const parseLocationAnnotation = ( entity: TemplateEntityV1alpha1, ): ParsedLocationAnnotation => { const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - if (!annotation) { throw new InputError( `No location annotation provided in entity: ${entity.metadata.name}`, ); } - // split on the first colon for the protocol and the rest after the first split - // is the location. - const [protocol, location] = annotation.split(/:(.+)/) as [ - ('file' | 'url')?, - string?, - ]; - - if (!protocol || !location) { - throw new InputError( - `Failure to parse either protocol or location for entity: ${entity.metadata.name}`, - ); - } - + const { type, target } = parseLocationReference(annotation); return { - protocol, - location, + protocol: type as 'file' | 'url', + location: target, }; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index d069008282..f3d7766d86 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -20,7 +20,7 @@ import { NotFoundError, resolvePackagePath, } from '@backstage/backend-common'; -import Knex from 'knex'; +import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { DbTaskEventRow, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts new file mode 100644 index 0000000000..b598b91036 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2021 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 { + getVoidLogger, + SingleConnectionDatabaseManager, +} from '@backstage/backend-common'; +import { TaskWorker } from './TaskWorker'; +import os from 'os'; +import { ConfigReader, JsonObject } from '@backstage/config'; +import { StorageTaskBroker } from './StorageTaskBroker'; +import { DatabaseTaskStore } from './DatabaseTaskStore'; +import { createTemplateAction, TemplateActionRegistry } from '../actions'; + +async function createStore(): Promise { + const manager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('scaffolder'); + return await DatabaseTaskStore.create(await manager.getClient()); +} + +describe('TaskWorker', () => { + let storage: DatabaseTaskStore; + + beforeAll(async () => { + storage = await createStore(); + }); + + const logger = getVoidLogger(); + const actionRegistry = new TemplateActionRegistry(); + actionRegistry.register({ + id: 'test-action', + handler: async ctx => { + ctx.output('testOutput', 'winning'); + }, + }); + + it('should fail when action does not exist', async () => { + const broker = new StorageTaskBroker(storage, logger); + const taskWorker = new TaskWorker({ + logger, + workingDirectory: os.tmpdir(), + actionRegistry, + taskBroker: broker, + }); + const { taskId } = await broker.dispatch({ + steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + values: {}, + }); + const task = await broker.claim(); + await taskWorker.runOneTask(task); + const { events } = await storage.listEvents({ taskId }); + const event = events.find(e => e.type === 'completion'); + + expect((event?.body?.error as JsonObject)?.message).toBe( + "Template action with ID 'not-found-action' is not registered.", + ); + }); + + it('should template output', async () => { + const broker = new StorageTaskBroker(storage, logger); + const taskWorker = new TaskWorker({ + logger, + workingDirectory: os.tmpdir(), + actionRegistry, + taskBroker: broker, + }); + + const { taskId } = await broker.dispatch({ + steps: [{ id: 'test', name: 'test', action: 'test-action' }], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + values: {}, + }); + + const task = await broker.claim(); + await taskWorker.runOneTask(task); + + const { events } = await storage.listEvents({ taskId }); + const event = events.find(e => e.type === 'completion'); + expect((event?.body?.output as JsonObject).result).toBe('winning'); + }); + + it('should template input', async () => { + const inputAction = createTemplateAction<{ + name: string; + }>({ + id: 'test-input', + schema: { + input: { + type: 'object', + required: ['name'], + properties: { + name: { + title: 'name', + description: 'Enter name', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + if (ctx.input.name !== 'winning') { + throw new Error( + `expected name to be "winning" got ${ctx.input.name}`, + ); + } + }, + }); + actionRegistry.register(inputAction); + + const broker = new StorageTaskBroker(storage, logger); + const taskWorker = new TaskWorker({ + logger, + workingDirectory: os.tmpdir(), + actionRegistry, + taskBroker: broker, + }); + + const { taskId } = await broker.dispatch({ + steps: [ + { id: 'test', name: 'test', action: 'test-action' }, + { + id: 'test-input', + name: 'test-input', + action: 'test-input', + input: { + name: '{{ steps.test.output.testOutput }}', + }, + }, + ], + output: { + result: '{{ steps.test.output.testOutput }}', + }, + values: {}, + }); + + const task = await broker.claim(); + await taskWorker.runOneTask(task); + + const { events } = await storage.listEvents({ taskId }); + const event = events.find(e => e.type === 'completion'); + expect((event?.body?.output as JsonObject).result).toBe('winning'); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 4d8f652ae0..bd0a418598 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -98,9 +98,9 @@ export class TaskWorker { throw new Error(`Action '${step.action}' does not exist`); } - const input = JSON.parse( - JSON.stringify(step.input), - (_key, value) => { + const input = + step.input && + JSON.parse(JSON.stringify(step.input), (_key, value) => { if (typeof value === 'string') { return handlebars.compile(value, { noEscape: true, @@ -110,8 +110,7 @@ export class TaskWorker { })(templateCtx); } return value; - }, - ); + }); if (action.schema?.input) { const validateResult = validateJsonSchema(input, action.schema, { diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index 905d85c35d..cc0e2f3aaf 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -14,15 +14,16 @@ * limitations under the License. */ -import os from 'os'; -import fs from 'fs-extra'; -import { Logger } from 'winston'; -import { Config } from '@backstage/config'; import { Entity, LOCATION_ANNOTATION, + parseLocationReference, SOURCE_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import fs from 'fs-extra'; +import os from 'os'; +import { Logger } from 'winston'; export async function getWorkingDirectory( config: Config, @@ -64,16 +65,13 @@ export function getEntityBaseUrl(entity: Entity): string | undefined { return undefined; } - const [type, url] = location.split(/:(.+)/); - if (!url) { - return undefined; + const { type, target } = parseLocationReference(location); + if (type === 'url') { + return target; + } else if (type === 'file') { + return `file://${target}`; } - if (type === 'url') { - return url; - } else if (type === 'file') { - return `file://${url}`; - } // Only url and file location are handled, as we otherwise don't know if // what the url is pointing to makes sense to use as a baseUrl return undefined; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 53716fd1dc..eef66a45ca 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -66,7 +66,7 @@ "@types/humanize-duration": "^3.18.1", "@testing-library/react-hooks": "^3.3.0", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index ebce35a91f..8e8ba15ddc 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,14 +14,13 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { configApiRef, Content, ContentHeader, Header, + ItemCardGrid, Lifecycle, Page, Progress, @@ -30,14 +29,16 @@ import { WarningPanel, } from '@backstage/core'; import { useStarredEntities } from '@backstage/plugin-catalog-react'; -import { Box, Button, Link, makeStyles, Typography } from '@material-ui/core'; +import { Button, Link, makeStyles, Typography } from '@material-ui/core'; import StarIcon from '@material-ui/icons/Star'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; -import { TemplateCard, TemplateCardProps } from '../TemplateCard'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { ScaffolderFilter } from '../ScaffolderFilter'; import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter'; import SearchToolbar from '../SearchToolbar/SearchToolbar'; +import { TemplateCard, TemplateCardProps } from '../TemplateCard'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -46,12 +47,6 @@ const useStyles = makeStyles(theme => ({ gridTemplateColumns: '250px 1fr', gridColumnGap: theme.spacing(2), }, - templateGrid: { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))', - gridAutoRows: '1fr', - gridGap: theme.spacing(2), - }, })); const getTemplateCardProps = ( @@ -183,13 +178,13 @@ export const ScaffolderPageContents = () => { )} - + {matchingEntities && matchingEntities?.length > 0 && matchingEntities.map(template => ( ))} - + diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 26d5c79cc7..25f89f20ab 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,29 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, useRouteRef } from '@backstage/core'; +import { Button, ItemCardHeader, useRouteRef } from '@backstage/core'; import { BackstageTheme, pageTheme } from '@backstage/theme'; import { + Box, Card, CardActions, CardContent, CardMedia, Chip, makeStyles, - Typography, useTheme, } from '@material-ui/core'; import React from 'react'; import { generatePath } from 'react-router'; import { rootRouteRef } from '../../routes'; -const useStyles = makeStyles(theme => ({ - header: { - color: theme.palette.common.white, - padding: theme.spacing(2, 2, 3), - backgroundImage: (props: { backgroundImage: string }) => - props.backgroundImage, - backgroundPosition: 0, +const useStyles = makeStyles({ + title: { + backgroundImage: ({ backgroundImage }: any) => backgroundImage, }, description: { overflow: 'hidden', @@ -44,14 +40,7 @@ const useStyles = makeStyles(theme => ({ '-webkit-line-clamp': 10, '-webkit-box-orient': 'vertical', }, - card: { - display: 'flex', - flexDirection: 'column', - }, - cardContent: { - flexGrow: 1, - }, -})); +}); export type TemplateCardProps = { description: string; @@ -79,18 +68,21 @@ export const TemplateCard = ({ }); return ( - - - {type} - {title} + + + - - {tags?.map(tag => ( - - ))} - - {description} - + + + {tags?.map(tag => ( + + ))} + + {description} + + + ))} + ); diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx index a2369dcfc1..8128366392 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.test.tsx @@ -15,44 +15,49 @@ */ import React from 'react'; import { TechDocsPageHeader } from './TechDocsPageHeader'; -import { render, act } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { act } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; describe('', () => { it('should render a techdocs page header', async () => { await act(async () => { - const rendered = render( - wrapInTestApp( - , - ), + }, + }} + />, + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, + }, ); + expect(rendered.container.innerHTML).toContain('header'); expect(rendered.getAllByText('test-site-name')).toHaveLength(2); expect(rendered.getByText('test-site-desc')).toBeDefined(); @@ -61,27 +66,65 @@ describe('', () => { it('should render a techdocs page header even if metadata is missing', async () => { await act(async () => { - const rendered = render( - wrapInTestApp( - , - ), + const rendered = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, + }, ); expect(rendered.container.innerHTML).toContain('header'); }); }); + + it('should render a link back to the component page', async () => { + await act(async () => { + const rendered = await renderInTestApp( + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + }, + }, + ); + + expect(rendered.container.innerHTML).toContain( + '/catalog/test-namespace/test/test-name', + ); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx index c994b52422..bfab2f8515 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx @@ -17,9 +17,10 @@ import React from 'react'; import { AsyncState } from 'react-use/lib/useAsync'; import CodeIcon from '@material-ui/icons/Code'; -import { EntityName } from '@backstage/catalog-model'; -import { Header, HeaderLabel, Link } from '@backstage/core'; +import { EntityName, parseEntityName } from '@backstage/catalog-model'; +import { Header, HeaderLabel, Link, useRouteRef } from '@backstage/core'; import { TechDocsMetadata } from '../../types'; +import { EntityRefLink, entityRouteRef } from '@backstage/plugin-catalog-react'; type TechDocsPageHeaderProps = { entityId: EntityName; @@ -41,7 +42,7 @@ export const TechDocsPageHeader = ({ const { value: techdocsMetadataValues } = techdocsMetadata; const { value: entityMetadataValues } = entityMetadata; - const { kind, name } = entityId; + const { name } = entityId; const { site_name: siteName, site_description: siteDescription } = techdocsMetadataValues || {}; @@ -51,19 +52,39 @@ export const TechDocsPageHeader = ({ spec: { owner, lifecycle }, } = entityMetadataValues || { spec: {} }; - const componentLink = `/catalog/${kind}/${name}`; + const componentLink = useRouteRef(entityRouteRef); + + let ownerEntity; + if (owner) { + ownerEntity = parseEntityName(owner, { defaultKind: 'group' }); + } const labels = ( <> + {name} } /> - {owner ? : null} + {owner ? ( + + ) : ( + owner + ) + } + /> + ) : null} {lifecycle ? : null} {locationMetadata && locationMetadata.type !== 'dir' && @@ -92,7 +113,7 @@ export const TechDocsPageHeader = ({ siteDescription && siteDescription !== 'None' ? siteDescription : '' } type={name} - typeLink={componentLink} + typeLink={componentLink(entityId)} > {labels} diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 1435961141..7afdf2d43f 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -48,7 +48,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index ae0d066029..d7d6fb6e61 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -48,7 +48,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", - "@types/node": "^12.0.0", + "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, diff --git a/yarn.lock b/yarn.lock index 9b02391c7b..cfbdaf15c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1896,6 +1896,7 @@ "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.3" "@backstage/core" "^0.7.0" + "@backstage/integration" "^0.5.0" "@backstage/plugin-catalog-react" "^0.1.1" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" @@ -1918,6 +1919,7 @@ "@backstage/catalog-client" "^0.3.6" "@backstage/catalog-model" "^0.7.3" "@backstage/core" "^0.7.0" + "@backstage/integration" "^0.5.0" "@backstage/plugin-catalog-react" "^0.1.1" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" @@ -2346,6 +2348,23 @@ query-string "^6.13.3" xcase "^2.0.1" +"@google-cloud/common@^0.32.0": + version "0.32.1" + resolved "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz#6a32c340172cea3db6674d0e0e34e78740a0073f" + integrity sha512-bLdPzFvvBMtVkwsoBtygE9oUm3yrNmPa71gvOgucYI/GqvNP2tb6RYsDHPq98kvignhcgHGDI5wyNgxaCo8bKQ== + dependencies: + "@google-cloud/projectify" "^0.3.3" + "@google-cloud/promisify" "^0.4.0" + "@types/request" "^2.48.1" + arrify "^2.0.0" + duplexify "^3.6.0" + ent "^2.2.0" + extend "^3.0.2" + google-auth-library "^3.1.1" + pify "^4.0.1" + retry-request "^4.0.0" + teeny-request "^3.11.3" + "@google-cloud/common@^3.5.0": version "3.5.0" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz#0959e769e8075a06eb0823cc567eef00fd0c2d02" @@ -2361,6 +2380,23 @@ retry-request "^4.1.1" teeny-request "^7.0.0" +"@google-cloud/container@^2.2.0": + version "2.2.0" + resolved "https://registry.npmjs.org/@google-cloud/container/-/container-2.2.0.tgz#e97ae1cee9040b6af09cc8199ed0aa2d4ae6238e" + integrity sha512-O9xoAGo2qwBezcpIdKcp/zvSX2jUuTRISwXp26dOS+2jeJKpC8jWYU7aRUC8DGxvkEMK9wWESMe44XEBWnHq2Q== + dependencies: + google-gax "^2.9.2" + +"@google-cloud/paginator@^0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-0.2.0.tgz#eab2e6aa4b81df7418f6c51e2071f64dab2c2fa5" + integrity sha512-2ZSARojHDhkLvQ+CS32K+iUhBsWg3AEw+uxtqblA7xoCABDyhpj99FPp35xy6A+XlzMhOSrHHaxFE+t6ZTQq0w== + dependencies: + arrify "^1.0.1" + extend "^3.0.1" + split-array-stream "^2.0.0" + stream-events "^1.0.4" + "@google-cloud/paginator@^3.0.0": version "3.0.5" resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c" @@ -2369,16 +2405,53 @@ arrify "^2.0.0" extend "^3.0.2" +"@google-cloud/projectify@^0.3.3": + version "0.3.3" + resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-0.3.3.tgz#bde9103d50b20a3ea3337df8c6783a766e70d41d" + integrity sha512-7522YHQ4IhaafgSunsFF15nG0TGVmxgXidy9cITMe+256RgqfcrfWphiMufW+Ou4kqagW/u3yxwbzVEW3dk2Uw== + "@google-cloud/projectify@^2.0.0": version "2.0.1" resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== +"@google-cloud/promisify@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-0.4.0.tgz#4fbfcf4d85bb6a2e4ccf05aa63d2b10d6c9aad9b" + integrity sha512-4yAHDC52TEMCNcMzVC8WlqnKKKq+Ssi2lXoUg9zWWkZ6U6tq9ZBRYLHHCRdfU+EU9YJsVmivwGcKYCjRGjnf4Q== + "@google-cloud/promisify@^2.0.0": version "2.0.3" resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz#f934b5cdc939e3c7039ff62b9caaf59a9d89e3a8" integrity sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw== +"@google-cloud/storage@^2.4.3": + version "2.5.0" + resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-2.5.0.tgz#9dd3566d8155cf5ba0c212208f69f9ecd47fbd7e" + integrity sha512-q1mwB6RUebIahbA3eriRs8DbG2Ij81Ynb9k8hMqTPkmbd8/S6Z0d6hVvfPmnyvX9Ej13IcmEYIbymuq/RBLghA== + dependencies: + "@google-cloud/common" "^0.32.0" + "@google-cloud/paginator" "^0.2.0" + "@google-cloud/promisify" "^0.4.0" + arrify "^1.0.0" + async "^2.0.1" + compressible "^2.0.12" + concat-stream "^2.0.0" + date-and-time "^0.6.3" + duplexify "^3.5.0" + extend "^3.0.0" + gcs-resumable-upload "^1.0.0" + hash-stream-validation "^0.2.1" + mime "^2.2.0" + mime-types "^2.0.8" + onetime "^5.1.0" + pumpify "^1.5.1" + snakeize "^0.1.0" + stream-events "^1.0.1" + teeny-request "^3.11.3" + through2 "^3.0.0" + xdg-basedir "^3.0.0" + "@google-cloud/storage@^5.6.0": version "5.6.0" resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.6.0.tgz#bc6925c7970c375212a4da21c123298fc9665dec" @@ -2834,6 +2907,23 @@ is-promise "4.0.0" tslib "~2.0.1" +"@grpc/grpc-js@~1.2.0": + version "1.2.10" + resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.2.10.tgz#f316d29a45fcc324e923d593cb849d292b1ed598" + integrity sha512-wj6GkNiorWYaPiIZ767xImmw7avMMVUweTvPFg4mJWOxz2180DKwfuxhJJZ7rpc1+7D3mX/v8vJdxTuIo71Ieg== + dependencies: + "@types/node" ">=12.12.47" + google-auth-library "^6.1.1" + semver "^6.2.0" + +"@grpc/proto-loader@^0.5.1": + version "0.5.6" + resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz#1dea4b8a6412b05e2d58514d507137b63a52a98d" + integrity sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + "@hapi/hoek@^9.0.0": version "9.0.4" resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010" @@ -4641,7 +4731,7 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.20", "@storybook/addons@^6.1.11": +"@storybook/addons@6.1.20": version "6.1.20" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.20.tgz#da01dabd6692919b719fcb30519d53ea80887097" integrity sha512-kIhXYgF+ARNpYxO3qhz8yThDvKpaq+HDst8odPU9sCNEI66PSH6hrILhTmnffNnqdtY3LnKkU9rGVfZn+3TOTA== @@ -4656,6 +4746,21 @@ global "^4.3.2" regenerator-runtime "^0.13.7" +"@storybook/addons@^6.1.11": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.21.tgz#94bb66fc51d1dfee80d0fe84f5b83c10045651b5" + integrity sha512-xo5TGu9EZVCqgh3D1veVnfuGzyKDWWsvOMo18phVqRxj21G3/+hScVyfIYwNTv7Ys5/Ahp9JxJUMXL3V3ny+tw== + dependencies: + "@storybook/api" "6.1.21" + "@storybook/channels" "6.1.21" + "@storybook/client-logger" "6.1.21" + "@storybook/core-events" "6.1.21" + "@storybook/router" "6.1.21" + "@storybook/theming" "6.1.21" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.7" + "@storybook/api@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.11.tgz#1e0b798203df823ac21184386258cf8b5f17f440" @@ -4756,6 +4861,31 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/api@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.21.tgz#be753ca8d3602efe4a11783c81c689463bee0825" + integrity sha512-QjZk70VSXMw/wPPoWdMp5Bl9VmkfmGhIz8PALrFLLEZHjzptpfZE2qkGEEJHG0NAksFUv6NxGki2/632dzR7Ug== + dependencies: + "@reach/router" "^1.3.3" + "@storybook/channels" "6.1.21" + "@storybook/client-logger" "6.1.21" + "@storybook/core-events" "6.1.21" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.1.21" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.1.21" + "@types/reach__router" "^1.3.7" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + regenerator-runtime "^0.13.7" + store2 "^2.7.1" + telejson "^5.0.2" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.15.tgz#80ea2346d18496f9710dd7f87fd2a9eca46ef36f" @@ -4818,6 +4948,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/channels@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.21.tgz#adbfae5f4767234c5b17d9578be983584dddead4" + integrity sha512-7WoizMjyHqCyvcWncLexSg9FLPIErWAZL4NvluEthwsHSO2sDybn9mh1pzsFHdYMuTP6ml06Zt9ayWMtIveHDg== + dependencies: + core-js "^3.0.1" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-api@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.15.tgz#8f8ead111459b94621571bdb2276f8a0aace17b1" @@ -4898,6 +5037,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.21.tgz#fe7d9e645ddb4eb9dc18fdacea24b4baf11bc6c9" + integrity sha512-QJV+gnVM2fQ4M7lSkRLCXkOw/RU+aEtUefo9TAnXxPHK3UGG+DyvLmha6fHGaz9GAcFxyWtgqCyVOhMe03Q35g== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.15.tgz#b4a2af23ee6b9cba4c255191eae3d3463e29bfb7" @@ -5005,6 +5152,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.21.tgz#11f537f78f8c73ba5e627b57b282a279793a3511" + integrity sha512-KWqnh1C7M1pT//WfQb3AD60yTR8jL48AfaeLGto2gO9VK7VVgj/EGsrXZP/GTL90ygyExbbBI5gkr7EBTu/HYw== + dependencies: + core-js "^3.0.1" + "@storybook/core@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa" @@ -5205,6 +5359,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.21.tgz#0a822fa9cc67589a082f7a10fff15c8413f17706" + integrity sha512-m75WvUhoCBWDVekICAdbkidji/w5hCjHo+M8L13UghpwXWEnyr4/QqvkOb/PcSC8aZzxeMqSCpRQ1o6LWULneg== + dependencies: + "@reach/router" "^1.3.3" + "@types/reach__router" "^1.3.7" + core-js "^3.0.1" + global "^4.3.2" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/semver@^7.3.2": version "7.3.2" resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0" @@ -5302,6 +5468,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.1.21": + version "6.1.21" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.21.tgz#b8e612e5a39b77f7e63a5f9ea322ed62adb0d5b0" + integrity sha512-yq7+/mpdljRdSRJYw/In/9tnDGXIUDe//mhyMftFfrB2mq6zi1yAZpowCerWhiDE2ipGkrfzIYx/Sn7bcaXgqg== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.23" + "@storybook/client-logger" "6.1.21" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + resolve-from "^5.0.0" + ts-dedent "^2.0.0" + "@storybook/ui@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6" @@ -5666,10 +5850,10 @@ dependencies: "@types/node" "*" -"@types/codemirror@^0.0.100": - version "0.0.100" - resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.100.tgz#3ebdf1514208d73f38c25925d9cf9399a42a7471" - integrity sha512-4jGmu1T8vpQrJCe8cbe3KveiJmK2UAt3rZO2qE2sPoMhGLuwW0cMzFYJLyXebbRJg5G3RbuUXLip1IHPUESkFA== +"@types/codemirror@^0.0.108": + version "0.0.108" + resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz#e640422b666bf49251b384c390cdeb2362585bde" + integrity sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw== dependencies: "@types/tern" "*" @@ -5982,9 +6166,9 @@ "@types/uglify-js" "*" "@types/html-webpack-plugin@*", "@types/html-webpack-plugin@^3.2.2": - version "3.2.3" - resolved "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.3.tgz#865323e30e82560c0ca898dbf9f6f9d1c541cd7f" - integrity sha512-Y7dsVhTn75IaD4lMIY02UP1L8e0ou8KQu8DKPJAegEFKdJR28/8ejayDG8ykfR0DtYCx3dCEHIkdpN8AOB6txQ== + version "3.2.4" + resolved "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.4.tgz#ed770ddfec53ed2aa6b5f4523acca291192235c6" + integrity sha512-WM0s78bfCIXnTlICf+8nWP0IvP+fn4YfiI3uxAX1K1PSRpzs0iysp03j4zR0xTgxSqF67TbOsHs49YXonRAkeQ== dependencies: "@types/html-minifier" "*" "@types/tapable" "*" @@ -6185,7 +6369,7 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== -"@types/long@^4.0.0": +"@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== @@ -6297,20 +6481,30 @@ resolved "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c" integrity sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA== +"@types/node@>=12.12.47": + version "14.14.31" + resolved "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" + integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== + "@types/node@^10.1.0", "@types/node@^10.12.0": version "10.17.35" resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== -"@types/node@^12.0.0", "@types/node@^12.7.1": +"@types/node@^12.7.1": version "12.12.58" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== -"@types/node@^13.7.2": - version "13.13.15" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" - integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== +"@types/node@^13.7.0": + version "13.13.45" + resolved "https://registry.npmjs.org/@types/node/-/node-13.13.45.tgz#e6676bcca092bae5751d015f074a234d5a82eb63" + integrity sha512-703YTEp8AwQeapI0PTXDOj+Bs/mtdV/k9VcTP7z/de+lx6XjFMKdB+JhKnK+6PZ5za7omgZ3V6qm/dNkMj/Zow== + +"@types/node@^14.14.32": + version "14.14.32" + resolved "https://registry.npmjs.org/@types/node/-/node-14.14.32.tgz#90c5c4a8d72bbbfe53033f122341343249183448" + integrity sha512-/Ctrftx/zp4m8JOujM5ZhwzlWLx22nbQJiVqz8/zE15gOeEW+uly3FSX4fGFpcfEvFzXcMCJwq9lGVWgyARXhg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -6396,6 +6590,13 @@ dependencies: "@types/express" "*" +"@types/pkgcloud@^1.7.4": + version "1.7.4" + resolved "https://registry.npmjs.org/@types/pkgcloud/-/pkgcloud-1.7.4.tgz#ccce313cd584623787581d2d4384f6f911c06b64" + integrity sha512-syikaJpSx59aAxGd0i/FX5VgFLlO8OSxZLUVhwOhJqx+CRsdgaWB9TqIm5kSqJxLoyIird63QLt96Px63rHnOw== + dependencies: + "@types/node" "*" + "@types/pluralize@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c" @@ -6576,7 +6777,7 @@ resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y= -"@types/request@^2.47.1": +"@types/request@^2.47.1", "@types/request@^2.48.1": version "2.48.5" resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== @@ -7200,6 +7401,13 @@ abbrev@1: resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-2.0.3.tgz#b174827a732efadff81227ed4b8d1cc569baf20a" + integrity sha512-EPSq5wr2aFyAZ1PejJB32IX9Qd4Nwus+adnp7STYFM5/23nLPBazqZ1oor6ZqbH+4otaaGXTlC8RN5hq3C8w9Q== + dependencies: + event-target-shim "^5.0.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -7288,6 +7496,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^4.1.3: version "4.1.4" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" @@ -7561,7 +7776,7 @@ apollo-server-caching@^0.5.3: dependencies: lru-cache "^6.0.0" -apollo-server-core@^2.16.1, apollo-server-core@^2.21.0: +apollo-server-core@^2.21.0: version "2.21.0" resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.21.0.tgz#12ee11aee61fa124f11b1d73cae2e068112a3a53" integrity sha512-GtIiq2F0dVDLzzIuO5+dK/pGq/sGxYlKCqAuQQqzYg0fvZ7fukyluXtcTe0tMI+FJZjU0j0WnKgiLsboCoAlPQ== @@ -7605,7 +7820,7 @@ apollo-server-errors@^2.4.2: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz#1128738a1d14da989f58420896d70524784eabe5" integrity sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ== -apollo-server-express@^2.16.1: +apollo-server-express@^2.16.1, apollo-server-express@^2.21.0: version "2.21.0" resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.21.0.tgz#29bd4ec728e1992da240c5956c3ce6d95c1d252e" integrity sha512-zbOSNGuxUjlOFZnRrbMpga3pKDEroitF4NAqoVxgBivx7v2hGsE7rljct3PucTx2cMN90AyYe3cU4oA8jBxZIQ== @@ -7645,15 +7860,16 @@ apollo-server-types@^0.6.3: apollo-server-env "^3.0.0" apollo-server@^2.16.1: - version "2.16.1" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.16.1.tgz#edc319606eb29f73132239bdc005dd88ac40a142" - integrity sha512-oy9NVRzGwlpQ+W1DwLKRH+KASmodSYpvYIRY5DMAZtGqNmT2zOCpbIZVjBt23SuPB5NhIhhE4ROzoObRv3zy5w== + version "2.21.0" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-2.21.0.tgz#4e62131885b4a8a26bb8b5e77177bd0d4d210852" + integrity sha512-OqngjOSB0MEH6VKGWHcrqt4y39HlhYh9CrMvn4PhadTt53IPYRmBglk5qSRA8xMorGqy60iKrOReqj5YfCjTOg== dependencies: - apollo-server-core "^2.16.1" - apollo-server-express "^2.16.1" + apollo-server-core "^2.21.0" + apollo-server-express "^2.21.0" express "^4.0.0" graphql-subscriptions "^1.0.0" - graphql-tools "^4.0.0" + graphql-tools "^4.0.8" + stoppable "^1.1.0" apollo-tracing@^0.12.2: version "0.12.2" @@ -7793,11 +8009,6 @@ array-differ@^3.0.0: resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - array-equal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" @@ -7832,11 +8043,6 @@ array-includes@^3.0.3, array-includes@^3.1.1: es-abstract "^1.17.0" is-string "^1.0.5" -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== - array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -7887,7 +8093,7 @@ array.prototype.map@^1.0.1: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.4" -arrify@^1.0.1: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= @@ -7990,7 +8196,7 @@ async@0.9.x: resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= -async@^2.6.1, async@^2.6.2: +async@^2.0.1, async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -8042,6 +8248,21 @@ autoprefixer@^9.7.2: postcss "^7.0.32" postcss-value-parser "^4.1.0" +aws-sdk@^2.382.0: + version "2.849.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.849.0.tgz#7fc9863e04f9e97c1f81f204fca84dfb2e4175dd" + integrity sha512-CzpK+0TcycRUzYKOdL5AnFcRmLDty+POWR+ZTeqCIY/rho3mM6jBTSseFreZTFiNxj8lIBBrMTsZ45KZ7Xm2Dg== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sdk@^2.840.0: version "2.840.0" resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.840.0.tgz#f5529c9bd3bf0be7f8e855a23ff9c12b1705418f" @@ -9788,6 +10009,16 @@ command-exists@^1.2.9: resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== +commander@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" + integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= + +commander@2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" + integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= + commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -9803,11 +10034,16 @@ commander@^5.0.0, commander@^5.1.0: resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.1.0, commander@^6.2.0: +commander@^6.1.0: version "6.2.1" resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" + integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== + common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -9945,6 +10181,18 @@ config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" +configstore@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" + integrity sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ== + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + configstore@^5.0.0, configstore@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" @@ -10382,6 +10630,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -10946,6 +11199,11 @@ date-and-time@^0.14.0: resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz#969634697b78956fb66b8be6fb0f39fbd631f2f6" integrity sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA== +date-and-time@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237" + integrity sha512-lcWy3AXDRJOD7MplwZMmNSRM//kZtJaLz4n6D1P5z9wEmZGBKhJRBIr1Xs9KNQJmdXPblvgffynYji4iylUTcA== + date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" @@ -10976,6 +11234,13 @@ debounce@^1.2.0: resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== +debug@2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= + dependencies: + ms "0.7.1" + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -11227,11 +11492,6 @@ destroy@~1.0.4: resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -11317,6 +11577,11 @@ diff3@0.0.3: resolved "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz#d4e5c3a4cdf4e5fe1211ab42e693fcb4321580fc" integrity sha1-1OXDpM305f4SEatC5pP8tDIVgPw= +diff@1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= + diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -11564,6 +11829,13 @@ dot-case@^3.0.3: no-case "^3.0.3" tslib "^1.10.0" +dot-prop@^4.1.0: + version "4.2.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" + integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== + dependencies: + is-obj "^1.0.0" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -11653,7 +11925,7 @@ duplexer@^0.1.1, duplexer@~0.1.1: resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= -duplexify@^3.4.2, duplexify@^3.6.0: +duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== @@ -11886,6 +12158,11 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" +errs@^0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz#798099b2dbd37ca2bc749e538a7c1307d0b50499" + integrity sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk= + es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: version "1.17.4" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" @@ -11988,6 +12265,18 @@ es6-iterator@^2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + es6-shim@^0.35.5: version "0.35.5" resolved "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab" @@ -12011,10 +12300,10 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -esbuild@^0.8.16: - version "0.8.16" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.16.tgz#8ae34b15d938e8b8b5ac2459414fe3e7fd7dd6b2" - integrity sha512-HMvPNxDIhEGO/YUh8oO8oxQ1g+ttWz2anUF7NJmQglj2XfJS8zd8mP0Sb2y+jE1SVk3UjD/rYhdsEOFULN9/xw== +esbuild@^0.8.56: + version "0.8.57" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.57.tgz#a42d02bc2b57c70bcd0ef897fe244766bb6dd926" + integrity sha512-j02SFrUwFTRUqiY0Kjplwjm1psuzO1d6AjaXKuOR9hrY0HuPsT6sV42B6myW34h1q4CRy+Y3g4RU/cGJeI/nNA== escalade@^3.0.1: version "3.0.2" @@ -12036,6 +12325,11 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-regexp@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" + integrity sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE= + escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" @@ -12376,6 +12670,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter2@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452" + integrity sha1-YZegldX7a1folC9v1+qtY6CclFI= + eventemitter2@^6.4.2: version "6.4.3" resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" @@ -12538,13 +12837,6 @@ expand-template@^2.0.3: resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - expect@^24.8.0: version "24.9.0" resolved "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" @@ -12650,7 +12942,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@3.0.2, extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@3.0.2, extend@^3.0.0, extend@^3.0.1, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -12713,7 +13005,7 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-deep-equal@2.0.1: +fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= @@ -12752,6 +13044,13 @@ fast-json-parse@^1.0.3: resolved "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== +fast-json-patch@^2.1.0: + version "2.2.1" + resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz#18150d36c9ab65c7209e7d4eb113f4f8eaabe6d9" + integrity sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-patch@^3.0.0-1: version "3.0.0-1" resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz#4c68f2e7acfbab6d29d1719c44be51899c93dabb" @@ -12777,7 +13076,7 @@ fast-shallow-equal@^1.0.0: resolved "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== -fast-text-encoding@^1.0.0: +fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== @@ -12926,6 +13225,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filed-mimefix@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/filed-mimefix/-/filed-mimefix-0.1.3.tgz#0b0b67d075a63fc74f26fdf39c7f9d4314967bb5" + integrity sha1-Cwtn0HWmP8dPJv3znH+dQxSWe7U= + dependencies: + mime "^1.4.0" + filefy@0.1.10: version "0.1.10" resolved "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz#174677c8e2fa5bc39a3af0ed6fb492f16b8fbf42" @@ -13049,32 +13355,6 @@ find-yarn-workspace-root2@1.2.16: micromatch "^4.0.2" pkg-dir "^4.2.0" -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.2.0" - resolved "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" - integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== - dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -flagged-respawn@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" - integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== - flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -13124,13 +13404,6 @@ for-own@^0.1.3: dependencies: for-in "^1.0.1" -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - foreach@^2.0.4: version "2.0.5" resolved "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -13245,15 +13518,6 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@8.1.0, fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" @@ -13284,6 +13548,15 @@ fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -13389,6 +13662,16 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.5.0: + version "1.8.4" + resolved "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz#e08c34fe93c0a9b67a52b7b9e7a64e6435f9a339" + integrity sha512-BoENMnu1Gav18HcpV9IleMPZ9exM+AvUjrAOV4Mzs/vfz2Lu/ABv451iEXByKiMPn2M140uul1txXCg83sAENw== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^2.2.1" + node-fetch "^2.3.0" + gaxios@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a" @@ -13411,6 +13694,14 @@ gaxios@^4.0.0: is-stream "^2.0.0" node-fetch "^2.3.0" +gcp-metadata@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-1.0.0.tgz#5212440229fa099fc2f7c2a5cdcb95575e9b2ca6" + integrity sha512-Q6HrgfrCQeEircnNP3rCcEgiDv7eF9+1B+1MMgpE190+/+0mjQR8PxeOaRgxZWmdDAF9EIryHB9g1moPiw1SbQ== + dependencies: + gaxios "^1.0.2" + json-bigint "^0.3.0" + gcp-metadata@^4.2.0: version "4.2.1" resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" @@ -13419,6 +13710,18 @@ gcp-metadata@^4.2.0: gaxios "^4.0.0" json-bigint "^1.0.0" +gcs-resumable-upload@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-1.1.0.tgz#2b06f5876dcf60f18a309343f79ed951aff01399" + integrity sha512-uBz7uHqp44xjSDzG3kLbOYZDjxxR/UAGbB47A0cC907W6yd2LkcyFDTHg+bjivkHMwiJlKv4guVWcjPCk2zScg== + dependencies: + abort-controller "^2.0.2" + configstore "^4.0.0" + gaxios "^1.5.0" + google-auth-library "^3.0.0" + pumpify "^1.5.1" + stream-events "^1.0.4" + gcs-resumable-upload@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz#67c766a0555d6a352f9651b7603337207167d0de" @@ -13658,6 +13961,14 @@ glob-to-regexp@^0.3.0: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob@3.2.11: + version "3.2.11" + resolved "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" + integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= + dependencies: + inherits "2" + minimatch "0.3" + glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -13695,26 +14006,6 @@ global-modules@2.0.0: dependencies: global-prefix "^3.0.0" -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - global-prefix@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" @@ -13846,6 +14137,21 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" +google-auth-library@^3.0.0, google-auth-library@^3.1.1: + version "3.1.2" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz#ff2f88cd5cd2118a57bd3d5ad3c093c8837fc350" + integrity sha512-cDQMzTotwyWMrg5jRO7q0A4TL/3GWBgO7I7q5xGKNiiFf9SmGY/OJ1YsLMgI2MVHHsEGyrqYnbnmV1AE+Z6DnQ== + dependencies: + base64-js "^1.3.0" + fast-text-encoding "^1.0.0" + gaxios "^1.2.1" + gcp-metadata "^1.0.0" + gtoken "^2.3.2" + https-proxy-agent "^2.2.1" + jws "^3.1.5" + lru-cache "^5.0.0" + semver "^5.5.0" + google-auth-library@^6.0.0, google-auth-library@^6.1.1: version "6.1.3" resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz#39d868140b70d0c4b32c6f6d8f4ccc1400d84dca" @@ -13861,6 +14167,46 @@ google-auth-library@^6.0.0, google-auth-library@^6.1.1: jws "^4.0.0" lru-cache "^6.0.0" +google-auth-library@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.2.tgz#cab6fc7f94ebecc97be6133d6519d9946ccf3e9d" + integrity sha512-vjyNZR3pDLC0u7GHLfj+Hw9tGprrJwoMwkYGqURCXYITjCrP9HprOyxVV+KekdLgATtWGuDkQG2MTh0qpUPUgg== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^2.9.2: + version "2.10.3" + resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.10.3.tgz#53278bb5cc4b654876ade774a249f0241fdb15cc" + integrity sha512-jESs/ME9WgMzfGQKJDu9ea2mEKjznKByRL+5xb8mKfHlbUfS/LxNLNCg/35RgXwVXcNSCqkEY90z8wHxvgdd/Q== + dependencies: + "@grpc/grpc-js" "~1.2.0" + "@grpc/proto-loader" "^0.5.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.0.2" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + protobufjs "^6.10.2" + retry-request "^4.0.0" + +google-p12-pem@^1.0.0: + version "1.0.4" + resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz#b77fb833a2eb9f7f3c689e2e54f095276f777605" + integrity sha512-SwLAUJqUfTB2iS+wFfSS/G9p7bt4eWcc2LyfvmUXe7cWp6p3mpxDo6LLI29MXdU6wvPcQ/up298X7GMC5ylAlA== + dependencies: + node-forge "^0.8.0" + pify "^4.0.0" + google-p12-pem@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" @@ -14053,7 +14399,7 @@ graphql-tools@5.0.0: tslib "^1.11.1" uuid "^7.0.3" -graphql-tools@^4.0.0, graphql-tools@^4.0.8: +graphql-tools@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz#e7fb9f0d43408fb0878ba66b522ce871bafe9d30" integrity sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg== @@ -14079,11 +14425,27 @@ graphql@15.5.0, graphql@^15.3.0: resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +growl@1.9.2: + version "1.9.2" + resolved "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +gtoken@^2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz#8a7fe155c5ce0c4b71c886cfb282a9060d94a641" + integrity sha512-EaB49bu/TCoNeQjhCYKI/CurooBKkGxIqFHsWABW0b25fobBYVTMe84A8EBVVZhl8emiUdNypil9huMOTmyAnw== + dependencies: + gaxios "^1.0.4" + google-p12-pem "^1.0.0" + jws "^3.1.5" + mime "^2.2.0" + pify "^4.0.0" + gtoken@^5.0.4: version "5.1.0" resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz#4ba8d2fc9a8459098f76e7e8fd7beaa39fda9fe4" @@ -14237,7 +14599,7 @@ hash-base@^3.0.0: inherits "^2.0.1" safe-buffer "^5.0.1" -hash-stream-validation@^0.2.2: +hash-stream-validation@^0.2.1, hash-stream-validation@^0.2.2: version "0.2.4" resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== @@ -14329,13 +14691,6 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - hoopy@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" @@ -14602,6 +14957,14 @@ https-browserify@^1.0.0: resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@^2.2.1: + version "2.2.4" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -15112,6 +15475,13 @@ is-core-module@^2.1.0: dependencies: has "^1.0.3" +is-core-module@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -15307,7 +15677,7 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^1.0.1: +is-obj@^1.0.0, is-obj@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= @@ -15445,6 +15815,11 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" +is-stream-ended@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -15665,6 +16040,14 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jade@0.26.3: + version "0.26.3" + resolved "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" + integrity sha1-jxDXl32NefL2/4YqgbBRPMslaGw= + dependencies: + commander "0.6.1" + mkdirp "0.3.0" + jake@^10.6.1: version "10.8.2" resolved "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" @@ -16299,6 +16682,13 @@ jsesc@~0.5.0: resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-bigint@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz#0c1729d679f580d550899d6a2226c228564afe60" + integrity sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ== + dependencies: + bignumber.js "^9.0.0" + json-bigint@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" @@ -16630,7 +17020,7 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.2.2: +jws@^3.1.5, jws@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -16731,23 +17121,24 @@ klona@^2.0.3: resolved "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== -knex@^0.21.6: - version "0.21.18" - resolved "https://registry.npmjs.org/knex/-/knex-0.21.18.tgz#ca16e95b8b5c0891a6b012fb3fb34185e067b0e2" - integrity sha512-hP4Qyy2LjQLsabQYc6YgDFHb+CJJBjQz1TMPTKyN85CF2NLIsZsF4FDe0NvQXeXMeak3E/kKr5iJog1JBm6ZCQ== +knex@^0.95.1: + version "0.95.1" + resolved "https://registry.npmjs.org/knex/-/knex-0.95.1.tgz#6a99dfebe992829b417144c68d1532ee2b47e90e" + integrity sha512-8vAmH4M6ks0qXHqaIacUOTtGAVc1PPFuF8W/W9bzuUHcQur4809mtufw1LY6n/tNRTLwMFBSXWkUnfFQFqFvNQ== dependencies: colorette "1.2.1" - commander "^6.2.0" + commander "^7.1.0" debug "4.3.1" + escalade "^3.1.1" esm "^3.2.25" getopts "2.2.5" interpret "^2.2.0" - liftoff "3.1.0" - lodash "^4.17.20" + lodash "^4.17.21" pg-connection-string "2.4.0" + rechoir "^0.7.0" + resolve-from "^5.0.0" tarn "^3.0.1" tildify "2.0.0" - v8flags "^3.2.0" kuler@1.0.x: version "1.0.1" @@ -16900,19 +17291,13 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.0" -liftoff@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" - integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== +liboneandone@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/liboneandone/-/liboneandone-1.2.0.tgz#8d2c91c72a6323a96aed8410de05e06d6584b602" + integrity sha512-EB6Ak9qw+U4HAOnKqPtatxQ9pLclvtsBsggrvOuD4zclJ5xOeEASojsLKEC3O8KJ1Q4obE2JHhOeDuqWXvkoUQ== dependencies: - extend "^3.0.0" - findup-sync "^3.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" + mocha "^2.5.3" + request "^2.74.0" lines-and-columns@^1.1.6: version "1.1.6" @@ -17401,6 +17786,11 @@ lowlight@^1.17.0: fault "^1.0.0" highlight.js "~10.4.0" +lru-cache@2: + version "2.7.3" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -17457,6 +17847,13 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -17498,13 +17895,6 @@ make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" -make-iterator@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" - integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== - dependencies: - kind-of "^6.0.2" - makeerror@1.0.x: version "1.0.11" resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -17901,7 +18291,7 @@ micromark@~2.10.0: debug "^4.0.0" parse-entities "^2.0.0" -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -17948,7 +18338,7 @@ mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, m dependencies: mime-db "1.44.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0, mime@^1.4.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -17963,6 +18353,11 @@ mime@^2.3.1, mime@^2.4.4: resolved "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== +mime@^2.4.1: + version "2.5.2" + resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -18020,6 +18415,14 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= +minimatch@0.3: + version "0.3.0" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" + integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -18036,6 +18439,11 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -18195,6 +18603,18 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" +mkdirp@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -18207,6 +18627,22 @@ mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: dependencies: minimist "^1.2.5" +mocha@^2.5.3: + version "2.5.3" + resolved "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" + integrity sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg= + dependencies: + commander "2.3.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.2" + glob "3.2.11" + growl "1.9.2" + jade "0.26.3" + mkdirp "0.5.1" + supports-color "1.2.0" + to-iso-string "0.0.2" + mock-fs@^4.13.0: version "4.13.0" resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-4.13.0.tgz#31c02263673ec3789f90eb7b6963676aa407a598" @@ -18245,6 +18681,11 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +ms@0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -18526,6 +18967,11 @@ node-forge@^0.10.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-forge@^0.8.0: + version "0.8.5" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee" + integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q== + node-gyp-build@~3.7.0: version "3.7.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d" @@ -19010,16 +19456,6 @@ object.assign@^4.1.1: has-symbols "^1.0.1" object-keys "^1.1.1" -object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - object.entries@^1.1.0, object.entries@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" @@ -19047,15 +19483,7 @@ object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0 define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.pick@^1.2.0, object.pick@^1.3.0: +object.pick@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= @@ -19586,7 +20014,7 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-filepath@^1.0.1, parse-filepath@^1.0.2: +parse-filepath@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= @@ -19630,11 +20058,6 @@ parse-package-name@^0.1.0: resolved "https://registry.npmjs.org/parse-package-name/-/parse-package-name-0.1.0.tgz#3f44dd838feb4c2be4bf318bae4477d7706bade4" integrity sha1-P0Tdg4/rTCvkvzGLrkR313BrreQ= -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - parse-path@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.1.tgz#0ec769704949778cb3b8eda5e994c32073a1adff" @@ -20053,7 +20476,7 @@ pify@^3.0.0: resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= -pify@^4.0.1: +pify@^4.0.0, pify@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== @@ -20117,6 +20540,28 @@ pkg-up@3.1.0, pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pkgcloud@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/pkgcloud/-/pkgcloud-2.2.0.tgz#2b4e644b6da53c76c0a30bcc0c3861971011a714" + integrity sha512-ZbbGqJA8gMwR0peq57aNbjzgLbDj52oi59QJEShZmGUl3ckFBZ92j0h/C2L0tJeCb2VE12tnTwmftBgQ0f3gNw== + dependencies: + "@google-cloud/storage" "^2.4.3" + async "^2.6.1" + aws-sdk "^2.382.0" + errs "^0.3.2" + eventemitter2 "^5.0.1" + fast-json-patch "^2.1.0" + filed-mimefix "^0.1.3" + ip "^1.1.5" + liboneandone "^1.2.0" + lodash "^4.17.10" + mime "^2.4.1" + qs "^6.5.2" + request "^2.88.0" + through2 "^3.0.1" + url-join "^4.0.0" + xml2js "^0.4.19" + pkginfo@0.2.x: version "0.2.3" resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" @@ -20671,7 +21116,12 @@ prettier@^1.18.2, prettier@^1.19.1: resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -prettier@^2.0.5, prettier@~2.0.5: +prettier@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + +prettier@~2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== @@ -20856,6 +21306,25 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protobufjs@^6.10.2, protobufjs@^6.8.6: + version "6.10.2" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b" + integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" "^13.7.0" + long "^4.0.0" + protocols@^1.1.0, protocols@^1.4.0: version "1.4.7" resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" @@ -20929,7 +21398,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.3.3: +pumpify@^1.3.3, pumpify@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== @@ -20979,7 +21448,7 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4: +qs@^6.5.1, qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4: version "6.9.6" resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== @@ -21809,7 +22278,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -21882,6 +22351,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +rechoir@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca" + integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q== + dependencies: + resolve "^1.9.0" + recursive-readdir@2.2.2, recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -22218,7 +22694,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.74.0, request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -22298,14 +22774,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - resolve-from@5.0.0, resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -22326,14 +22794,7 @@ resolve-url@^0.2.1: resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.17.0: - version "1.17.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2: version "1.19.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -22341,6 +22802,14 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13. is-core-module "^2.1.0" path-parse "^1.0.6" +resolve@^1.9.0: + version "1.20.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -22384,7 +22853,7 @@ ret@~0.1.10: resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry-request@^4.1.1: +retry-request@^4.0.0, retry-request@^4.1.1: version "4.1.3" resolved "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== @@ -22464,10 +22933,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-dts@1.4.13: - version "1.4.13" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.13.tgz#4f086e84f4fdcc1f49160799ebc66f6b09db292b" - integrity sha512-7mxoQ6PcmCkBE5ZhrjGDL4k42XLy8BkSqpiRi1MipwiGs+7lwi4mQkp2afX+OzzLjJp/TGM8llfe8uayIUhPEw== +rollup-plugin-dts@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-2.0.1.tgz#333f50a637e199a073d490b198746f3c6bd07701" + integrity sha512-y38NSXIY37YExCumbGBTL5dXg7pL7XD+Kbe98iEHWFN9yiKJf7t4kKBOkml5ylUDjQIXBnNClGDeRktc1T5dmA== + dependencies: + magic-string "^0.25.7" optionalDependencies: "@babel/code-frame" "^7.10.4" @@ -22505,17 +22976,6 @@ rollup-plugin-postcss@^3.1.1: safe-identifier "^0.4.1" style-inject "^0.3.0" -rollup-plugin-typescript2@^0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" - integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== - dependencies: - "@rollup/pluginutils" "^3.1.0" - find-cache-dir "^3.3.1" - fs-extra "8.1.0" - resolve "1.17.0" - tslib "2.0.1" - rollup-pluginutils@^2.8.2: version "2.8.2" resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" @@ -22982,6 +23442,11 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -23309,6 +23774,13 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-array-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/split-array-stream/-/split-array-stream-2.0.0.tgz#85a4f8bfe14421d7bca7f33a6d176d0c076a53b1" + integrity sha512-hmMswlVY91WvGMxs0k8MRgq8zb2mSen4FmDNc5AFiTWtrBpdZN6nwD6kROVe4vNL+ywrvbCKsWVCnEd4riELIg== + dependencies: + is-stream-ended "^0.1.4" + split-ca@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" @@ -23883,10 +24355,10 @@ subscriptions-transport-ws@0.9.18, subscriptions-transport-ws@^0.9.11, subscript symbol-observable "^1.0.4" ws "^5.2.0" -sucrase@^3.16.0: - version "3.17.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.17.0.tgz#d9fe5d7e359d884cdb31130358fbdfc18bfb4c24" - integrity sha512-wtiqaokYRjFSSrv8fQu7pThKTIZSLwiffW+PHQG52hlI8eJO47v1tXbKt6fYb8Z1kCyuCkNH9etpTUebb7g+pA== +sucrase@^3.17.1: + version "3.17.1" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.17.1.tgz#b5e35ca7d99db2cc82b3e942934c3746b41ff8e2" + integrity sha512-04cNLFAhS4NBG2Z/MTkLY6HdoBsqErv3wCncymFlfFtnpMthurlWYML2RlID4M2BbiJSu1eZdQnE8Lcz4PCe2g== dependencies: commander "^4.0.0" glob "7.1.6" @@ -23919,6 +24391,11 @@ supertest@^4.0.2: methods "^1.1.2" superagent "^3.8.3" +supports-color@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" + integrity sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4= + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -24172,6 +24649,15 @@ tarn@^3.0.1: resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== +teeny-request@^3.11.3: + version "3.11.3" + resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55" + integrity sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw== + dependencies: + https-proxy-agent "^2.2.1" + node-fetch "^2.2.0" + uuid "^3.3.2" + teeny-request@^7.0.0: version "7.0.1" resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" @@ -24338,6 +24824,14 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" +through2@^3.0.0, through2@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" + integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== + dependencies: + inherits "^2.0.4" + readable-stream "2 || 3" + through2@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" @@ -24453,6 +24947,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +to-iso-string@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" + integrity sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE= + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -24711,11 +25210,6 @@ tslib@2.0.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== -tslib@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" - integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== - tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -25013,6 +25507,13 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= + dependencies: + crypto-random-string "^1.0.0" + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -25183,6 +25684,11 @@ urix@^0.1.0: resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-join@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + url-loader@^4.0.0, url-loader@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" @@ -25352,13 +25858,6 @@ v8-to-istanbul@^7.0.0: convert-source-map "^1.6.0" source-map "^0.7.3" -v8flags@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - valid-url@1.0.9, valid-url@^1.0.9: version "1.0.9" resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" @@ -25794,7 +26293,7 @@ which-pm@2.0.0: load-yaml-file "^0.2.0" path-exists "^4.0.0" -which@1, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@1, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -25936,7 +26435,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.4.2: +write-file-atomic@^2.0.0, write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== @@ -26019,6 +26518,11 @@ xcase@^2.0.1: resolved "https://registry.npmjs.org/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" integrity sha1-x/pyyqD0QNt4/VZzQyA4rJhEULk= +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"